Files
dnsmgr/app/lib/deploy/k8s.php
2025-10-16 23:09:13 +08:00

203 lines
8.6 KiB
PHP

<?php
namespace app\lib\deploy;
use app\lib\DeployInterface;
use Symfony\Component\Yaml\Yaml;
use Exception;
class k8s implements DeployInterface
{
private $logger;
private $kubeconfig;
private $server;
private $bearerToken;
private $tls = [];
private $proxy;
public function __construct($config)
{
$this->kubeconfig = $config['kubeconfig'];
$this->proxy = $config['proxy'] == 1;
}
public function check()
{
if (empty($this->kubeconfig)) throw new Exception('Kubeconfig不能为空');
$this->verify();
}
public function deploy($fullchain, $privatekey, $config, &$info)
{
$namespace = $config['namespace'];
$secretName = $config['secret_name'];
if (empty($namespace)) throw new Exception('命名空间不能为空');
if (empty($secretName)) throw new Exception('Secret名称不能为空');
$this->parse();
$secretPayload = [
'apiVersion' => 'v1',
'kind' => 'Secret',
'metadata' => ['name' => $secretName, 'namespace' => $namespace],
'type' => 'kubernetes.io/tls',
'data' => [
'tls.crt' => base64_encode($config['fullchain']),
'tls.key' => base64_encode($config['privatekey']),
],
];
$secretUrl = '/api/v1/namespaces/' . $namespace . '/secrets/' . $secretName;
list($sCode, $sBody, $sErr) = $this->k8s_request('GET', $secretUrl);
if ($sCode === 404) {
$createUrl = '/api/v1/namespaces/' . $namespace . '/secrets';
$this->log('Secret:' . $secretName . ' 不存在,正在创建...');
list($cCode, $cBody, $cErr) = $this->k8s_request('POST', $createUrl, json_encode($secretPayload));
if ($cCode < 200 || $cCode >= 300) throw new Exception("创建Secret失败 (HTTP $cCode): $cBody | $cErr");
$this->log('Secret:' . $namespace . ' 创建成功');
} elseif ($sCode >= 200 && $sCode < 300) {
$this->log('Secret:' . $secretName . ' 已存在,正在更新...');
$patch = ['data' => $secretPayload['data'], 'type' => 'kubernetes.io/tls'];
list($pCode, $pBody, $pErr) = $this->k8s_request('PATCH', $secretUrl, json_encode($patch));
if ($pCode < 200 || $pCode >= 300) throw new Exception("更新Secret失败 (HTTP $pCode): $pBody | $pErr");
$this->log('Secret:' . $secretName . ' 更新成功');
} else {
throw new Exception("获取Secret失败 (HTTP $sCode): $sBody | $sErr");
}
// Bind Secret to specified Ingresses (merge spec.tls & hosts) ----
if (!empty($config['ingresses'])) {
$ingressUrl = '/apis/networking.k8s.io/v1/namespaces/' . $namespace . '/ingresses';
foreach (explode(',', $config['ingresses']) as $ingName) {
list($gCode, $gBody, $gErr) = $this->k8s_request('GET', $ingressUrl . '/' . $ingName);
if ($gCode < 200 || $gCode >= 300) throw new Exception("获取Ingress '$ingName' 失败 (HTTP $gCode): $gBody | $gErr");
$ing = json_decode($gBody, true);
if (!$ing) throw new Exception("解析Ingress '$ingName' JSON失败: $gBody");
// collect hosts from spec.rules
$hosts = [];
foreach (($ing['spec']['rules'] ?? []) as $rule) {
if (!empty($rule['host'])) $hosts[] = $rule['host'];
}
$hosts = array_values(array_unique($hosts));
// merge/ensure spec.tls entry
$tls = $ing['spec']['tls'] ?? [];
$found = false;
foreach ($tls as &$entry) {
if (($entry['secretName'] ?? '') === $secretName) {
$found = true;
$existingHosts = $entry['hosts'] ?? [];
$entry['hosts'] = array_values(array_unique(array_merge($existingHosts, $hosts)));
}
}
unset($entry);
if (!$found) {
$tls[] = ['secretName' => $secretName, 'hosts' => $hosts];
}
$patch = ['spec' => ['tls' => $tls]];
list($iCode, $iBody, $iErr) = $this->k8s_request('PATCH', $ingressUrl . '/' . $ingName, json_encode($patch, JSON_UNESCAPED_SLASHES));
if ($iCode < 200 || $iCode >= 300) throw new Exception("更新Ingress '$ingName' 失败 (HTTP $iCode): $iBody | $iErr");
$this->log("Ingress '$ingName' 更新TLS成功");
}
}
}
private function parse()
{
$kcfg = Yaml::parse($this->kubeconfig);
if (!$kcfg) throw new Exception('Kubeconfig格式错误');
$curr = $kcfg['current-context'] ?? null;
if (!$curr) throw new Exception('Kubeconfig缺少current-context');
$contexts = $this->index_by_name($kcfg['contexts'] ?? []);
$clusters = $this->index_by_name($kcfg['clusters'] ?? []);
$users = $this->index_by_name($kcfg['users'] ?? []);
$ctx = $contexts[$curr] ?? null;
if (!$ctx) throw new Exception("Kubeconfig中找不到current-context: $curr");
$clusterName = $ctx['context']['cluster'] ?? null;
$userName = $ctx['context']['user'] ?? null;
if (!$clusterName || !$userName) throw new Exception("Kubeconfig中context缺少cluster或user: $curr");
$cluster = $clusters[$clusterName] ?? null;
$user = $users[$userName] ?? null;
if (!$cluster) throw new Exception("Kubeconfig中找不到cluster: $clusterName");
if (!$user) throw new Exception("Kubeconfig中找不到user: $userName");
$this->server = $cluster['cluster']['server'] ?? null;
if (!$this->server) throw new Exception("Kubeconfig中找不到cluster.server");
$this->server = rtrim($this->server, '/');
$this->bearerToken = $user['user']['token'] ?? ($user['user']['auth-provider']['config']['access-token'] ?? null);
$clientCertFile = $clientKeyFile = null;
if (!empty($user['user']['client-certificate-data']) && !empty($user['user']['client-key-data'])) {
$clientCertFile = tempnam(sys_get_temp_dir(), 'kcc_');
$clientKeyFile = tempnam(sys_get_temp_dir(), 'kck_');
file_put_contents($clientCertFile, base64_decode($user['user']['client-certificate-data']));
file_put_contents($clientKeyFile, base64_decode($user['user']['client-key-data']));
} elseif (!empty($user['user']['client-certificate']) && !empty($user['user']['client-key'])) {
$clientCertFile = $user['user']['client-certificate'];
$clientKeyFile = $user['user']['client-key'];
}
$this->tls = ['cert' => $clientCertFile, 'key' => $clientKeyFile];
}
private function verify()
{
$this->parse();
list($vCode, $vBody, $vErr) = $this->k8s_request('GET', '/version');
if ($vErr) throw new Exception("连接Kubernetes API服务器失败: $vErr");
if ($vCode != 200) throw new Exception("连接Kubernetes API服务器失败: HTTP $vCode $vBody");
}
private function k8s_request($method, $path, $body = null)
{
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $this->server . $path);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
$headers = ['Accept: application/json'];
if ($this->bearerToken) $headers[] = 'Authorization: Bearer ' . $this->bearerToken;
if ($body !== null) $headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
if ($body !== null) curl_setopt($ch, CURLOPT_POSTFIELDS, $body);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0);
if (!empty($this->tls['cert']) && !empty($this->tls['key'])) {
curl_setopt($ch, CURLOPT_SSLCERT, $this->tls['cert']);
curl_setopt($ch, CURLOPT_SSLKEY, $this->tls['key']);
}
$resp = curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$err = curl_error($ch);
curl_close($ch);
return [$code, $resp, $err];
}
private function index_by_name($arr)
{
$out = [];
foreach ($arr as $item) {
if (isset($item['name'])) $out[$item['name']] = $item;
}
return $out;
}
public function setLogger($func)
{
$this->logger = $func;
}
private function log($txt)
{
if ($this->logger) {
call_user_func($this->logger, $txt);
}
}
}