203 lines
8.6 KiB
PHP
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);
|
|
}
|
|
}
|
|
}
|