From 3df0658949ab1314a0cbd99405e6fc7c0c8acdd6 Mon Sep 17 00:00:00 2001 From: merlin Date: Mon, 20 Oct 2025 15:09:42 +0800 Subject: [PATCH] ci test --- .gitea/workflows/build.yml | 26 +++++ tls-sync/Dockerfile | 26 +++++ tls-sync/change_log/0.md | 8 ++ tls-sync/job.py | 213 +++++++++++++++++++++++++++++++++++++ tls-sync/readme.md | 42 ++++++++ 5 files changed, 315 insertions(+) create mode 100644 .gitea/workflows/build.yml create mode 100644 tls-sync/Dockerfile create mode 100644 tls-sync/change_log/0.md create mode 100644 tls-sync/job.py create mode 100644 tls-sync/readme.md diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..f7212e4 --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,26 @@ +name: Docker Image CI +on: + push: + branches: + - main + +jobs: + build: + runs-on: controller + steps: + - uses: actions/checkout@v4 + - name: Build the Docker image + image: harbor.merlin.xin/mirrors/gcr.io/kaniko-project/executor:debug + run: | + for df in $(find . -name Dockerfile); do + dir=$(dirname "$df") + image_name="${vars.REGISTRY_URL}/testing/merlin/$(basename $dir):${gitea.sha:0:7}" + /kaniko/executor \ + --dockerfile "$df" \ + --context "$dir" \ + --destination "$image_name" \ + --cache=true \ + --username "${secrets.HARBOR_USER_NAME}" \ + --password "${secrets.HARBOR_USER_SECRET}" + echo "✅ Successfully built and pushed $image_name" + done \ No newline at end of file diff --git a/tls-sync/Dockerfile b/tls-sync/Dockerfile new file mode 100644 index 0000000..c7f682c --- /dev/null +++ b/tls-sync/Dockerfile @@ -0,0 +1,26 @@ +FROM python:3.11-slim + +# 安装依赖 如果有需要,可以取消注释安装网络调试工具 +RUN apt-get update && apt-get install -y \ + # openssh-client \ + # curl \ + # iputils-ping \ + # netcat-openbsd \ + # dnsutils \ + # telnet \ + && rm -rf /var/lib/apt/lists/* + +# 安装 Python 依赖 +RUN pip install --no-cache-dir kubernetes pycryptodome + +# 复制脚本 +COPY job.py /app/job.py +WORKDIR /app + +# 设置环境变量(可在部署时覆盖) +ENV NGINX_HOST=10.0.0.1 +ENV NGINX_USER=nginx +ENV NAMESPACE=basic + +# 默认命令 +CMD ["python", "job.py"] diff --git a/tls-sync/change_log/0.md b/tls-sync/change_log/0.md new file mode 100644 index 0000000..f855797 --- /dev/null +++ b/tls-sync/change_log/0.md @@ -0,0 +1,8 @@ +# 0.0.0 +基础版本,监听目标namespace证书的生成 +针对集群网关为traefik + +# 0.0.1 - 0.0.4 +切换针对集群内部网关为nginx的配置 +监听所有namespace的证书的生成 + diff --git a/tls-sync/job.py b/tls-sync/job.py new file mode 100644 index 0000000..f4c2df4 --- /dev/null +++ b/tls-sync/job.py @@ -0,0 +1,213 @@ +#!/usr/bin/env python3 +import os +import time +import base64 +import logging +import subprocess +from kubernetes import client, config, watch +from kubernetes.client.rest import ApiException +from Crypto.PublicKey import RSA + +# 配置日志 +logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s') + +# 配置 Kubernetes 客户端 +try: + config.load_incluster_config() # 如果在 Pod 内运行 +except Exception as e: + logging.error("Failed to load kube config: %s", e) + exit(1) + +v1 = client.CoreV1Api() + +# 配置 Nginx 主机和目录 +CLUSTER_HOST = "10.0.0.4" +NGINX_HOST = "10.0.0.1" +NGINX_USER = "nginx" +NGINX_CERT_DIR = "/etc/nginx/certs" +NGINX_CONF_DIR = "/etc/nginx/conf.d" +NAMESPACE = "basic" +MAX_RETRIES = 10 # 最大重试次数 +RETRY_DELAY = 20 # 每次重试的时间间隔(秒) + +# 创建目录用于存放临时证书和密钥 +TEMP_DIR = "/tmp/nginx_certs" +RSA_DIR = "/root/.ssh" # 新增的文件夹,用于存放 SSH 密钥对 +PUBLIC_KEY_COMMENT = "generated-key" +os.makedirs(TEMP_DIR, exist_ok=True) +os.makedirs(RSA_DIR, exist_ok=True) + +def generate_ssh_key_pair(): + """ + 生成 SSH 密钥对(私钥为 PEM,公钥为 OpenSSH 单行格式),并保存到 RSA_DIR。 + 私钥权限设为 600,公钥权限设为 644。 + """ + try: + logging.info("Generating SSH key pair...") + + os.makedirs(RSA_DIR, exist_ok=True) + + # 生成 RSA 私钥 + key = RSA.generate(2048) + + # 私钥(PEM,多行) + private_key_bytes = key.export_key(format='PEM') + # 公钥(OpenSSH 单行格式,例如: b"ssh-rsa AAAAB3NzaC1yc2E..."} + public_key_bytes = key.publickey().export_key(format='OpenSSH') + + # 给公钥添加注释(可选),并确保以换行结尾 + if PUBLIC_KEY_COMMENT: + public_key_bytes = public_key_bytes + b' ' + PUBLIC_KEY_COMMENT.encode('utf-8') + public_key_bytes = public_key_bytes + b'\n' + + private_key_path = os.path.join(RSA_DIR, "id_rsa") + public_key_path = os.path.join(RSA_DIR, "id_rsa.pub") + + # 写入私钥与公钥 + with open(private_key_path, "wb") as private_file: + private_file.write(private_key_bytes) + + with open(public_key_path, "wb") as public_file: + public_file.write(public_key_bytes) + + # 设置合理的文件权限 + try: + os.chmod(private_key_path, 0o600) + os.chmod(public_key_path, 0o644) + except PermissionError: + logging.warning("No permission to chmod the key files; please set permissions manually if needed.") + + logging.info(f"SSH key pair saved to {private_key_path} and {public_key_path}") + except Exception as e: + logging.error("Failed to generate SSH key pair: %s", e) + +def upload_cert_with_retry(tmp_crt, tmp_key, domain): + """ + 尝试多次上传证书文件到 Nginx 服务器,直到成功或达到最大重试次数 + """ + retries = 0 + while retries < MAX_RETRIES: + try: + logging.info(f"Attempting to upload certificate for {domain}, attempt {retries + 1}...") + + # 上传证书和私钥,使用 ssh + sudo tee + for local_file, remote_file in [ + (tmp_crt, f"{NGINX_CERT_DIR}/{domain}.pem"), + (tmp_key, f"{NGINX_CERT_DIR}/{domain}.key") + ]: + ssh_command = [ + "ssh", f"{NGINX_USER}@{NGINX_HOST}", + f"sudo tee {remote_file} > /dev/null" + ] + with open(local_file, "rb") as f: + subprocess.run(ssh_command, stdin=f, check=True) + + logging.info(f"Uploaded certificate and key for {domain} to Nginx server.") + return # 上传成功,退出循环 + + except subprocess.CalledProcessError as e: + retries += 1 + logging.error(f"Attempt {retries} failed: {e}. Retrying in {RETRY_DELAY} seconds...") + time.sleep(RETRY_DELAY) + + logging.error(f"Failed to upload certificate for {domain} after {MAX_RETRIES} attempts.") + + +def upload_cert(domain, crt_data, key_data): + """ + 上传证书和私钥到 Nginx 中转服务器,并生成 Nginx 配置文件 + """ + try: + # 临时保存证书和私钥 + tmp_crt = os.path.join(TEMP_DIR, f"{domain}.crt") + tmp_key = os.path.join(TEMP_DIR, f"{domain}.key") + + with open(tmp_crt, "wb") as f: + f.write(base64.b64decode(crt_data)) + with open(tmp_key, "wb") as f: + f.write(base64.b64decode(key_data)) + + logging.info(f"Certificate and key for {domain} saved locally.") + + # 上传证书和私钥 + upload_cert_with_retry(tmp_crt, tmp_key, domain) + + # 生成 Nginx 配置文件 + conf_content = f""" +server {{ + listen 443 ssl http2; + server_name {domain}; + + ssl_certificate /etc/nginx/certs/{domain}.pem; + ssl_certificate_key /etc/nginx/certs/{domain}.key; + + location / {{ + proxy_pass https://{CLUSTER_HOST}; + proxy_ssl_server_name on; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + }} +}} +""" + tmp_conf = os.path.join(TEMP_DIR, f"{domain}.conf") + with open(tmp_conf, "w") as f: + f.write(conf_content) + + # 上传 Nginx 配置文件 + ssh_command = [ + "ssh", f"{NGINX_USER}@{NGINX_HOST}", + f"sudo tee {NGINX_CONF_DIR}/{domain}.conf > /dev/null" + ] + logging.info(f"Uploading Nginx config for {domain} using sudo tee...") + with open(tmp_conf, "rb") as f: + subprocess.run(ssh_command, stdin=f, check=True) + + logging.info(f"Uploaded Nginx config for {domain} to Nginx server via sudo tee.") + + # 重载 Nginx + reload_command = ["ssh", f"{NGINX_USER}@{NGINX_HOST}", "sudo nginx -s reload"] + subprocess.run(reload_command, check=True) + logging.info(f"Nginx reloaded successfully for {domain}.") + + except subprocess.CalledProcessError as e: + logging.error("Error during SSH commands: %s", e) + except Exception as e: + logging.error("Unexpected error: %s", e) + +def watch_secrets_all_ns(): + """ + 监听所有命名空间中的 Secrets,当发现以 -tls 结尾的 cert-manager 生成证书时上传到 Nginx + """ + logging.info("Starting to watch all Kubernetes secrets across namespaces.") + + w = watch.Watch() + try: + # 监听所有命名空间 + for event in w.stream(v1.list_secret_for_all_namespaces): + secret = event['object'] + secret_name = secret.metadata.name + annotations = secret.metadata.annotations or {} + + # 仅处理 cert-manager 生成的 TLS Secret + if secret_name.endswith("-tls") and annotations.get("cert-manager.io/issuer-name"): + if "tls.crt" in secret.data and "tls.key" in secret.data: + domain = secret_name[:-4] # 去掉 -tls 后缀 + logging.info(f"Found TLS secret {secret_name} for domain {domain}, uploading...") + + upload_cert(domain, secret.data["tls.crt"], secret.data["tls.key"]) + + except ApiException as e: + logging.error("Kubernetes API exception: %s", e) + except Exception as e: + logging.error("Unexpected error: %s", e) + exit(1) + + +if __name__ == "__main__": + # 生成 SSH 密钥对 + generate_ssh_key_pair() + + # 开始监听 Secrets 变化 + watch_secrets_all_ns() diff --git a/tls-sync/readme.md b/tls-sync/readme.md new file mode 100644 index 0000000..d5749fa --- /dev/null +++ b/tls-sync/readme.md @@ -0,0 +1,42 @@ +# 使用说明 +建议直接拉取镜像: +``` +docker pull forever4526/tls:{tag} +``` +编写yaml后,使用kubectl apply应用 + +# 镜像运行必备环境变量 + +**中转服务器与pod相关** +CLUSTER_HOST = "10.0.0.4" //集群相对ip +NGINX_HOST = "10.0.0.1" //nginx主机地址 +NGINX_USER = "nginx" //ssh链接nginx主机的用户名 +NGINX_CERT_DIR = "/etc/nginx/certs" //存放证书的目录 +NGINX_CONF_DIR = "/etc/nginx/conf.d" //存放server配置块的目录 +NAMESPACE = "basic" //pod运行的命名空间 +TEMP_DIR = "/tmp/nginx_certs" //临时目录 +RSA_DIR = "/root/.ssh" //存放 SSH 密钥对 +PUBLIC_KEY_COMMENT = "generated-key" //ssh公钥后缀 + +**上传重试相关** +MAX_RETRIES = 10 // 最大重试次数 +RETRY_DELAY = 20 // 每次重试的时间间隔(秒) + +# 创建serviceAccount供其使用 +关键:监听单namespace +``` +rules: + - apiGroups: [""] + resources: ["secrets"] + verbs: ["watch", "get", "list"] +``` + +监听全部namespace需要更高的权限,default用户就无法使用了,关键的rules是一样的,只不过需要自己创建用户,绑定权限,部署时绑定创建的用户即可 + +# 请注意 + +**部署完成之后,需要进入容器,手动复制ssh公钥到中转服务器的对应用户下的.ssh/authorized_keys中** + +**配置完公钥之后,需要在pod中手动进行一次ssh远程连接,目的是为了验证指纹信息(目前我还没自动实现)** + +**可以使用固定的ssh密钥对,只是我的环境走docker hub中转镜像比较方便,所以就写了一个通用的(个人感觉麻烦程度差不多)** \ No newline at end of file