diff --git a/.github/workflows/push_image.yml b/.github/workflows/push_image.yml index 276e5f9f..a5889dcc 100644 --- a/.github/workflows/push_image.yml +++ b/.github/workflows/push_image.yml @@ -14,7 +14,6 @@ env: jobs: build: runs-on: ubuntu-latest - steps: - name: Checkout uses: actions/checkout@v4 diff --git a/.github/workflows/release_sync_gitee.py b/.github/workflows/release_sync_gitee.py new file mode 100644 index 00000000..bacfda72 --- /dev/null +++ b/.github/workflows/release_sync_gitee.py @@ -0,0 +1,276 @@ +#!/usr/bin/env python3 +import logging +import json +import mimetypes +import tempfile +import os +import random +import re +import shutil +import time +from urllib import request +from urllib.error import HTTPError + +GITHUB_REPO = "certimate-go/certimate" +GITEE_REPO = "certimate-go/certimate" +GITEE_TOKEN = os.getenv("GITEE_TOKEN", "") + +SYNC_MARKER = "SYNCING FROM GITHUB, PLEASE WAIT ..." +TEMP_DIR = tempfile.mkdtemp() + +logging.basicConfig(level=logging.INFO) + + +def do_httpreq(url, method="GET", headers=None, data=None): + req = request.Request(url, data=data, method=method) + headers = headers or {} + for key, value in headers.items(): + req.add_header(key, value) + + try: + with request.urlopen(req) as resp: + resp_data = resp.read().decode("utf-8") + if resp_data: + try: + return json.loads(resp_data) + except json.JSONDecodeError: + pass + return None + except HTTPError as e: + errmsg = "" + if e.readable(): + try: + errmsg = e.read().decode('utf-8') + errmsg = errmsg.replace("\r", "\\r").replace("\n", "\\n") + except: + pass + logging.error(f"Error occurred when sending request: status={e.status}, response={errmsg}") + raise e + except Exception as e: + raise e + + +def get_github_stable_release(): + page = 1 + while True: + releases = do_httpreq( + url=f"https://api.github.com/repos/{GITHUB_REPO}/releases?page={page}&per_page=100", + headers={"Accept": "application/vnd.github+json"}, + ) + if not releases or len(releases) == 0: + break + + for release in releases: + release_name = release.get("name", "") + if re.match(r"^v[0-9]", release_name): + if any( + x in release_name + for x in ["alpha", "beta", "rc", "preview", "test", "unstable"] + ): + continue + return release + + page += 1 + + return None + + +def get_gitee_release_list(): + page = 1 + list = [] + while True: + releases = do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}&page={page}&per_page=100", + ) + if not releases or len(releases) == 0: + break + + list.extend(releases) + page += 1 + + return list + + +def get_gitee_release_by_tag(tag_name): + releases = get_gitee_release_list() + for release in releases: + if release.get("tag_name") == tag_name: + return release + + return None + + +def delete_gitee_release(release_info): + if not release_info: + raise ValueError("Release info is invalid") + + release_id = release_info.get("id", "") + release_name = release_info.get("tag_name", "") + if not release_id: + raise ValueError("Release ID is missing") + + attachpage = 1 + attachfiles = [] + while True: + releases = do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}&page={attachpage}&per_page=100", + ) + if not releases or len(releases) == 0: + break + + attachfiles.extend(releases) + attachpage += 1 + + for attachfile in attachfiles: + attachfile_id = attachfile.get("id") + attachfile_name = attachfile.get("name") + logging.info("Trying to delete Gitee attach file: %s/%s", release_name, attachfile_name) + do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files/{attachfile_id}?access_token={GITEE_TOKEN}", + method="DELETE", + ) + + logging.info("Trying to delete Gitee release: %s", release_name) + do_httpreq( + url=f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", + method="DELETE", + ) + + +def create_gitee_release(name, tag, body, prerelease, gh_assets): + release_info = do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases?access_token={GITEE_TOKEN}", + method="POST", + headers={"Content-Type": "application/json"}, + data=json.dumps({ + "tag_name": tag, + "name": name, + "body": SYNC_MARKER, + "prerelease": prerelease, + "target_commitish": "", + }).encode("utf-8"), + ) + + if not release_info or "id" not in release_info: + return None + logging.info("Gitee release created") + + release_id = release_info["id"] + + assets_dir = os.path.join(TEMP_DIR, "assets") + os.makedirs(assets_dir, exist_ok=True) + + gh_assets = gh_assets or [] + for asset in gh_assets: + logging.info("Tring to download asset from GitHub: %s", asset["name"]) + + opener = request.build_opener() + request.install_opener(opener) + download_ts = time.time() + download_url = asset.get("browser_download_url") + download_path = os.path.join(assets_dir, asset["name"]) + def _hook(blocknum, blocksize, totalsize): + nonlocal download_ts + TIMESPAN = 5 # print progress every 5sec + ts = time.time() + pct = min(round(100 * blocknum * blocksize / totalsize, 2), 100) + if (ts - download_ts < TIMESPAN) and (pct < 100): + return + download_ts = ts + logging.info(f"Downloading {download_url} >>> {pct}%") + + request.urlretrieve(download_url, download_path, _hook) + + for asset in gh_assets: + logging.info("Tring to upload asset to Gitee: %s", asset["name"]) + + boundary = '----boundary' + ''.join(random.choice('0123456789abcdef') for _ in range(16)) + print(f"Using boundary: {boundary}") + with open(os.path.join(assets_dir, asset["name"]), 'rb') as f: + attachfile_mime = mimetypes.guess_type(asset["name"])[0] or 'application/octet-stream' + attachfile_req = [] + attachfile_req.append(f"--{boundary}") + attachfile_req.append(f'Content-Disposition: form-data; name="file"; filename="{asset["name"]}"') + attachfile_req.append(f"Content-Type: {attachfile_mime}") + attachfile_req.append("") + attachfile_req.append(f.read().decode('latin-1')) + attachfile_req.append(f"--{boundary}--") + attachfile_req.append("") + attachfile_req = "\r\n".join(attachfile_req).encode('latin-1') + + do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}/attach_files?access_token={GITEE_TOKEN}", + method="POST", + headers={'Content-Type': f'multipart/form-data; boundary={boundary}'}, + data=attachfile_req, + ) + logging.info("Asset uploaded: %s", asset["name"]) + + release_info = do_httpreq( + f"https://gitee.com/api/v5/repos/{GITEE_REPO}/releases/{release_id}?access_token={GITEE_TOKEN}", + method="PATCH", + headers={"Content-Type": "application/json"}, + data=json.dumps({ + "tag_name": tag, + "name": name, + "body": f"**此发行版同步自 GitHub,完整变更日志请访问 https://github.com/{GITHUB_REPO}/releases/{tag} 查看。**\n\n**因 Gitee 存储空间容量有限,仅能保留最新一个发行版,如需其余版本请访问 GitHub 获取。**\n\n---\n\n" + body, + "prerelease": prerelease, + }).encode("utf-8"), + ) + logging.info("Gitee release updated") + return release_info + + +def main(): + try: + # 获取 GitHub 最新稳定发行版 + github_release = get_github_stable_release() + if not github_release: + logging.warning("GitHub stable release not found. Foget to release?") + return + else: + logging.info("GitHub stable release found: %s", github_release.get('name')) + + # 提取稳定版的信息 + release_name = github_release.get("name") + release_tag = github_release.get("tag_name") + release_body = github_release.get("body") + release_prerelease = github_release.get("prerelease", False) + release_assets = github_release.get("assets", []) + + # 检查 Gitee 是否已有同名发行版 + gitee_release = get_gitee_release_by_tag(release_tag) + if gitee_release and gitee_release.get("body") == SYNC_MARKER: + logging.warning("Gitee syncing release found, cleaning up...") + delete_gitee_release(gitee_release) + elif gitee_release: + logging.info("Gitee release already exists, exit.") + return + + # 同步发行版 + gitee_release = create_gitee_release(release_name, release_tag, release_body, release_prerelease, release_assets) + if not gitee_release: + logging.warning("Failed to create Gitee release.") + return + + # 清除历史发行版 + gitee_release_list = get_gitee_release_list() + for release in gitee_release_list: + if release.get("tag_name") == release_tag: + continue + else: + delete_gitee_release(release) + + logging.info("Sync release completed.") + + except Exception as e: + logging.fatal(str(e)) + exit(1) + + finally: + if os.path.exists(TEMP_DIR): + shutil.rmtree(TEMP_DIR) + + +if __name__ == "__main__": + main() diff --git a/.github/workflows/release_sync_gitee.yml b/.github/workflows/release_sync_gitee.yml new file mode 100644 index 00000000..6c3a1dd6 --- /dev/null +++ b/.github/workflows/release_sync_gitee.yml @@ -0,0 +1,27 @@ +name: Release Sync to Gitee + +on: + # release: + # types: [published, unpublished, deleted] + workflow_dispatch: + +jobs: + sync-to-gitee: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up Python3 + uses: actions/setup-python@v5 + with: + python-version: "3.13" + + - name: Run script + env: + GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} + run: | + cd .github/workflows + python ./release_sync_gitee.py diff --git a/go.mod b/go.mod index f9bd4fb6..712a9904 100644 --- a/go.mod +++ b/go.mod @@ -40,6 +40,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.3.0 github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.155 github.com/jdcloud-api/jdcloud-sdk-go v1.64.0 + github.com/kong/go-kong v0.66.1 github.com/libdns/dynv6 v1.0.0 github.com/libdns/libdns v0.2.3 github.com/luthermonson/go-proxmox v0.2.2 @@ -109,12 +110,15 @@ require ( github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.7 // indirect + github.com/imdario/mergo v0.3.12 // indirect github.com/jinzhu/copier v0.3.4 // indirect github.com/josharian/intern v1.0.0 // indirect + github.com/kong/semver/v4 v4.0.1 // indirect github.com/kylelemons/godebug v1.1.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect github.com/magefile/mage v1.14.0 // indirect github.com/mailru/easyjson v0.9.0 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/namedotcom/go v0.0.0-20180403034216-08470befbe04 // indirect github.com/nrdcg/bunny-go v0.0.0-20240207213615-dde5bf4577a3 // indirect @@ -127,6 +131,9 @@ require ( github.com/qiniu/x v1.10.5 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect github.com/sirupsen/logrus v1.9.4-0.20230606125235-dd1b4c2e81af // indirect + github.com/tidwall/gjson v1.18.0 // indirect + github.com/tidwall/match v1.1.1 // indirect + github.com/tidwall/pretty v1.2.0 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect diff --git a/go.sum b/go.sum index e574635c..149f9151 100644 --- a/go.sum +++ b/go.sum @@ -552,6 +552,8 @@ github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.155/go.mod h1:Y/+YLCFCJtS29i2M github.com/hudl/fargo v1.4.0/go.mod h1:9Ai6uvFy5fQNq6VPKtg+Ceq1+eTY4nKUlR2JElEOcDo= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= +github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/influxdata/influxdb1-client v0.0.0-20200827194710-b269163b24ab/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= @@ -596,6 +598,10 @@ github.com/klauspost/compress v1.13.6/go.mod h1:/3/Vjq9QcHkK5uEr5lBEmyoZ1iFhe47e github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= +github.com/kong/go-kong v0.66.1 h1:UVdemzcCpfXEl6O/VHdf0rT2bXdIO5ykuJbf2z1JTko= +github.com/kong/go-kong v0.66.1/go.mod h1:wRMPAXGOB3kn53TF6zN4l2JhIWPUfXDFKNHkMHBB3iQ= +github.com/kong/semver/v4 v4.0.1 h1:DIcNR8W3gfx0KabFBADPalxxsp+q/5COwIFkkhrFQ2Y= +github.com/kong/semver/v4 v4.0.1/go.mod h1:LImQ0oT15pJvSns/hs2laLca2zcYoHu5EsSNY0J6/QA= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/fs v0.1.0 h1:Jskdu9ieNAYnjxsi0LbQp1ulIKZV1LAFgK1tWhpZgl8= @@ -656,6 +662,8 @@ github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eI github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -786,6 +794,8 @@ github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWN github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= +github.com/samber/lo v1.50.0 h1:XrG0xOeHs+4FQ8gJR97zDz5uOFMW7OwFWiFVzqopKgY= +github.com/samber/lo v1.50.0/go.mod h1:RjZyNk6WSnUFRKK6EyOhsRJMqft3G+pg7dCWHQCWvsc= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= @@ -829,8 +839,6 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1187 h1:x2q6BAFm2f+9YaE7/lGPWXL7HzRkovjoqOMbdtRdpBw= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1187/go.mod h1:GoIHP0ayv0QOWN4c9aUEaKi74lY/tbeJz7h5i8y2gdU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1193 h1:zOWZKDVA3kvA5/b+AwKzDtz5ewdiibeKxVqtCFJSTNI= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1193/go.mod h1:ufxDBGyS3X/9QKkZzuOFKLNra9FmSfgAHBO/FlFZaTU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1188 h1:zzaIE12soTfyAgRvBYhb5bYxFXRCelvYXDEfvtkT5Y4= @@ -840,26 +848,18 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1163/go.mod github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1172/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1182/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1183/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1187/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1188/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1189/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1191/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1192 h1:3K6aJXXkjBLxqFYnBqAqFW5YqxmwMT0HR2F4gxQiNMU= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1192/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1193 h1:anxhOjL4WrQDqUcX7eT8VEaQITiKWllKwsH1fEt6lBw= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1193/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128 h1:mrJ5Fbkd7sZIJ5F6oRfh5zebPQaudPH9Y0+GUmFytYU= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1128/go.mod h1:zbsYIBT+VTX4z4ocjTAdLBIWyNYj3z0BRqd0iPdnjsk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163 h1:putqrH5n1SVRqFWHOylVqYI5yLQUjRTkHqZPLT2yeVY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163/go.mod h1:aEWRXlAvovPUUoS3kVB/LVWEQ19WqzTj2lXGvR1YArY= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1192 h1:2430drceaOXASJZyVZ+e7QSzgBfgwSjDEDM5rh4046M= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1192/go.mod h1:JHZLo95Fde/0et2Ag2E5P6VmCZQIq74MClUtanJ4JcY= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1193 h1:VtXqRnzGz3KheXu2msNPvA/fUYQGsVVRC30WgyAUEqg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1193/go.mod h1:42I1OwaedHR6Yvg7J6UYoOjNYUYfFqwaeEkvx3x+NZc= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172 h1:6SUO0hTie3zxnUEMxmhnS1iRIXpAukSZV27Nrx4NwIk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172/go.mod h1:tmN4zfu70SD0iee3qfpc09NRLel30zGoAuzIs4X0Kfs= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1189 h1:Db7gmkey7On70PAohvrna6RMLZzLHRjbALxPlH5JC3c= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1189/go.mod h1:x+WlMCjbePO7M3R0qzKmrpmieUWrtsRpcKBDpxJNQ5A= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1193 h1:tmACSthp5JLjrdxzng6XFs4gfQcZHBTTVlXR0tO6hSk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1193/go.mod h1:LWf5UPUl41EQICrq0jswgQEO/BtRQY+CxAI6X+i709o= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1191 h1:4l1Db+yFh9HgqNynYbG93khxLtXSBwnXZgNmc88jOE0= @@ -868,6 +868,12 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.0.1183 h1:3fvxkF github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/vod v1.0.1183/go.mod h1:d47RTYqj/2xjIk/lmq8bQ9deUwfEQcWhPQxUgqZnz24= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.0.1182 h1:2DaykFM5mXvQBvuhQEU/aOG5amissS31XI1wZh+FeMA= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/waf v1.0.1182/go.mod h1:pTAgdVcS28xFIARJXhg10hx2+g/Q9FqVkAkal3ARNfc= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= +github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= +github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= +github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= +github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= github.com/tjfoc/gmsm v1.3.2/go.mod h1:HaUcFuY0auTiaHB9MHFGCPx5IaLhTUd2atbCFBQXn9w= github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= diff --git a/internal/app/app.go b/internal/app/app.go index 11c81d8d..62de2c9a 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -9,15 +9,21 @@ import ( "github.com/pocketbase/pocketbase/core" ) -var instance core.App - -var intanceOnce sync.Once +var ( + instance core.App + intanceOnce sync.Once +) func GetApp() core.App { intanceOnce.Do(func() { - instance = pocketbase.NewWithConfig(pocketbase.Config{ + pb := pocketbase.NewWithConfig(pocketbase.Config{ HideStartBanner: true, }) + + pb.RootCmd.Flags().MarkHidden("encryptionEnv") + pb.RootCmd.Flags().MarkHidden("queryTimeout") + + instance = pb }) return instance diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 0b2a77bd..61ae8785 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -63,11 +63,13 @@ import ( pJDCloudLive "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/jdcloud-live" pJDCloudVOD "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/jdcloud-vod" pK8sSecret "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/k8s-secret" + pKong "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/kong" pLeCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/lecdn" pLocal "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/local" pNetlifySite "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/netlify-site" pProxmoxVE "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/proxmoxve" pQiniuCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-cdn" + pQiniuKodo "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo" pQiniuPili "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-pili" pRainYunRCDN "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/rainyun-rcdn" pRatPanelConsole "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/ratpanel-console" @@ -924,6 +926,24 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy return deployer, err } + case domain.DeploymentProviderTypeKong: + { + access := domain.AccessConfigForKong{} + if err := xmaps.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + deployer, err := pKong.NewSSLDeployerProvider(&pKong.SSLDeployerProviderConfig{ + ServerUrl: access.ServerUrl, + ApiToken: access.ApiToken, + AllowInsecureConnections: access.AllowInsecureConnections, + ResourceType: pKong.ResourceType(xmaps.GetString(options.ProviderServiceConfig, "resourceType")), + Workspace: xmaps.GetString(options.ProviderServiceConfig, "workspace"), + CertificateId: xmaps.GetString(options.ProviderServiceConfig, "certificateId"), + }) + return deployer, err + } + case domain.DeploymentProviderTypeKubernetesSecret: { access := domain.AccessConfigForKubernetes{} @@ -982,7 +1002,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy } switch options.Provider { - case domain.DeploymentProviderTypeQiniuCDN, domain.DeploymentProviderTypeQiniuKodo: + case domain.DeploymentProviderTypeQiniuCDN: deployer, err := pQiniuCDN.NewSSLDeployerProvider(&pQiniuCDN.SSLDeployerProviderConfig{ AccessKey: access.AccessKey, SecretKey: access.SecretKey, @@ -990,6 +1010,14 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy }) return deployer, err + case domain.DeploymentProviderTypeQiniuKodo: + deployer, err := pQiniuKodo.NewSSLDeployerProvider(&pQiniuKodo.SSLDeployerProviderConfig{ + AccessKey: access.AccessKey, + SecretKey: access.SecretKey, + Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"), + }) + return deployer, err + case domain.DeploymentProviderTypeQiniuPili: deployer, err := pQiniuPili.NewSSLDeployerProvider(&pQiniuPili.SSLDeployerProviderConfig{ AccessKey: access.AccessKey, @@ -1183,7 +1211,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy SecretKey: access.SecretKey, Endpoint: xmaps.GetString(options.ProviderServiceConfig, "endpoint"), ZoneId: xmaps.GetString(options.ProviderServiceConfig, "zoneId"), - Domain: xmaps.GetString(options.ProviderServiceConfig, "domain"), + Domains: xslices.Filter(strings.Split(xmaps.GetString(options.ProviderServiceConfig, "domains"), ";"), func(s string) bool { return s != "" }), }) return deployer, err @@ -1232,7 +1260,7 @@ func createSSLDeployerProvider(options *deployerProviderOptions) (core.SSLDeploy SecretId: access.SecretId, SecretKey: access.SecretKey, Endpoint: xmaps.GetString(options.ProviderServiceConfig, "endpoint"), - CertificiateId: xmaps.GetString(options.ProviderServiceConfig, "certificiateId"), + CertificateId: xmaps.GetString(options.ProviderServiceConfig, "certificateId"), IsReplaced: xmaps.GetBool(options.ProviderServiceConfig, "isReplaced"), ResourceTypes: xslices.Filter(strings.Split(xmaps.GetString(options.ProviderServiceConfig, "resourceTypes"), ";"), func(s string) bool { return s != "" }), ResourceRegions: xslices.Filter(strings.Split(xmaps.GetString(options.ProviderServiceConfig, "resourceRegions"), ";"), func(s string) bool { return s != "" }), diff --git a/internal/domain/access.go b/internal/domain/access.go index 178a9d57..dd63ada1 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -227,6 +227,12 @@ type AccessConfigForJDCloud struct { AccessKeySecret string `json:"accessKeySecret"` } +type AccessConfigForKong struct { + ServerUrl string `json:"serverUrl"` + ApiToken string `json:"apiToken,omitempty"` + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` +} + type AccessConfigForKubernetes struct { KubeConfig string `json:"kubeConfig,omitempty"` } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 6deedee8..d86faa07 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -52,6 +52,7 @@ const ( AccessProviderTypeHetzner = AccessProviderType("hetzner") AccessProviderTypeHuaweiCloud = AccessProviderType("huaweicloud") AccessProviderTypeJDCloud = AccessProviderType("jdcloud") + AccessProviderTypeKong = AccessProviderType("kong") AccessProviderTypeKubernetes = AccessProviderType("k8s") AccessProviderTypeLarkBot = AccessProviderType("larkbot") AccessProviderTypeLetsEncrypt = AccessProviderType("letsencrypt") @@ -236,6 +237,7 @@ const ( DeploymentProviderTypeJDCloudCDN = DeploymentProviderType(AccessProviderTypeJDCloud + "-cdn") DeploymentProviderTypeJDCloudLive = DeploymentProviderType(AccessProviderTypeJDCloud + "-live") DeploymentProviderTypeJDCloudVOD = DeploymentProviderType(AccessProviderTypeJDCloud + "-vod") + DeploymentProviderTypeKong = DeploymentProviderType(AccessProviderTypeKong) DeploymentProviderTypeKubernetesSecret = DeploymentProviderType(AccessProviderTypeKubernetes + "-secret") DeploymentProviderTypeLeCDN = DeploymentProviderType(AccessProviderTypeLeCDN) DeploymentProviderTypeLocal = DeploymentProviderType(AccessProviderTypeLocal) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 04ba1e4f..16895229 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -70,11 +70,11 @@ type WorkflowNodeConfigForApply struct { ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 Provider string `json:"provider"` // DNS 提供商 ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 + ProviderConfig map[string]any `json:"providerConfig,omitempty"` // DNS 提供商额外配置 CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值时使用全局配置) CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 - KeyAlgorithm string `json:"keyAlgorithm"` // 证书算法 + KeyAlgorithm string `json:"keyAlgorithm,omitempty"` // 证书算法 ACMEProfile string `json:"acmeProfile,omitempty"` // ACME Profiles Extension Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 DnsPropagationWait int32 `json:"dnsPropagationWait,omitempty"` // DNS 传播等待时间,等同于 lego 的 `--dns-propagation-wait` 参数 @@ -124,6 +124,7 @@ func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { return WorkflowNodeConfigForApply{ Domains: xmaps.GetString(n.Config, "domains"), ContactEmail: xmaps.GetString(n.Config, "contactEmail"), + ChallengeType: xmaps.GetString(n.Config, "challengeType"), Provider: xmaps.GetString(n.Config, "provider"), ProviderAccessId: xmaps.GetString(n.Config, "providerAccessId"), ProviderConfig: xmaps.GetKVMapAny(n.Config, "providerConfig"), diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index 5f73d308..7f174b15 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -37,7 +37,7 @@ func NewApplyNode(node *domain.WorkflowNode) *applyNode { func (n *applyNode) Process(ctx context.Context) error { nodeCfg := n.node.GetConfigForApply() - n.logger.Info("ready to obtain certificiate ...", slog.Any("config", nodeCfg)) + n.logger.Info("ready to obtain certificate ...", slog.Any("config", nodeCfg)) // 查询上次执行结果 lastOutput, err := n.outputRepo.GetByNodeId(ctx, n.node.Id) @@ -67,7 +67,7 @@ func (n *applyNode) Process(ctx context.Context) error { // 申请证书 applyResult, err := applicant.Apply(ctx) if err != nil { - n.logger.Warn("failed to obtain certificiate") + n.logger.Warn("failed to obtain certificate") return err } diff --git a/main.go b/main.go index 7184d781..8e3477c9 100644 --- a/main.go +++ b/main.go @@ -64,7 +64,6 @@ func main() { app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error { routes.Unregister() - slog.Info("[CERTIMATE] Exit!") return e.Next() }) diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index 1bc0d482..1cc05cf3 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -270,12 +270,12 @@ func init() { Id string `json:"id"` Type string `json:"type"` Name string `json:"name"` - Config map[string]any `json:"config"` - Inputs []map[string]any `json:"inputs"` - Outputs []map[string]any `json:"outputs"` + Config map[string]any `json:"config,omitempty"` + Inputs []map[string]any `json:"inputs,omitempty"` + Outputs []map[string]any `json:"outputs,omitempty"` Next *dWorkflowNode `json:"next,omitempty"` - Branches []dWorkflowNode `json:"branches,omitempty"` - Validated bool `json:"validated"` + Branches []*dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated,omitempty"` } for _, workflowRun := range workflowRuns { diff --git a/migrations/1751961600_upgrade.go b/migrations/1751961600_upgrade.go new file mode 100644 index 00000000..91ac39d2 --- /dev/null +++ b/migrations/1751961600_upgrade.go @@ -0,0 +1,112 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + tracer := NewTracer("(v0.3)1751961600") + tracer.Printf("go ...") + + // migrate data + { + workflows, err := app.FindAllRecords("workflow") + if err != nil { + return err + } + + type dWorkflowNode struct { + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Config map[string]any `json:"config,omitempty"` + Inputs []map[string]any `json:"inputs,omitempty"` + Outputs []map[string]any `json:"outputs,omitempty"` + Next *dWorkflowNode `json:"next,omitempty"` + Branches []*dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated,omitempty"` + } + + deepChangeFn := func(node *dWorkflowNode) bool { + stack := []*dWorkflowNode{node} + + for len(stack) > 0 { + current := stack[len(stack)-1] + stack = stack[:len(stack)-1] + + if current.Type == "deploy" { + configMap := current.Config + if configMap != nil { + if provider, ok := configMap["provider"]; ok { + if provider.(string) == "tencentcloud-eo" { + if providerConfig, ok := configMap["providerConfig"]; ok { + if providerConfigMap, ok := providerConfig.(map[string]any); ok { + if _, ok := providerConfigMap["domain"]; ok { + providerConfigMap["domains"] = providerConfigMap["domain"] + delete(providerConfigMap, "domain") + configMap["providerConfig"] = providerConfigMap + return true + } + } + } + } + } + } + } + + if current.Next != nil { + stack = append(stack, current.Next) + } + + if current.Branches != nil { + for i := len(current.Branches) - 1; i >= 0; i-- { + stack = append(stack, current.Branches[i]) + } + } + } + + return false + } + + for _, workflow := range workflows { + changed := false + + rootNodeContent := &dWorkflowNode{} + if err := workflow.UnmarshalJSONField("content", rootNodeContent); err != nil { + return err + } else { + if deepChangeFn(rootNodeContent) { + workflow.Set("content", rootNodeContent) + changed = true + } + } + + rootNodeDraft := &dWorkflowNode{} + if err := workflow.UnmarshalJSONField("draft", rootNodeDraft); err != nil { + return err + } else { + if deepChangeFn(rootNodeDraft) { + workflow.Set("draft", rootNodeDraft) + changed = true + } + } + + if changed { + err = app.Save(workflow) + if err != nil { + return err + } + + tracer.Printf("record #%s in collection '%s' updated", workflow.Id, workflow.Collection().Name) + } + } + } + + tracer.Printf("done") + return nil + }, func(app core.App) error { + return nil + }) +} diff --git a/pkg/core/ssl-deployer/providers/aliyun-alb/aliyun_alb.go b/pkg/core/ssl-deployer/providers/aliyun-alb/aliyun_alb.go index 391f4b55..2b165967 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-alb/aliyun_alb.go +++ b/pkg/core/ssl-deployer/providers/aliyun-alb/aliyun_alb.go @@ -109,12 +109,12 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: - if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { + if err := d.deployToLoadbalancer(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: - if err := d.deployToListener(ctx, upres.CertId); err != nil { + if err := d.deployToListener(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil { return nil, err } @@ -338,13 +338,13 @@ func (d *SSLDeployerProvider) updateListenerCertificate(ctx context.Context, clo continue } - // 监听证书 ID 格式:${证书 ID}-${地域} - certificateId := strings.Split(tea.StringValue(listenerCertificate.CertificateId), "-")[0] - if certificateId == cloudCertId { + if tea.StringValue(listenerCertificate.CertificateId) == cloudCertId { certificateIsAlreadyAssociated = true break } + // 监听证书 ID 格式:${证书 ID}-${地域} + certificateId := strings.Split(tea.StringValue(listenerCertificate.CertificateId), "-")[0] certificateIdAsInt64, err := strconv.ParseInt(certificateId, 10, 64) if err != nil { errs = append(errs, err) diff --git a/pkg/core/ssl-deployer/providers/aliyun-clb/aliyun_clb.go b/pkg/core/ssl-deployer/providers/aliyun-clb/aliyun_clb.go index dd6f4664..5d5448c4 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-clb/aliyun_clb.go +++ b/pkg/core/ssl-deployer/providers/aliyun-clb/aliyun_clb.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "log/slog" - "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alislb "github.com/alibabacloud-go/slb-20140515/v4/client" @@ -13,7 +12,6 @@ import ( "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-slb" - "github.com/certimate-go/certimate/pkg/utils/ifelse" ) type SSLDeployerProviderConfig struct { @@ -61,10 +59,7 @@ func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProv AccessKeyId: config.AccessKeyId, AccessKeySecret: config.AccessKeySecret, ResourceGroupId: config.ResourceGroupId, - Region: ifelse. - If[string](config.Region == "" || strings.HasPrefix(config.Region, "cn-")). - Then("cn-hangzhou"). - Else("ap-southeast-1"), + Region: config.Region, }) if err != nil { return nil, fmt.Errorf("could not create ssl manager: %w", err) diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go b/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go index 184d978c..10788365 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go +++ b/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -13,7 +13,7 @@ import ( "github.com/alibabacloud-go/tea/tea" "github.com/certimate-go/certimate/pkg/core" - sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-slb" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" "github.com/certimate-go/certimate/pkg/utils/ifelse" ) diff --git a/pkg/core/ssl-deployer/providers/aliyun-nlb/aliyun_nlb.go b/pkg/core/ssl-deployer/providers/aliyun-nlb/aliyun_nlb.go index 6a6a0411..809c7391 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-nlb/aliyun_nlb.go +++ b/pkg/core/ssl-deployer/providers/aliyun-nlb/aliyun_nlb.go @@ -97,12 +97,12 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // 根据部署资源类型决定部署方式 switch d.config.ResourceType { case RESOURCE_TYPE_LOADBALANCER: - if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { + if err := d.deployToLoadbalancer(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil { return nil, err } case RESOURCE_TYPE_LISTENER: - if err := d.deployToListener(ctx, upres.CertId); err != nil { + if err := d.deployToListener(ctx, upres.ExtendedData["certIdentifier"].(string)); err != nil { return nil, err } diff --git a/pkg/core/ssl-deployer/providers/apisix/apisix_test.go b/pkg/core/ssl-deployer/providers/apisix/apisix_test.go index c9b73c27..4fe922ce 100644 --- a/pkg/core/ssl-deployer/providers/apisix/apisix_test.go +++ b/pkg/core/ssl-deployer/providers/apisix/apisix_test.go @@ -37,7 +37,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_APISIX_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CERTIMATE_SSLDEPLOYER_APISIX_SERVERURL="http://127.0.0.1:9080" \ --CERTIMATE_SSLDEPLOYER_APISIX_APIKEY="your-api-key" \ - --CERTIMATE_SSLDEPLOYER_APISIX_CERTIFICATEID="your-cerficiate-id" + --CERTIMATE_SSLDEPLOYER_APISIX_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-deployer/providers/ctcccloud-ao/ctcccloud_ao.go b/pkg/core/ssl-deployer/providers/ctcccloud-ao/ctcccloud_ao.go index 7a78d41e..0e6d9831 100644 --- a/pkg/core/ssl-deployer/providers/ctcccloud-ao/ctcccloud_ao.go +++ b/pkg/core/ssl-deployer/providers/ctcccloud-ao/ctcccloud_ao.go @@ -5,10 +5,12 @@ import ( "errors" "fmt" "log/slog" + "strconv" "github.com/certimate-go/certimate/pkg/core" sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/ctcccloud-ao" ctyunao "github.com/certimate-go/certimate/pkg/sdk3rd/ctyun/ao" + xslices "github.com/certimate-go/certimate/pkg/utils/slices" xtypes "github.com/certimate-go/certimate/pkg/utils/types" ) @@ -80,7 +82,8 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // 域名基础及加速配置查询 // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167 getDomainConfigReq := &ctyunao.GetDomainConfigRequest{ - Domain: xtypes.ToPtr(d.config.Domain), + Domain: xtypes.ToPtr(d.config.Domain), + ProductCode: xtypes.ToPtr("020"), } getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq) d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) @@ -93,7 +96,17 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{ Domain: xtypes.ToPtr(d.config.Domain), ProductCode: xtypes.ToPtr(getDomainConfigResp.ReturnObj.ProductCode), - Origin: getDomainConfigResp.ReturnObj.Origin, + Origin: xslices.Map(getDomainConfigResp.ReturnObj.Origin, func(item *ctyunao.DomainOriginConfigWithWeight) *ctyunao.DomainOriginConfig { + weight := item.Weight + if weight == 0 { + weight = 1 + } + return &ctyunao.DomainOriginConfig{ + Origin: item.Origin, + Role: item.Role, + Weight: strconv.Itoa(int(weight)), + } + }), HttpsStatus: xtypes.ToPtr("on"), CertName: xtypes.ToPtr(upres.CertName), } diff --git a/pkg/core/ssl-deployer/providers/flexcdn/flexcdn_test.go b/pkg/core/ssl-deployer/providers/flexcdn/flexcdn_test.go index 32ee801c..0a53acd0 100644 --- a/pkg/core/ssl-deployer/providers/flexcdn/flexcdn_test.go +++ b/pkg/core/ssl-deployer/providers/flexcdn/flexcdn_test.go @@ -40,7 +40,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_FLEXCDN_SERVERURL="http://127.0.0.1:7788" \ --CERTIMATE_SSLDEPLOYER_FLEXCDN_ACCESSKEYID="your-access-key-id" \ --CERTIMATE_SSLDEPLOYER_FLEXCDN_ACCESSKEY="your-access-key" \ - --CERTIMATE_SSLDEPLOYER_FLEXCDN_CERTIFICATEID="your-cerficiate-id" + --CERTIMATE_SSLDEPLOYER_FLEXCDN_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-deployer/providers/goedge/goedge_test.go b/pkg/core/ssl-deployer/providers/goedge/goedge_test.go index 757527cb..ac5cacb5 100644 --- a/pkg/core/ssl-deployer/providers/goedge/goedge_test.go +++ b/pkg/core/ssl-deployer/providers/goedge/goedge_test.go @@ -40,7 +40,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_GOEDGE_SERVERURL="http://127.0.0.1:7788" \ --CERTIMATE_SSLDEPLOYER_GOEDGE_ACCESSKEYID="your-access-key-id" \ --CERTIMATE_SSLDEPLOYER_GOEDGE_ACCESSKEY="your-access-key" \ - --CERTIMATE_SSLDEPLOYER_GOEDGE_CERTIFICATEID="your-cerficiate-id" + --CERTIMATE_SSLDEPLOYER_GOEDGE_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-deployer/providers/huaweicloud-elb/huaweicloud_elb.go b/pkg/core/ssl-deployer/providers/huaweicloud-elb/huaweicloud_elb.go index a6c29e36..6325fbe8 100644 --- a/pkg/core/ssl-deployer/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/pkg/core/ssl-deployer/providers/huaweicloud-elb/huaweicloud_elb.go @@ -297,21 +297,20 @@ func (d *SSLDeployerProvider) modifyListenerCertificate(ctx context.Context, clo return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err) } - for _, certificate := range *listOldCertificateResp.Certificates { - oldCertificate := certificate - newCertificate := showNewCertificateResp.Certificate + for _, oldCertInfo := range *listOldCertificateResp.Certificates { + newCertInfo := showNewCertificateResp.Certificate - if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil { - if slices.Equal(*oldCertificate.SubjectAlternativeNames, *newCertificate.SubjectAlternativeNames) { + if oldCertInfo.SubjectAlternativeNames != nil && newCertInfo.SubjectAlternativeNames != nil { + if slices.Equal(*oldCertInfo.SubjectAlternativeNames, *newCertInfo.SubjectAlternativeNames) { continue } } else { - if oldCertificate.Domain == newCertificate.Domain { + if oldCertInfo.Domain == newCertInfo.Domain { continue } } - sniCertIds = append(sniCertIds, certificate.Id) + sniCertIds = append(sniCertIds, oldCertInfo.Id) } updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds diff --git a/pkg/core/ssl-deployer/providers/kong/consts.go b/pkg/core/ssl-deployer/providers/kong/consts.go new file mode 100644 index 00000000..91b462bb --- /dev/null +++ b/pkg/core/ssl-deployer/providers/kong/consts.go @@ -0,0 +1,8 @@ +package kong + +type ResourceType string + +const ( + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate") +) diff --git a/pkg/core/ssl-deployer/providers/kong/kong.go b/pkg/core/ssl-deployer/providers/kong/kong.go new file mode 100644 index 00000000..d2855ccf --- /dev/null +++ b/pkg/core/ssl-deployer/providers/kong/kong.go @@ -0,0 +1,143 @@ +package kong + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "log/slog" + "net/http" + + "github.com/kong/go-kong/kong" + + "github.com/certimate-go/certimate/pkg/core" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" + xhttp "github.com/certimate-go/certimate/pkg/utils/http" +) + +type SSLDeployerProviderConfig struct { + // Kong 服务地址。 + ServerUrl string `json:"serverUrl"` + // Kong Admin API Token。 + ApiToken string `json:"apiToken,omitempty"` + // 是否允许不安全的连接。 + AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 工作空间。 + // 选填。 + Workspace string `json:"workspace,omitempty"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId string `json:"certificateId,omitempty"` +} + +type SSLDeployerProvider struct { + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *kong.Client +} + +var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) + +func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the ssl deployer provider is nil") + } + + client, err := createSDKClient(config.ServerUrl, config.Workspace, config.ApiToken, config.AllowInsecureConnections) + if err != nil { + return nil, fmt.Errorf("could not create sdk client: %w", err) + } + + return &SSLDeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } +} + +func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) + } + + return &core.SSLDeployResult{}, nil +} + +func (d *SSLDeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error { + if d.config.CertificateId == "" { + return errors.New("config `certificateId` is required") + } + + // 解析证书内容 + certX509, err := xcert.ParseCertificateFromPEM(certPEM) + if err != nil { + return err + } + + // 更新证书 + // REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate + // REF: https://developer.konghq.com/api/gateway/admin-ee/3.10/#/operations/upsert-certificate-in-workspace + updateCertificateReq := &kong.Certificate{ + ID: kong.String(d.config.CertificateId), + Cert: kong.String(certPEM), + Key: kong.String(privkeyPEM), + SNIs: kong.StringSlice(certX509.DNSNames...), + } + updateCertificateResp, err := d.sdkClient.Certificates.Update(context.TODO(), updateCertificateReq) + d.logger.Debug("sdk request 'kong.UpdateCertificate'", slog.String("sslId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'kong.UpdateCertificate': %w", err) + } + + return nil +} + +func createSDKClient(serverUrl, workspace, apiToken string, skipTlsVerify bool) (*kong.Client, error) { + httpClient := &http.Client{ + Transport: xhttp.NewDefaultTransport(), + Timeout: http.DefaultClient.Timeout, + } + if skipTlsVerify { + transport := xhttp.NewDefaultTransport() + if transport.TLSClientConfig == nil { + transport.TLSClientConfig = &tls.Config{} + } + transport.TLSClientConfig.InsecureSkipVerify = true + httpClient.Transport = transport + } else { + httpClient.Transport = http.DefaultTransport + } + + httpHeaders := http.Header{} + if apiToken != "" { + httpHeaders.Set("Kong-Admin-Token", apiToken) + } + + client, err := kong.NewClient(kong.String(serverUrl), kong.HTTPClientWithHeaders(httpClient, httpHeaders)) + if err != nil { + return nil, err + } + + if workspace != "" { + client.SetWorkspace(workspace) + } + + return client, nil +} diff --git a/pkg/core/ssl-deployer/providers/kong/kong_test.go b/pkg/core/ssl-deployer/providers/kong/kong_test.go new file mode 100644 index 00000000..5a312993 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/kong/kong_test.go @@ -0,0 +1,77 @@ +package kong_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/kong" +) + +var ( + fInputCertPath string + fInputKeyPath string + fServerUrl string + fApiToken string + fCertificateId string +) + +func init() { + argsPrefix := "CERTIMATE_SSLDEPLOYER_KONG_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") + flag.StringVar(&fApiToken, argsPrefix+"APITOKEN", "", "") + flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./kong_test.go -args \ + --CERTIMATE_SSLDEPLOYER_KONG_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLDEPLOYER_KONG_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLDEPLOYER_KONG_SERVERURL="http://127.0.0.1:9080" \ + --CERTIMATE_SSLDEPLOYER_KONG_APITOKEN="your-admin-token" \ + --CERTIMATE_SSLDEPLOYER_KONG_CERTIFICATEID="your-certificate-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("SERVERURL: %v", fServerUrl), + fmt.Sprintf("APITOKEN: %v", fApiToken), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ + ServerUrl: fServerUrl, + ApiToken: fApiToken, + AllowInsecureConnections: true, + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, + CertificateId: fCertificateId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/pkg/core/ssl-deployer/providers/lecdn/lecdn_test.go b/pkg/core/ssl-deployer/providers/lecdn/lecdn_test.go index a94db0bd..ef15c27e 100644 --- a/pkg/core/ssl-deployer/providers/lecdn/lecdn_test.go +++ b/pkg/core/ssl-deployer/providers/lecdn/lecdn_test.go @@ -42,7 +42,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_LECDN_SERVERURL="http://127.0.0.1:5090" \ --CERTIMATE_SSLDEPLOYER_LECDN_USERNAME="your-username" \ --CERTIMATE_SSLDEPLOYER_LECDN_PASSWORD="your-password" \ - --CERTIMATE_SSLDEPLOYER_LECDN_CERTIFICATEID="your-cerficiate-id" + --CERTIMATE_SSLDEPLOYER_LECDN_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go new file mode 100644 index 00000000..7dcec172 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo.go @@ -0,0 +1,88 @@ +package qiniukodo + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "github.com/qiniu/go-sdk/v7/auth" + + "github.com/certimate-go/certimate/pkg/core" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert" + qiniusdk "github.com/certimate-go/certimate/pkg/sdk3rd/qiniu" +) + +type SSLDeployerProviderConfig struct { + // 七牛云 AccessKey。 + AccessKey string `json:"accessKey"` + // 七牛云 SecretKey。 + SecretKey string `json:"secretKey"` + // 自定义域名(不支持泛域名)。 + Domain string `json:"domain"` +} + +type SSLDeployerProvider struct { + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *qiniusdk.KodoManager + sslManager core.SSLManager +} + +var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) + +func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProvider, error) { + if config == nil { + return nil, errors.New("the configuration of the ssl deployer provider is nil") + } + + client := qiniusdk.NewKodoManager(auth.New(config.AccessKey, config.SecretKey)) + + sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{ + AccessKey: config.AccessKey, + SecretKey: config.SecretKey, + }) + if err != nil { + return nil, fmt.Errorf("could not create ssl manager: %w", err) + } + + return &SSLDeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslManager: sslmgr, + }, nil +} + +func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } + + d.sslManager.SetLogger(logger) +} + +func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + if d.config.Domain == "" { + return nil, fmt.Errorf("config `domain` is required") + } + + // 上传证书 + upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 绑定空间域名证书 + bindBucketCertResp, err := d.sdkClient.BindBucketCert(context.TODO(), d.config.Domain, upres.CertId) + d.logger.Debug("sdk request 'kodo.BindCert'", slog.String("request.domain", d.config.Domain), slog.String("request.certId", upres.CertId), slog.Any("response", bindBucketCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'kodo.BindCert': %w", err) + } + + return &core.SSLDeployResult{}, nil +} diff --git a/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go new file mode 100644 index 00000000..3dfcf456 --- /dev/null +++ b/pkg/core/ssl-deployer/providers/qiniu-kodo/qiniu_kodo_test.go @@ -0,0 +1,75 @@ +package qiniukodo_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-deployer/providers/qiniu-kodo" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKey string + fSecretKey string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_SSLDEPLOYER_QINIUKODO_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./qiniu_kodo_test.go -args \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_ACCESSKEY="your-access-key" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_SECRETKEY="your-secret-key" \ + --CERTIMATE_SSLDEPLOYER_QINIUKODO_DOMAIN="example.com" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("SECRETKEY: %v", fSecretKey), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ + AccessKey: fAccessKey, + SecretKey: fSecretKey, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/pkg/core/ssl-deployer/providers/safeline/safeline_test.go b/pkg/core/ssl-deployer/providers/safeline/safeline_test.go index 80b89839..14aaba7f 100644 --- a/pkg/core/ssl-deployer/providers/safeline/safeline_test.go +++ b/pkg/core/ssl-deployer/providers/safeline/safeline_test.go @@ -37,7 +37,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_SAFELINE_INPUTKEYPATH="/path/to/your-input-key.pem" \ --CERTIMATE_SSLDEPLOYER_SAFELINE_SERVERURL="http://127.0.0.1:9443" \ --CERTIMATE_SSLDEPLOYER_SAFELINE_APITOKEN="your-api-token" \ - --CERTIMATE_SSLDEPLOYER_SAFELINE_CERTIFICATEID="your-cerficiate-id" + --CERTIMATE_SSLDEPLOYER_SAFELINE_CERTIFICATEID="your-certificate-id" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-deployer/providers/ssh/ssh.go b/pkg/core/ssl-deployer/providers/ssh/ssh.go index 94d0b6bf..7cbe7c8f 100644 --- a/pkg/core/ssl-deployer/providers/ssh/ssh.go +++ b/pkg/core/ssl-deployer/providers/ssh/ssh.go @@ -419,7 +419,7 @@ func writeFileWithSFTP(sshCli *ssh.Client, path string, data []byte) error { } defer sftpCli.Close() - if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { + if err := sftpCli.MkdirAll(filepath.ToSlash(filepath.Dir(path))); err != nil { return fmt.Errorf("failed to create remote directory: %w", err) } diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go index 5540c8c8..e2fcaf7d 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo.go @@ -25,8 +25,8 @@ type SSLDeployerProviderConfig struct { Endpoint string `json:"endpoint,omitempty"` // 站点 ID。 ZoneId string `json:"zoneId"` - // 加速域名(支持泛域名)。 - Domain string `json:"domain"` + // 加速域名列表(支持泛域名)。 + Domains []string `json:"domains"` } type SSLDeployerProvider struct { @@ -82,8 +82,8 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke if d.config.ZoneId == "" { return nil, errors.New("config `zoneId` is required") } - if d.config.Domain == "" { - return nil, errors.New("config `domain` is required") + if len(d.config.Domains) == 0 { + return nil, errors.New("config `domains` is required") } // 上传证书 @@ -99,7 +99,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke modifyHostsCertificateReq := tcteo.NewModifyHostsCertificateRequest() modifyHostsCertificateReq.ZoneId = common.StringPtr(d.config.ZoneId) modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") - modifyHostsCertificateReq.Hosts = common.StringPtrs([]string{d.config.Domain}) + modifyHostsCertificateReq.Hosts = common.StringPtrs(d.config.Domains) modifyHostsCertificateReq.ServerCertInfo = []*tcteo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} modifyHostsCertificateResp, err := d.sdkClient.ModifyHostsCertificate(modifyHostsCertificateReq) d.logger.Debug("sdk request 'teo.ModifyHostsCertificate'", slog.Any("request", modifyHostsCertificateReq), slog.Any("response", modifyHostsCertificateResp)) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go index cbca3fe3..51a440c1 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-eo/tencentcloud_eo_test.go @@ -17,7 +17,7 @@ var ( fSecretId string fSecretKey string fZoneId string - fDomain string + fDomains string ) func init() { @@ -28,7 +28,7 @@ func init() { flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "") flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") flag.StringVar(&fZoneId, argsPrefix+"ZONEID", "", "") - flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") + flag.StringVar(&fDomains, argsPrefix+"DOMAINS", "", "") } /* @@ -40,7 +40,7 @@ Shell command to run this test: --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETID="your-secret-id" \ --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_SECRETKEY="your-secret-key" \ --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_ZONEID="your-zone-id" \ - --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAIN="example.com" + --CERTIMATE_SSLDEPLOYER_TENCENTCLOUDEO_DOMAINS="example.com" */ func TestDeploy(t *testing.T) { flag.Parse() @@ -53,14 +53,14 @@ func TestDeploy(t *testing.T) { fmt.Sprintf("SECRETID: %v", fSecretId), fmt.Sprintf("SECRETKEY: %v", fSecretKey), fmt.Sprintf("ZONEID: %v", fZoneId), - fmt.Sprintf("DOMAIN: %v", fDomain), + fmt.Sprintf("DOMAINS: %v", fDomains), }, "\n")) deployer, err := provider.NewSSLDeployerProvider(&provider.SSLDeployerProviderConfig{ SecretId: fSecretId, SecretKey: fSecretKey, ZoneId: fZoneId, - Domain: fDomain, + Domains: strings.Split(fDomains, ";"), }) if err != nil { t.Errorf("err: %+v", err) diff --git a/pkg/core/ssl-deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go b/pkg/core/ssl-deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go index 2fff2992..753e8be4 100644 --- a/pkg/core/ssl-deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go +++ b/pkg/core/ssl-deployer/providers/tencentcloud-ssl-update/tencentcloud_ssl_update.go @@ -24,7 +24,7 @@ type SSLDeployerProviderConfig struct { // 腾讯云接口端点。 Endpoint string `json:"endpoint,omitempty"` // 原证书 ID。 - CertificiateId string `json:"certificateId"` + CertificateId string `json:"certificateId"` // 是否替换原有证书(即保持原证书 ID 不变)。 IsReplaced bool `json:"isReplaced,omitempty"` // 云资源类型数组。 @@ -80,7 +80,7 @@ func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { } func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { - if d.config.CertificiateId == "" { + if d.config.CertificateId == "" { return nil, errors.New("config `certificateId` is required") } if len(d.config.ResourceTypes) == 0 { @@ -120,7 +120,7 @@ func (d *SSLDeployerProvider) executeUpdateCertificateInstance(ctx context.Conte } updateCertificateInstanceReq := tcssl.NewUpdateCertificateInstanceRequest() - updateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificiateId) + updateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId) updateCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) updateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceTypes) updateCertificateInstanceReq.ResourceTypesRegions = wrapResourceTypeRegions(d.config.ResourceTypes, d.config.ResourceRegions) @@ -198,7 +198,7 @@ func (d *SSLDeployerProvider) executeUploadUpdateCertificateInstance(ctx context } uploadUpdateCertificateInstanceReq := tcssl.NewUploadUpdateCertificateInstanceRequest() - uploadUpdateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificiateId) + uploadUpdateCertificateInstanceReq.OldCertificateId = common.StringPtr(d.config.CertificateId) uploadUpdateCertificateInstanceReq.CertificatePublicKey = common.StringPtr(certPEM) uploadUpdateCertificateInstanceReq.CertificatePrivateKey = common.StringPtr(privkeyPEM) uploadUpdateCertificateInstanceReq.ResourceTypes = common.StringPtrs(d.config.ResourceTypes) diff --git a/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas.go b/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas.go index bcbecd3b..8e875a54 100644 --- a/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas.go +++ b/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas.go @@ -93,13 +93,23 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey } if listUserCertificateOrderResp.Body.CertificateOrderList != nil { - for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList { - if !strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) { + for _, certOrder := range listUserCertificateOrderResp.Body.CertificateOrderList { + // 先对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, tea.StringValue(certOrder.CommonName)) { continue } + // 再对比证书序列号 + // 注意阿里云 CAS 会在序列号前补零,需去除后再比较 + oldCertSN := strings.TrimLeft(tea.StringValue(certOrder.SerialNo), "0") + newCertSN := strings.TrimLeft(certX509.SerialNumber.Text(16), "0") + if !strings.EqualFold(newCertSN, oldCertSN) { + continue + } + + // 最后对比证书内容 getUserCertificateDetailReq := &alicas.GetUserCertificateDetailRequest{ - CertId: certDetail.CertificateId, + CertId: certOrder.CertificateId, } getUserCertificateDetailResp, err := m.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq) m.logger.Debug("sdk request 'cas.GetUserCertificateDetail'", slog.Any("request", getUserCertificateDetailReq), slog.Any("response", getUserCertificateDetailResp)) @@ -123,8 +133,8 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)), - CertName: *certDetail.Name, + CertId: fmt.Sprintf("%d", tea.Int64Value(certOrder.CertificateId)), + CertName: *certOrder.Name, ExtendedData: map[string]any{ "instanceId": tea.StringValue(getUserCertificateDetailResp.Body.InstanceId), "certIdentifier": tea.StringValue(getUserCertificateDetailResp.Body.CertIdentifier), diff --git a/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas_test.go b/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas_test.go new file mode 100644 index 00000000..31955399 --- /dev/null +++ b/pkg/core/ssl-manager/providers/aliyun-cas/aliyun_cas_test.go @@ -0,0 +1,77 @@ +package aliyuncas_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string + fRegion string +) + +func init() { + argsPrefix := "CERTIMATE_SSLMANAGER_ALIYUNCAS_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") + flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") +} + +/* +Shell command to run this test: + + go test -v ./aliyun_cas_test.go -args \ + --CERTIMATE_SSLMANAGER_ALIYUNCAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLMANAGER_ALIYUNCAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLMANAGER_ALIYUNCAS_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_SSLMANAGER_ALIYUNCAS_ACCESSKEYSECRET="your-access-key-secret" \ + --CERTIMATE_SSLMANAGER_ALIYUNCAS_REGION="cn-hangzhou" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYSECRET: %v", fAccessKeySecret), + fmt.Sprintf("REGION: %v", fRegion), + }, "\n")) + + sslmanager, err := provider.NewSSLManagerProvider(&provider.SSLManagerProviderConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + Region: fRegion, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := sslmanager.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/pkg/core/ssl-manager/providers/aliyun-slb/aliyun_slb.go b/pkg/core/ssl-manager/providers/aliyun-slb/aliyun_slb.go index eced6360..3bf2aa90 100644 --- a/pkg/core/ssl-manager/providers/aliyun-slb/aliyun_slb.go +++ b/pkg/core/ssl-manager/providers/aliyun-slb/aliyun_slb.go @@ -86,16 +86,16 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil { fingerprint := sha256.Sum256(certX509.Raw) fingerprintHex := hex.EncodeToString(fingerprint[:]) - for _, certDetail := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate { - isSameCert := *certDetail.IsAliCloudCertificate == 0 && - strings.EqualFold(fingerprintHex, strings.ReplaceAll(*certDetail.Fingerprint, ":", "")) && - strings.EqualFold(certX509.Subject.CommonName, *certDetail.CommonName) + for _, serverCert := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate { + isSameCert := *serverCert.IsAliCloudCertificate == 0 && + strings.EqualFold(fingerprintHex, strings.ReplaceAll(*serverCert.Fingerprint, ":", "")) && + strings.EqualFold(certX509.Subject.CommonName, *serverCert.CommonName) // 如果已存在相同证书,直接返回 if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: *certDetail.ServerCertificateId, - CertName: *certDetail.ServerCertificateName, + CertId: *serverCert.ServerCertificateId, + CertName: *serverCert.ServerCertificateName, }, nil } } diff --git a/pkg/core/ssl-manager/providers/azure-keyvault/azure_keyvault.go b/pkg/core/ssl-manager/providers/azure-keyvault/azure_keyvault.go index 79209710..b03ecafb 100644 --- a/pkg/core/ssl-manager/providers/azure-keyvault/azure_keyvault.go +++ b/pkg/core/ssl-manager/providers/azure-keyvault/azure_keyvault.go @@ -141,7 +141,7 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey // 生成新证书名(需符合 Azure 命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) - // Azure Key Vault 不支持导入带有 Certificiate Chain 的 PEM 证书。 + // Azure Key Vault 不支持导入带有 Certificate Chain 的 PEM 证书。 // Issue Link: https://github.com/Azure/azure-cli/issues/19017 // 暂时的解决方法是,将 PEM 证书转换成 PFX 格式,然后再导入。 certPFX, err := xcert.TransformCertificateFromPEMToPFX(certPEM, privkeyPEM, "") diff --git a/pkg/core/ssl-manager/providers/baiducloud-cert/baiducloud_cert_test.go b/pkg/core/ssl-manager/providers/baiducloud-cert/baiducloud_cert_test.go index 80c7d790..9360621c 100644 --- a/pkg/core/ssl-manager/providers/baiducloud-cert/baiducloud_cert_test.go +++ b/pkg/core/ssl-manager/providers/baiducloud-cert/baiducloud_cert_test.go @@ -20,7 +20,7 @@ var ( ) func init() { - argsPrefix := "CERTIMATE_SSLMANAGER_BAIDUCLOUDCAS_" + argsPrefix := "CERTIMATE_SSLMANAGER_BAIDUCLOUDCERT_" flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") @@ -31,11 +31,11 @@ func init() { /* Shell command to run this test: - go test -v ./baiducloud_cas_test.go -args \ - --CERTIMATE_SSLMANAGER_BAIDUCLOUDCAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ - --CERTIMATE_SSLMANAGER_BAIDUCLOUDCAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ - --CERTIMATE_SSLMANAGER_BAIDUCLOUDCAS_ACCESSKEYID="your-access-key-id" \ - --CERTIMATE_SSLMANAGER_BAIDUCLOUDCAS_SECRETACCESSKEY="your-access-key-secret" + go test -v ./baiducloud_cert_test.go -args \ + --CERTIMATE_SSLMANAGER_BAIDUCLOUDCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLMANAGER_BAIDUCLOUDCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLMANAGER_BAIDUCLOUDCERT_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_SSLMANAGER_BAIDUCLOUDCERT_SECRETACCESSKEY="your-access-key-secret" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/pkg/core/ssl-manager/providers/byteplus-cdn/byteplus_cdn.go b/pkg/core/ssl-manager/providers/byteplus-cdn/byteplus_cdn.go index cc42c749..32a4b6ed 100644 --- a/pkg/core/ssl-manager/providers/byteplus-cdn/byteplus_cdn.go +++ b/pkg/core/ssl-manager/providers/byteplus-cdn/byteplus_cdn.go @@ -87,17 +87,17 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey } if listCertInfoResp.Result.CertInfo != nil { - for _, certDetail := range listCertInfoResp.Result.CertInfo { + for _, certInfo := range listCertInfoResp.Result.CertInfo { fingerprintSha1 := sha1.Sum(certX509.Raw) fingerprintSha256 := sha256.Sum256(certX509.Raw) - isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) && - strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256) + isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certInfo.CertFingerprint.Sha1) && + strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certInfo.CertFingerprint.Sha256) // 如果已存在相同证书,直接返回 if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: certDetail.CertId, - CertName: certDetail.Desc, + CertId: certInfo.CertId, + CertName: certInfo.Desc, }, nil } } diff --git a/pkg/core/ssl-manager/providers/huaweicloud-elb/huaweicloud_elb.go b/pkg/core/ssl-manager/providers/huaweicloud-elb/huaweicloud_elb.go index 131572a3..15ad5e6f 100644 --- a/pkg/core/ssl-manager/providers/huaweicloud-elb/huaweicloud_elb.go +++ b/pkg/core/ssl-manager/providers/huaweicloud-elb/huaweicloud_elb.go @@ -95,12 +95,12 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey } if listCertificatesResp.Certificates != nil { - for _, certDetail := range *listCertificatesResp.Certificates { + for _, certInfo := range *listCertificatesResp.Certificates { var isSameCert bool - if certDetail.Certificate == certPEM { + if certInfo.Certificate == certPEM { isSameCert = true } else { - oldCertX509, err := xcert.ParseCertificateFromPEM(certDetail.Certificate) + oldCertX509, err := xcert.ParseCertificateFromPEM(certInfo.Certificate) if err != nil { continue } @@ -112,8 +112,8 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: certDetail.Id, - CertName: certDetail.Name, + CertId: certInfo.Id, + CertName: certInfo.Name, }, nil } } diff --git a/pkg/core/ssl-manager/providers/huaweicloud-scm/huaweicloud_scm.go b/pkg/core/ssl-manager/providers/huaweicloud-scm/huaweicloud_scm.go index 7084aaf2..e60010b8 100644 --- a/pkg/core/ssl-manager/providers/huaweicloud-scm/huaweicloud_scm.go +++ b/pkg/core/ssl-manager/providers/huaweicloud-scm/huaweicloud_scm.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "log/slog" + "strings" "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" @@ -95,6 +96,17 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey if listCertificatesResp.Certificates != nil { for _, certDetail := range *listCertificatesResp.Certificates { + // 先对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, certDetail.Domain) { + continue + } + + // 再对比证书有效期 + if certX509.NotAfter.Local().Format(time.DateTime) != strings.TrimSuffix(certDetail.ExpireTime, ".0") { + continue + } + + // 最后对比证书内容 exportCertificateReq := &hcscmmodel.ExportCertificateRequest{ CertificateId: certDetail.Id, } @@ -105,27 +117,27 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey continue } return nil, fmt.Errorf("failed to execute sdk request 'scm.ExportCertificate': %w", err) - } - - var isSameCert bool - if *exportCertificateResp.Certificate == certPEM { - isSameCert = true } else { - oldCertX509, err := xcert.ParseCertificateFromPEM(*exportCertificateResp.Certificate) - if err != nil { - continue + var isSameCert bool + if *exportCertificateResp.Certificate == certPEM { + isSameCert = true + } else { + oldCertX509, err := xcert.ParseCertificateFromPEM(*exportCertificateResp.Certificate) + if err != nil { + continue + } + + isSameCert = xcert.EqualCertificate(certX509, oldCertX509) } - isSameCert = xcert.EqualCertificate(certX509, oldCertX509) - } - - // 如果已存在相同证书,直接返回 - if isSameCert { - m.logger.Info("ssl certificate already exists") - return &core.SSLManageUploadResult{ - CertId: certDetail.Id, - CertName: certDetail.Name, - }, nil + // 如果已存在相同证书,直接返回 + if isSameCert { + m.logger.Info("ssl certificate already exists") + return &core.SSLManageUploadResult{ + CertId: certDetail.Id, + CertName: certDetail.Name, + }, nil + } } } } diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go index 07775b21..dcbe26b0 100644 --- a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go +++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert.go @@ -2,9 +2,12 @@ package qiniusslcert import ( "context" + "crypto/x509" "errors" "fmt" "log/slog" + "slices" + "strings" "time" "github.com/qiniu/go-sdk/v7/auth" @@ -24,7 +27,7 @@ type SSLManagerProviderConfig struct { type SSLManagerProvider struct { config *SSLManagerProviderConfig logger *slog.Logger - sdkClient *qiniusdk.CdnManager + sdkClient *qiniusdk.SslCertManager } var _ core.SSLManager = (*SSLManagerProvider)(nil) @@ -64,12 +67,80 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey // 生成新证书名(需符合七牛云命名规则) certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + // 遍历查询已有证书,避免重复上传 + getSslCertListMarker := "" + getSslCertListLimit := int32(200) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + getSslCertListResp, err := m.sdkClient.GetSslCertList(context.TODO(), getSslCertListMarker, getSslCertListLimit) + m.logger.Debug("sdk request 'sslcert.GetList'", slog.Any("request.marker", getSslCertListMarker), slog.Any("response", getSslCertListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'sslcert.GetList': %w", err) + } + + if getSslCertListResp.Certs != nil { + for _, sslCert := range getSslCertListResp.Certs { + // 先对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, sslCert.CommonName) { + continue + } + + // 再对比证书多域名 + if !slices.Equal(certX509.DNSNames, sslCert.DnsNames) { + continue + } + + // 再对比证书有效期 + if certX509.NotBefore.Unix() != sslCert.NotBefore || certX509.NotAfter.Unix() != sslCert.NotAfter { + continue + } + + // 最后对比证书公钥算法 + switch certX509.PublicKeyAlgorithm { + case x509.RSA: + if !strings.EqualFold(sslCert.Encrypt, "RSA") { + continue + } + case x509.ECDSA: + if !strings.EqualFold(sslCert.Encrypt, "ECDSA") { + continue + } + case x509.Ed25519: + if !strings.EqualFold(sslCert.Encrypt, "ED25519") { + continue + } + default: + // 未知算法,跳过 + continue + } + + // 如果以上信息都一致,则视为已存在相同证书,直接返回 + m.logger.Info("ssl certificate already exists") + return &core.SSLManageUploadResult{ + CertId: sslCert.CertID, + CertName: sslCert.Name, + }, nil + } + } + + if len(getSslCertListResp.Certs) < int(getSslCertListLimit) || getSslCertListResp.Marker == "" { + break + } else { + getSslCertListMarker = getSslCertListResp.Marker + } + } + // 上传新证书 // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate uploadSslCertResp, err := m.sdkClient.UploadSslCert(context.TODO(), certName, certX509.Subject.CommonName, certPEM, privkeyPEM) - m.logger.Debug("sdk request 'cdn.UploadSslCert'", slog.Any("response", uploadSslCertResp)) + m.logger.Debug("sdk request 'sslcert.Upload'", slog.Any("response", uploadSslCertResp)) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cdn.UploadSslCert': %w", err) + return nil, fmt.Errorf("failed to execute sdk request 'sslcert.Upload': %w", err) } return &core.SSLManageUploadResult{ @@ -78,7 +149,7 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey }, nil } -func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error) { +func createSDKClient(accessKey, secretKey string) (*qiniusdk.SslCertManager, error) { if secretKey == "" { return nil, errors.New("invalid qiniu access key") } @@ -88,6 +159,6 @@ func createSDKClient(accessKey, secretKey string) (*qiniusdk.CdnManager, error) } credential := auth.New(accessKey, secretKey) - client := qiniusdk.NewCdnManager(credential) + client := qiniusdk.NewSslCertManager(credential) return client, nil } diff --git a/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go new file mode 100644 index 00000000..87e14a08 --- /dev/null +++ b/pkg/core/ssl-manager/providers/qiniu-sslcert/qiniu_sslcert_test.go @@ -0,0 +1,72 @@ +package qiniusslcert_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/qiniu-sslcert" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKey string + fSecretKey string +) + +func init() { + argsPrefix := "CERTIMATE_SSLMANAGER_QINIUSSLCERT_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKey, argsPrefix+"ACCESSKEY", "", "") + flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./qiniu_sslcert_test.go -args \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_ACCESSKEY="your-access-key" \ + --CERTIMATE_SSLMANAGER_QINIUSSLCERT_SECRETKEY="your-secret-key" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEY: %v", fAccessKey), + fmt.Sprintf("SECRETKEY: %v", fSecretKey), + }, "\n")) + + sslmanager, err := provider.NewSSLManagerProvider(&provider.SSLManagerProviderConfig{ + AccessKey: fAccessKey, + SecretKey: fSecretKey, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := sslmanager.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/pkg/core/ssl-manager/providers/rainyun-sslcenter/rainyun_sslcenter.go b/pkg/core/ssl-manager/providers/rainyun-sslcenter/rainyun_sslcenter.go index 1fc930d6..50aa64da 100644 --- a/pkg/core/ssl-manager/providers/rainyun-sslcenter/rainyun_sslcenter.go +++ b/pkg/core/ssl-manager/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -114,19 +114,19 @@ func (m *SSLManagerProvider) findCertIfExists(ctx context.Context, certPEM strin } if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil { - for _, sslItem := range sslCenterListResp.Data.Records { + for _, sslRecord := range sslCenterListResp.Data.Records { // 先对比证书的多域名 - if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") { + if sslRecord.Domain != strings.Join(certX509.DNSNames, ", ") { continue } // 再对比证书的有效期 - if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() { + if sslRecord.StartDate != certX509.NotBefore.Unix() || sslRecord.ExpireDate != certX509.NotAfter.Unix() { continue } // 最后对比证书内容 - sslCenterGetResp, err := m.sdkClient.SslCenterGet(sslItem.ID) + sslCenterGetResp, err := m.sdkClient.SslCenterGet(sslRecord.ID) if err != nil { return nil, fmt.Errorf("failed to execute sdk request 'sslcenter.Get': %w", err) } @@ -148,7 +148,7 @@ func (m *SSLManagerProvider) findCertIfExists(ctx context.Context, certPEM strin // 如果已存在相同证书,直接返回 if isSameCert { return &core.SSLManageUploadResult{ - CertId: fmt.Sprintf("%d", sslItem.ID), + CertId: fmt.Sprintf("%d", sslRecord.ID), }, nil } } diff --git a/pkg/core/ssl-manager/providers/ucloud-ussl/ucloud_ussl.go b/pkg/core/ssl-manager/providers/ucloud-ussl/ucloud_ussl.go index 66824412..b6a3f851 100644 --- a/pkg/core/ssl-manager/providers/ucloud-ussl/ucloud_ussl.go +++ b/pkg/core/ssl-manager/providers/ucloud-ussl/ucloud_ussl.go @@ -143,24 +143,24 @@ func (m *SSLManagerProvider) findCertIfExists(ctx context.Context, certPEM strin } if getCertificateListResp.CertificateList != nil { - for _, certInfo := range getCertificateListResp.CertificateList { + for _, certItem := range getCertificateListResp.CertificateList { // 优刻得未提供可唯一标识证书的字段,只能通过多个字段尝试对比来判断是否为同一证书 // 先分别对比证书的多域名、品牌、有效期,再对比签名算法 - if len(certX509.DNSNames) == 0 || certInfo.Domains != strings.Join(certX509.DNSNames, ",") { + if len(certX509.DNSNames) == 0 || certItem.Domains != strings.Join(certX509.DNSNames, ",") { continue } - if len(certX509.Issuer.Organization) == 0 || certInfo.Brand != certX509.Issuer.Organization[0] { + if len(certX509.Issuer.Organization) == 0 || certItem.Brand != certX509.Issuer.Organization[0] { continue } - if int64(certInfo.NotBefore) != certX509.NotBefore.UnixMilli() || int64(certInfo.NotAfter) != certX509.NotAfter.UnixMilli() { + if int64(certItem.NotBefore) != certX509.NotBefore.UnixMilli() || int64(certItem.NotAfter) != certX509.NotAfter.UnixMilli() { continue } getCertificateDetailInfoReq := m.sdkClient.NewGetCertificateDetailInfoRequest() - getCertificateDetailInfoReq.CertificateID = ucloud.Int(certInfo.CertificateID) + getCertificateDetailInfoReq.CertificateID = ucloud.Int(certItem.CertificateID) if m.config.ProjectId != "" { getCertificateDetailInfoReq.ProjectId = ucloud.String(m.config.ProjectId) } @@ -212,10 +212,10 @@ func (m *SSLManagerProvider) findCertIfExists(ctx context.Context, certPEM strin } return &core.SSLManageUploadResult{ - CertId: fmt.Sprintf("%d", certInfo.CertificateID), - CertName: certInfo.Name, + CertId: fmt.Sprintf("%d", certItem.CertificateID), + CertName: certItem.Name, ExtendedData: map[string]any{ - "resourceId": certInfo.CertificateSN, + "resourceId": certItem.CertificateSN, }, }, nil } diff --git a/pkg/core/ssl-manager/providers/volcengine-cdn/volcengine_cdn.go b/pkg/core/ssl-manager/providers/volcengine-cdn/volcengine_cdn.go index 9ad13187..fd12f830 100644 --- a/pkg/core/ssl-manager/providers/volcengine-cdn/volcengine_cdn.go +++ b/pkg/core/ssl-manager/providers/volcengine-cdn/volcengine_cdn.go @@ -88,17 +88,17 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey } if listCertInfoResp.Result.CertInfo != nil { - for _, certDetail := range listCertInfoResp.Result.CertInfo { + for _, certInfo := range listCertInfoResp.Result.CertInfo { fingerprintSha1 := sha1.Sum(certX509.Raw) fingerprintSha256 := sha256.Sum256(certX509.Raw) - isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certDetail.CertFingerprint.Sha1) && - strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certDetail.CertFingerprint.Sha256) + isSameCert := strings.EqualFold(hex.EncodeToString(fingerprintSha1[:]), certInfo.CertFingerprint.Sha1) && + strings.EqualFold(hex.EncodeToString(fingerprintSha256[:]), certInfo.CertFingerprint.Sha256) // 如果已存在相同证书,直接返回 if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: certDetail.CertId, - CertName: certDetail.Desc, + CertId: certInfo.CertId, + CertName: certInfo.Desc, }, nil } } diff --git a/pkg/core/ssl-manager/providers/volcengine-live/volcengine_live.go b/pkg/core/ssl-manager/providers/volcengine-live/volcengine_live.go index 147a8ec9..2f06683a 100644 --- a/pkg/core/ssl-manager/providers/volcengine-live/volcengine_live.go +++ b/pkg/core/ssl-manager/providers/volcengine-live/volcengine_live.go @@ -70,11 +70,11 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey return nil, fmt.Errorf("failed to execute sdk request 'live.ListCertV2': %w", err) } if listCertResp.Result.CertList != nil { - for _, certDetail := range listCertResp.Result.CertList { + for _, certInfo := range listCertResp.Result.CertList { // 查询证书详细信息 // REF: https://www.volcengine.com/docs/6469/1186278#%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E8%AF%A6%E6%83%85 describeCertDetailSecretReq := &velive.DescribeCertDetailSecretV2Body{ - ChainID: ve.String(certDetail.ChainID), + ChainID: ve.String(certInfo.ChainID), } describeCertDetailSecretResp, err := m.sdkClient.DescribeCertDetailSecretV2(ctx, describeCertDetailSecretReq) m.logger.Debug("sdk request 'live.DescribeCertDetailSecretV2'", slog.Any("request", describeCertDetailSecretReq), slog.Any("response", describeCertDetailSecretResp)) @@ -99,8 +99,8 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey if isSameCert { m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: certDetail.ChainID, - CertName: certDetail.CertName, + CertId: certInfo.ChainID, + CertName: certInfo.CertName, }, nil } } diff --git a/pkg/core/ssl-manager/providers/wangsu-certificate/wangsu_certificate.go b/pkg/core/ssl-manager/providers/wangsu-certificate/wangsu_certificate.go index d2523c9b..c94d8ca6 100644 --- a/pkg/core/ssl-manager/providers/wangsu-certificate/wangsu_certificate.go +++ b/pkg/core/ssl-manager/providers/wangsu-certificate/wangsu_certificate.go @@ -71,16 +71,16 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey } if listCertificatesResp.Certificates != nil { - for _, certificate := range listCertificatesResp.Certificates { + for _, certRecord := range listCertificatesResp.Certificates { // 对比证书序列号 - if !strings.EqualFold(certX509.SerialNumber.Text(16), certificate.Serial) { + if !strings.EqualFold(certX509.SerialNumber.Text(16), certRecord.Serial) { continue } // 再对比证书有效期 cstzone := time.FixedZone("CST", 8*60*60) - oldCertNotBefore, _ := time.ParseInLocation(time.DateTime, certificate.ValidityFrom, cstzone) - oldCertNotAfter, _ := time.ParseInLocation(time.DateTime, certificate.ValidityTo, cstzone) + oldCertNotBefore, _ := time.ParseInLocation(time.DateTime, certRecord.ValidityFrom, cstzone) + oldCertNotAfter, _ := time.ParseInLocation(time.DateTime, certRecord.ValidityTo, cstzone) if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { continue } @@ -88,8 +88,8 @@ func (m *SSLManagerProvider) Upload(ctx context.Context, certPEM string, privkey // 如果以上信息都一致,则视为已存在相同证书,直接返回 m.logger.Info("ssl certificate already exists") return &core.SSLManageUploadResult{ - CertId: certificate.CertificateId, - CertName: certificate.Name, + CertId: certRecord.CertificateId, + CertName: certRecord.Name, }, nil } } diff --git a/pkg/sdk3rd/baishan/client.go b/pkg/sdk3rd/baishan/client.go index fb9a1df7..2978cd33 100644 --- a/pkg/sdk3rd/baishan/client.go +++ b/pkg/sdk3rd/baishan/client.go @@ -22,7 +22,7 @@ func NewClient(apiToken string) (*Client, error) { SetHeader("Accept", "application/json"). SetHeader("Content-Type", "application/json"). SetHeader("User-Agent", "certimate"). - SetHeader("Token", apiToken) + SetQueryParam("token", apiToken) return &Client{client}, nil } diff --git a/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go b/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go index 01c007ab..9d760a45 100644 --- a/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go +++ b/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go @@ -14,15 +14,15 @@ type GetDomainConfigResponse struct { apiResponseBase ReturnObj *struct { - Domain string `json:"domain"` - ProductCode string `json:"product_code"` - Status int32 `json:"status"` - AreaScope int32 `json:"area_scope"` - Cname string `json:"cname"` - Origin []*DomainOriginConfig `json:"origin,omitempty"` - HttpsStatus string `json:"https_status"` - HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` - CertName string `json:"cert_name"` + Domain string `json:"domain"` + ProductCode string `json:"product_code"` + Status int32 `json:"status"` + AreaScope int32 `json:"area_scope"` + Cname string `json:"cname"` + Origin []*DomainOriginConfigWithWeight `json:"origin,omitempty"` + HttpsStatus string `json:"https_status"` + HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` + CertName string `json:"cert_name"` } `json:"returnObj,omitempty"` } diff --git a/pkg/sdk3rd/ctyun/ao/types.go b/pkg/sdk3rd/ctyun/ao/types.go index c706afd9..83f703d4 100644 --- a/pkg/sdk3rd/ctyun/ao/types.go +++ b/pkg/sdk3rd/ctyun/ao/types.go @@ -90,6 +90,12 @@ type CertDetail struct { } type DomainOriginConfig struct { + Origin string `json:"origin"` + Role string `json:"role"` + Weight string `json:"weight"` +} + +type DomainOriginConfigWithWeight struct { Origin string `json:"origin"` Role string `json:"role"` Weight int32 `json:"weight"` diff --git a/pkg/sdk3rd/qiniu/cdn.go b/pkg/sdk3rd/qiniu/cdn.go index 54a56517..74745d0b 100644 --- a/pkg/sdk3rd/qiniu/cdn.go +++ b/pkg/sdk3rd/qiniu/cdn.go @@ -2,16 +2,12 @@ package qiniu import ( "context" - "fmt" "net/http" - "strings" "github.com/qiniu/go-sdk/v7/auth" "github.com/qiniu/go-sdk/v7/client" ) -const qiniuHost = "https://api.qiniu.com" - type CdnManager struct { client *client.Client } @@ -21,16 +17,10 @@ func NewCdnManager(mac *auth.Credentials) *CdnManager { mac = auth.Default() } - client := &client.Client{&http.Client{Transport: newTransport(mac, nil)}} + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} return &CdnManager{client: client} } -func (m *CdnManager) urlf(pathf string, pathargs ...any) string { - path := fmt.Sprintf(pathf, pathargs...) - path = strings.TrimPrefix(path, "/") - return qiniuHost + "/" + path -} - type GetDomainInfoResponse struct { Code *int `json:"code,omitempty"` Error *string `json:"error,omitempty"` @@ -52,7 +42,7 @@ type GetDomainInfoResponse struct { func (m *CdnManager) GetDomainInfo(ctx context.Context, domain string) (*GetDomainInfoResponse, error) { resp := new(GetDomainInfoResponse) - if err := m.client.Call(ctx, resp, http.MethodGet, m.urlf("domain/%s", domain), nil); err != nil { + if err := m.client.Call(ctx, resp, http.MethodGet, urlf("domain/%s", domain), nil); err != nil { return nil, err } return resp, nil @@ -76,7 +66,7 @@ func (m *CdnManager) ModifyDomainHttpsConf(ctx context.Context, domain string, c Http2Enable: http2Enable, } resp := new(ModifyDomainHttpsConfResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/httpsconf", domain), nil, req); err != nil { + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/httpsconf", domain), nil, req); err != nil { return nil, err } return resp, nil @@ -100,34 +90,7 @@ func (m *CdnManager) EnableDomainHttps(ctx context.Context, domain string, certI Http2Enable: http2Enable, } resp := new(EnableDomainHttpsResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPut, m.urlf("domain/%s/sslize", domain), nil, req); err != nil { - return nil, err - } - return resp, nil -} - -type UploadSslCertRequest struct { - Name string `json:"name"` - CommonName string `json:"common_name"` - Certificate string `json:"ca"` - PrivateKey string `json:"pri"` -} - -type UploadSslCertResponse struct { - Code *int `json:"code,omitempty"` - Error *string `json:"error,omitempty"` - CertID string `json:"certID"` -} - -func (m *CdnManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { - req := &UploadSslCertRequest{ - Name: name, - CommonName: commonName, - Certificate: certificate, - PrivateKey: privateKey, - } - resp := new(UploadSslCertResponse) - if err := m.client.CallWithJson(ctx, resp, http.MethodPost, m.urlf("sslcert"), nil, req); err != nil { + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("domain/%s/sslize", domain), nil, req); err != nil { return nil, err } return resp, nil diff --git a/pkg/sdk3rd/qiniu/kodo.go b/pkg/sdk3rd/qiniu/kodo.go new file mode 100644 index 00000000..6a3245a2 --- /dev/null +++ b/pkg/sdk3rd/qiniu/kodo.go @@ -0,0 +1,44 @@ +package qiniu + +import ( + "context" + "net/http" + + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/client" +) + +type KodoManager struct { + client *client.Client +} + +func NewKodoManager(mac *auth.Credentials) *KodoManager { + if mac == nil { + mac = auth.Default() + } + + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} + return &KodoManager{client: client} +} + +type BindBucketCertRequest struct { + CertID string `json:"certid"` + Domain string `json:"domain"` +} + +type BindBucketCertResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` +} + +func (m *KodoManager) BindBucketCert(ctx context.Context, domain string, certId string) (*BindBucketCertResponse, error) { + req := &BindBucketCertRequest{ + CertID: certId, + Domain: domain, + } + resp := new(BindBucketCertResponse) + if err := m.client.CallWithJson(ctx, resp, http.MethodPut, urlf("cert/bind"), nil, req); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/sdk3rd/qiniu/sslcert.go b/pkg/sdk3rd/qiniu/sslcert.go new file mode 100644 index 00000000..f9784270 --- /dev/null +++ b/pkg/sdk3rd/qiniu/sslcert.go @@ -0,0 +1,80 @@ +package qiniu + +import ( + "context" + "net/http" + "net/url" + + "github.com/qiniu/go-sdk/v7/auth" + "github.com/qiniu/go-sdk/v7/client" +) + +type SslCertManager struct { + client *client.Client +} + +func NewSslCertManager(mac *auth.Credentials) *SslCertManager { + if mac == nil { + mac = auth.Default() + } + + client := &client.Client{Client: &http.Client{Transport: newTransport(mac, nil)}} + return &SslCertManager{client: client} +} + +type GetSslCertListResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + Certs []*struct { + CertID string `json:"certid"` + Name string `json:"name"` + CommonName string `json:"common_name"` + DnsNames []string `json:"dnsnames"` + CreateTime int64 `json:"create_time"` + NotBefore int64 `json:"not_before"` + NotAfter int64 `json:"not_after"` + ProductType string `json:"product_type"` + ProductShortName string `json:"product_short_name,omitempty"` + OrderId string `json:"orderid,omitempty"` + CertType string `json:"cert_type"` + Encrypt string `json:"encrypt"` + EncryptParameter string `json:"encryptParameter,omitempty"` + Enable bool `json:"enable"` + } `json:"certs"` + Marker string `json:"marker"` +} + +func (m *SslCertManager) GetSslCertList(ctx context.Context, marker string, limit int32) (*GetSslCertListResponse, error) { + resp := new(GetSslCertListResponse) + if err := m.client.Call(ctx, resp, http.MethodGet, urlf("sslcert?marker=%s&limit=%d", url.QueryEscape(marker), limit), nil); err != nil { + return nil, err + } + return resp, nil +} + +type UploadSslCertRequest struct { + Name string `json:"name"` + CommonName string `json:"common_name"` + Certificate string `json:"ca"` + PrivateKey string `json:"pri"` +} + +type UploadSslCertResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + CertID string `json:"certID"` +} + +func (m *SslCertManager) UploadSslCert(ctx context.Context, name string, commonName string, certificate string, privateKey string) (*UploadSslCertResponse, error) { + req := &UploadSslCertRequest{ + Name: name, + CommonName: commonName, + Certificate: certificate, + PrivateKey: privateKey, + } + resp := new(UploadSslCertResponse) + if err := m.client.CallWithJson(ctx, resp, http.MethodPost, urlf("sslcert"), nil, req); err != nil { + return nil, err + } + return resp, nil +} diff --git a/pkg/sdk3rd/qiniu/util.go b/pkg/sdk3rd/qiniu/util.go new file mode 100644 index 00000000..0957a310 --- /dev/null +++ b/pkg/sdk3rd/qiniu/util.go @@ -0,0 +1,14 @@ +package qiniu + +import ( + "fmt" + "strings" +) + +const qiniuHost = "https://api.qiniu.com" + +func urlf(pathf string, pathargs ...any) string { + path := fmt.Sprintf(pathf, pathargs...) + path = strings.TrimPrefix(path, "/") + return qiniuHost + "/" + path +} diff --git a/pkg/sdk3rd/upyun/console/client.go b/pkg/sdk3rd/upyun/console/client.go index 7af3e7ae..0c54ea61 100644 --- a/pkg/sdk3rd/upyun/console/client.go +++ b/pkg/sdk3rd/upyun/console/client.go @@ -103,8 +103,11 @@ func (c *Client) doRequestWithResult(req *resty.Request, res apiResponse) (*rest if err := json.Unmarshal(resp.Body(), &res); err != nil { return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err) } else { - if tdata := res.GetData(); tdata == nil { - return resp, fmt.Errorf("sdkerr: empty data") + tresp := &apiResponseBase{} + if err := json.Unmarshal(resp.Body(), &tresp); err != nil { + return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err) + } else if tdata := tresp.GetData(); tdata == nil { + return resp, fmt.Errorf("sdkerr: received empty data") } else if terrcode := tdata.GetErrorCode(); terrcode != 0 { return resp, fmt.Errorf("sdkerr: code='%d', message='%s'", terrcode, tdata.GetMessage()) } diff --git a/pkg/utils/cert/common.go b/pkg/utils/cert/common.go index 56703125..bdc86367 100644 --- a/pkg/utils/cert/common.go +++ b/pkg/utils/cert/common.go @@ -2,6 +2,7 @@ package cert import ( "crypto/x509" + "encoding/pem" ) // 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 @@ -24,3 +25,18 @@ func EqualCertificate(a, b *x509.Certificate) bool { a.Issuer.SerialNumber == b.Issuer.SerialNumber && a.Subject.SerialNumber == b.Subject.SerialNumber } + +func decodePEM(data []byte) []*pem.Block { + blocks := make([]*pem.Block, 0) + for { + block, rest := pem.Decode(data) + if block == nil { + break + } + + blocks = append(blocks, block) + data = rest + } + + return blocks +} diff --git a/pkg/utils/cert/extractor.go b/pkg/utils/cert/extractor.go index 1e116b1f..b9e4607f 100644 --- a/pkg/utils/cert/extractor.go +++ b/pkg/utils/cert/extractor.go @@ -3,6 +3,7 @@ package cert import ( "encoding/pem" "errors" + "fmt" ) // 从 PEM 编码的证书字符串解析并提取服务器证书和中间证书。 @@ -15,32 +16,27 @@ import ( // - intermediaCertPEM: 中间证书的 PEM 内容。 // - err: 错误。 func ExtractCertificatesFromPEM(certPEM string) (_serverCertPEM string, _intermediaCertPEM string, _err error) { - pemBlocks := make([]*pem.Block, 0) - pemData := []byte(certPEM) - for { - block, rest := pem.Decode(pemData) - if block == nil || block.Type != "CERTIFICATE" { - break + blocks := decodePEM([]byte(certPEM)) + for i, block := range blocks { + if block.Type != "CERTIFICATE" { + return "", "", fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) } - - pemBlocks = append(pemBlocks, block) - pemData = rest } serverCertPEM := "" intermediaCertPEM := "" - if len(pemBlocks) == 0 { + if len(blocks) == 0 { return "", "", errors.New("failed to decode PEM block") } - if len(pemBlocks) > 0 { - serverCertPEM = string(pem.EncodeToMemory(pemBlocks[0])) + if len(blocks) > 0 { + serverCertPEM = string(pem.EncodeToMemory(blocks[0])) } - if len(pemBlocks) > 1 { - for i := 1; i < len(pemBlocks); i++ { - intermediaCertPEM += string(pem.EncodeToMemory(pemBlocks[i])) + if len(blocks) > 1 { + for i := 1; i < len(blocks); i++ { + intermediaCertPEM += string(pem.EncodeToMemory(blocks[i])) } } diff --git a/pkg/utils/cert/transformer.go b/pkg/utils/cert/transformer.go index bf467efa..690ae19f 100644 --- a/pkg/utils/cert/transformer.go +++ b/pkg/utils/cert/transformer.go @@ -2,8 +2,9 @@ package cert import ( "bytes" - "encoding/pem" + "crypto/x509" "errors" + "fmt" "time" "github.com/pavlo-v-chernykh/keystore-go/v4" @@ -21,9 +22,19 @@ import ( // - data: PFX 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPassword string) ([]byte, error) { - cert, err := ParseCertificateFromPEM(certPEM) - if err != nil { - return nil, err + blocks := decodePEM([]byte(certPEM)) + + certs := make([]*x509.Certificate, 0, len(blocks)) + for i, block := range blocks { + if block.Type != "CERTIFICATE" { + return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, block.Type) + } + + cert, err := x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, err + } + certs = append(certs, cert) } privkey, err := ParsePrivateKeyFromPEM(privkeyPEM) @@ -31,12 +42,16 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass return nil, err } - pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, pfxPassword) - if err != nil { - return nil, err + var pfxData []byte + if len(certs) == 0 { + return nil, errors.New("failed to decode certificate PEM") + } else if len(certs) == 1 { + pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], nil, pfxPassword) + } else { + pfxData, err = pkcs12.Legacy.Encode(privkey, certs[0], certs[1:], pfxPassword) } - return pfxData, nil + return pfxData, err } // 将 PEM 编码的证书字符串转换为 JKS 格式。 @@ -52,28 +67,33 @@ func TransformCertificateFromPEMToPFX(certPEM string, privkeyPEM string, pfxPass // - data: JKS 格式的证书数据。 // - err: 错误。 func TransformCertificateFromPEMToJKS(certPEM string, privkeyPEM string, jksAlias string, jksKeypass string, jksStorepass string) ([]byte, error) { - certBlock, _ := pem.Decode([]byte(certPEM)) - if certBlock == nil { + certBlocks := decodePEM([]byte(certPEM)) + if len(certBlocks) == 0 { return nil, errors.New("failed to decode certificate PEM") } - privkeyBlock, _ := pem.Decode([]byte(privkeyPEM)) - if privkeyBlock == nil { + privkeyBlocks := decodePEM([]byte(privkeyPEM)) + if len(privkeyBlocks) == 0 { return nil, errors.New("failed to decode private key PEM") } - ks := keystore.New() entry := keystore.PrivateKeyEntry{ - CreationTime: time.Now(), - PrivateKey: privkeyBlock.Bytes, - CertificateChain: []keystore.Certificate{ - { - Type: "X509", - Content: certBlock.Bytes, - }, - }, + CreationTime: time.Now(), + PrivateKey: privkeyBlocks[0].Bytes, + CertificateChain: make([]keystore.Certificate, len(certBlocks)), + } + for i, certBlock := range certBlocks { + if certBlock.Type != "CERTIFICATE" { + return nil, fmt.Errorf("invalid PEM block type at %d, expected 'CERTIFICATE', got '%s'", i, certBlock.Type) + } + + entry.CertificateChain[i] = keystore.Certificate{ + Type: "X509", + Content: certBlock.Bytes, + } } + ks := keystore.New() if err := ks.SetPrivateKeyEntry(jksAlias, entry, []byte(jksKeypass)); err != nil { return nil, err } diff --git a/pkg/utils/ifelse/ifelse.go b/pkg/utils/ifelse/ifelse.go index fce28401..2e1bc388 100644 --- a/pkg/utils/ifelse/ifelse.go +++ b/pkg/utils/ifelse/ifelse.go @@ -1,34 +1,84 @@ package ifelse +type branch[T any] struct { + cond bool + fn func() T +} + type ifExpr[T any] struct { - condition bool + cond bool } type thenExpr[T any] struct { - condition bool - consequent T + branches []branch[T] } -// 示例: +type elseIfExpr[T any] struct { + branches []branch[T] + cond bool +} + +// 用法示例: // -// result := ifelse.If[T](condition).Then(consequent).Else(alternative) +// result := ifelse.If[string](age < 18).Then("child"). +// ElseIf(age < 60).Then("adult"). +// ElseIf(age < 120).Then("senior"). +// Else("invalid") func If[T any](condition bool) *ifExpr[T] { - return &ifExpr[T]{ - condition: condition, - } + return &ifExpr[T]{cond: condition} } func (e *ifExpr[T]) Then(consequent T) *thenExpr[T] { return &thenExpr[T]{ - condition: e.condition, - consequent: consequent, + branches: []branch[T]{ + {cond: e.cond, fn: func() T { return consequent }}, + }, + } +} + +func (e *ifExpr[T]) ThenFunc(consequent func() T) *thenExpr[T] { + return &thenExpr[T]{ + branches: []branch[T]{ + {cond: e.cond, fn: consequent}, + }, + } +} + +func (e *thenExpr[T]) ElseIf(condition bool) *elseIfExpr[T] { + return &elseIfExpr[T]{ + branches: e.branches, + cond: condition, + } +} + +func (e *elseIfExpr[T]) Then(alternative T) *thenExpr[T] { + branch := branch[T]{cond: e.cond, fn: func() T { return alternative }} + return &thenExpr[T]{ + branches: append(e.branches, branch), + } +} + +func (e *elseIfExpr[T]) ThenFunc(alternativeFunc func() T) *thenExpr[T] { + branch := branch[T]{cond: e.cond, fn: alternativeFunc} + return &thenExpr[T]{ + branches: append(e.branches, branch), } } func (e *thenExpr[T]) Else(alternative T) T { - if e.condition { - return e.consequent + for _, b := range e.branches { + if b.cond { + return b.fn() + } } - return alternative } + +func (e *thenExpr[T]) ElseFunc(alternativeFunc func() T) T { + for _, b := range e.branches { + if b.cond { + return b.fn() + } + } + return alternativeFunc() +} diff --git a/pkg/utils/ifelse/ifelse_test.go b/pkg/utils/ifelse/ifelse_test.go new file mode 100644 index 00000000..51656487 --- /dev/null +++ b/pkg/utils/ifelse/ifelse_test.go @@ -0,0 +1,196 @@ +package ifelse_test + +import ( + "testing" + + "github.com/certimate-go/certimate/pkg/utils/ifelse" +) + +func TestIfTrue(t *testing.T) { + result := ifelse.If[string](true). + Then("true branch"). + Else("false branch") + + if result != "true branch" { + t.Errorf("Expected 'true branch', got '%s'", result) + } +} + +func TestIfFalse(t *testing.T) { + result := ifelse.If[string](false). + Then("true branch"). + Else("false branch") + + if result != "false branch" { + t.Errorf("Expected 'false branch', got '%s'", result) + } +} + +func TestElseIfFirstMatch(t *testing.T) { + result := ifelse.If[string](false). + Then("should not run"). + ElseIf(true). + Then("elseif branch"). + Else("should not run") + + if result != "elseif branch" { + t.Errorf("Expected 'elseif branch', got '%s'", result) + } +} + +func TestElseIfSecondMatch(t *testing.T) { + result := ifelse.If[string](false). + Then("should not run"). + ElseIf(false). + Then("should not run"). + ElseIf(true). + Then("second elseif"). + Else("should not run") + + if result != "second elseif" { + t.Errorf("Expected 'second elseif', got '%s'", result) + } +} + +func TestMultipleConditions(t *testing.T) { + result := ifelse.If[string](1 > 2). + Then("impossible"). + ElseIf(2+2 == 5). + Then("false math"). + ElseIf(3*3 == 9). + Then("correct math"). + Else("fallback") + + if result != "correct math" { + t.Errorf("Expected 'correct math', got '%s'", result) + } +} + +func TestAllConditionsFalse(t *testing.T) { + result := ifelse.If[int](false). + Then(1). + ElseIf(false). + Then(2). + ElseIf(false). + Then(3). + Else(99) + + if result != 99 { + t.Errorf("Expected 99, got %d", result) + } +} + +func TestLazyEvaluationThen(t *testing.T) { + called := []string{} + + result := ifelse.If[string](true). + ThenFunc(func() string { + called = append(called, "then") + return "then" + }). + ElseIf(true). + ThenFunc(func() string { + called = append(called, "elseif") + return "elseif" + }). + ElseFunc(func() string { + called = append(called, "else") + return "else" + }) + + // 验证结果和调用情况 + if result != "then" { + t.Errorf("Expected 'then', got '%s'", result) + } + + if len(called) != 1 || called[0] != "then" { + t.Errorf("Expected only 'then' called, got %v", called) + } +} + +func TestLazyEvaluationElseIf(t *testing.T) { + called := []string{} + + result := ifelse.If[string](false). + ThenFunc(func() string { + called = append(called, "then") + return "then" + }). + ElseIf(true). + ThenFunc(func() string { + called = append(called, "elseif") + return "elseif" + }). + ElseFunc(func() string { + called = append(called, "else") + return "else" + }) + + // 验证结果和调用情况 + if result != "elseif" { + t.Errorf("Expected 'elseif', got '%s'", result) + } + + if len(called) != 1 || called[0] != "elseif" { + t.Errorf("Expected only 'elseif' called, got %v", called) + } +} + +func TestLazyEvaluationElse(t *testing.T) { + called := []string{} + + result := ifelse.If[string](false). + ThenFunc(func() string { + called = append(called, "then") + return "then" + }). + ElseIf(false). + ThenFunc(func() string { + called = append(called, "elseif") + return "elseif" + }). + ElseFunc(func() string { + called = append(called, "else") + return "else" + }) + + // 验证结果和调用情况 + if result != "else" { + t.Errorf("Expected 'else', got '%s'", result) + } + + if len(called) != 1 || called[0] != "else" { + t.Errorf("Expected only 'else' called, got %v", called) + } +} + +func TestMixedValueAndFunc(t *testing.T) { + result := ifelse.If[int](false). + Then(0). + ElseIf(false). + ThenFunc(func() int { + return 1 + }). + ElseIf(true). + Then(2). + Else(3) + + if result != 2 { + t.Errorf("Expected 2, got %d", result) + } +} + +func TestComplexNumericLogic(t *testing.T) { + x := 15 + result := ifelse.If[string](x < 10). + Then("single digit"). + ElseIf(x < 20). + Then("teens"). + ElseIf(x < 30). + Then("twenties"). + Else("older") + + if result != "teens" { + t.Errorf("Expected 'teens', got '%s'", result) + } +} diff --git a/ui/public/imgs/providers/kong.png b/ui/public/imgs/providers/kong.png new file mode 100644 index 00000000..2b95e955 Binary files /dev/null and b/ui/public/imgs/providers/kong.png differ diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index d49fe661..ba9e1318 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -50,6 +50,7 @@ import AccessFormGoogleTrustServicesConfig from "./AccessFormGoogleTrustServices import AccessFormHetznerConfig from "./AccessFormHetznerConfig"; import AccessFormHuaweiCloudConfig from "./AccessFormHuaweiCloudConfig"; import AccessFormJDCloudConfig from "./AccessFormJDCloudConfig"; +import AccessFormKongConfig from "./AccessFormKongConfig"; import AccessFormKubernetesConfig from "./AccessFormKubernetesConfig"; import AccessFormLarkBotConfig from "./AccessFormLarkBotConfig"; import AccessFormLeCDNConfig from "./AccessFormLeCDNConfig"; @@ -266,6 +267,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.JDCLOUD: return ; + case ACCESS_PROVIDERS.KONG: + return ; case ACCESS_PROVIDERS.KUBERNETES: return ; case ACCESS_PROVIDERS.LARKBOT: diff --git a/ui/src/components/access/AccessFormKongConfig.tsx b/ui/src/components/access/AccessFormKongConfig.tsx new file mode 100644 index 00000000..6ac1d0d5 --- /dev/null +++ b/ui/src/components/access/AccessFormKongConfig.tsx @@ -0,0 +1,71 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Switch } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod/v4"; + +import { type AccessConfigForKong } from "@/domain/access"; + +type AccessFormKongConfigFieldValues = Nullish; + +export type AccessFormKongConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormKongConfigFieldValues; + onValuesChange?: (values: AccessFormKongConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormKongConfigFieldValues => { + return { + serverUrl: "http://:8001/", + apiToken: "", + }; +}; + +const AccessFormKongConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormKongConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z.url(t("common.errmsg.url_invalid")), + apiToken: z.string().nullish(), + allowInsecureConnections: z.boolean().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + + + +
+ ); +}; + +export default AccessFormKongConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index baf4dcf4..10fb1765 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -65,6 +65,7 @@ import DeployNodeConfigFormJDCloudALBConfig from "./DeployNodeConfigFormJDCloudA import DeployNodeConfigFormJDCloudCDNConfig from "./DeployNodeConfigFormJDCloudCDNConfig"; import DeployNodeConfigFormJDCloudLiveConfig from "./DeployNodeConfigFormJDCloudLiveConfig"; import DeployNodeConfigFormJDCloudVODConfig from "./DeployNodeConfigFormJDCloudVODConfig"; +import DeployNodeConfigFormKongConfig from "./DeployNodeConfigFormKongConfig"; import DeployNodeConfigFormKubernetesSecretConfig from "./DeployNodeConfigFormKubernetesSecretConfig"; import DeployNodeConfigFormLeCDNConfig from "./DeployNodeConfigFormLeCDNConfig"; import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; @@ -304,6 +305,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.JDCLOUD_VOD: return ; + case DEPLOYMENT_PROVIDERS.KONG: + return ; case DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET: return ; case DEPLOYMENT_PROVIDERS.LECDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx new file mode 100644 index 00000000..e023c014 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormKongConfig.tsx @@ -0,0 +1,89 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod/v4"; + +import Show from "@/components/Show"; + +type DeployNodeConfigFormKongConfigFieldValues = Nullish<{ + resourceType: string; + certificateId?: string; +}>; + +export type DeployNodeConfigFormKongConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormKongConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormKongConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + +const initFormModel = (): DeployNodeConfigFormKongConfigFieldValues => { + return { + resourceType: RESOURCE_TYPE_CERTIFICATE, + certificateId: "", + }; +}; + +const DeployNodeConfigFormKongConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormKongConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, t("workflow_node.deploy.form.kong_resource_type.placeholder")), + workspace: z.string().nullish(), + certificateId: z + .string() + .nullish() + .refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.kong_certificate_id.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + + } + > + + + +
+ ); +}; + +export default DeployNodeConfigFormKongConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx index ebeb1cff..132de43f 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx @@ -152,9 +152,11 @@ chmod 755 "$fnCertPath" chmod 755 "$fnKeyPath" # 更新数据库 +NEW_EFFECT_DATE=$(openssl x509 -startdate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") +NEW_EFFECT_TIMESTAMP=$(date -d "$NEW_EFFECT_DATE" +%s%3N) NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/") NEW_EXPIRY_TIMESTAMP=$(date -d "$NEW_EXPIRY_DATE" +%s%3N) -psql -U postgres -d trim_connect -c "UPDATE cert SET valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" +psql -U postgres -d trim_connect -c "UPDATE cert SET valid_from=$NEW_EFFECT_TIMESTAMP, valid_to=$NEW_EXPIRY_TIMESTAMP WHERE domain='$domain'" # 重启服务 systemctl restart webdav.service diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx index bc9e938d..fbb5f9d5 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCDNConfig.tsx @@ -58,7 +58,7 @@ const DeployNodeConfigFormTencentCloudCDNConfig = ({ rules={[formRule]} tooltip={} > - + } > - + } > - + } > - + ; export type DeployNodeConfigFormTencentCloudEOConfigProps = { @@ -23,6 +24,8 @@ const initFormModel = (): DeployNodeConfigFormTencentCloudEOConfigFieldValues => return {}; }; +const MULTIPLE_INPUT_SEPARATOR = ";"; + const DeployNodeConfigFormTencentCloudEOConfig = ({ form: formInst, formName, @@ -37,9 +40,14 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({ zoneId: z .string(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")) .nonempty(t("workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder")), - domain: z - .string(t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder")) - .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + domains: z + .string(t("workflow_node.deploy.form.tencentcloud_eo_domains.placeholder")) + .refine((v) => { + if (!v) return false; + return String(v) + .split(MULTIPLE_INPUT_SEPARATOR) + .every((e) => validDomainName(e, { allowWildcard: true })); + }, t("common.errmsg.domain_invalid")), }); const formRule = createSchemaFieldRule(formSchema); @@ -62,7 +70,7 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({ rules={[formRule]} tooltip={} > - + } + tooltip={} > - + ); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx index 1cac40dc..75cce78a 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx @@ -73,7 +73,7 @@ const DeployNodeConfigFormTencentCloudGAAPConfig = ({ rules={[formRule]} tooltip={} > - + diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx index 3128a41a..2ec93233 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSCFConfig.tsx @@ -60,7 +60,7 @@ const DeployNodeConfigFormTencentCloudSCFConfig = ({ rules={[formRule]} tooltip={} > - + } > - + ); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx index 1a104bf2..638fe688 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudSSLDeployConfig.tsx @@ -75,7 +75,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({ rules={[formRule]} tooltip={} > - + { - return { - isReplaced: true, - }; + return {}; }; const DeployNodeConfigFormTencentCloudSSLUpdateConfig = ({ @@ -49,12 +47,15 @@ const DeployNodeConfigFormTencentCloudSSLUpdateConfig = ({ .split(MULTIPLE_INPUT_SEPARATOR) .every((e) => !!e.trim()); }, t("workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.placeholder")), - resourceRegions: z.string(t("workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder")).refine((v) => { - if (!v) return false; - return String(v) - .split(MULTIPLE_INPUT_SEPARATOR) - .every((e) => !!e.trim()); - }, t("workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder")), + resourceRegions: z + .string(t("workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder")) + .nullish() + .refine((v) => { + if (!v) return true; + return String(v) + .split(MULTIPLE_INPUT_SEPARATOR) + .every((e) => !!e.trim()); + }, t("workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder")), isReplaced: z.boolean().nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -82,7 +83,7 @@ const DeployNodeConfigFormTencentCloudSSLUpdateConfig = ({ rules={[formRule]} tooltip={} > - + - + } + > diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx index 9a47a221..aa3fb691 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudVODConfig.tsx @@ -64,7 +64,7 @@ const DeployNodeConfigFormTencentCloudVODConfig = ({ rules={[formRule]} tooltip={} > - + } > - + [ type, diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 76be1cfd..0dd233d6 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -286,6 +286,11 @@ "access.form.k8s_kubeconfig.label": "KubeConfig", "access.form.k8s_kubeconfig.placeholder": "Please enter KubeConfig file", "access.form.k8s_kubeconfig.tooltip": "For more information, see https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/

Leave it blank to use the Pod's ServiceAccount.", + "access.form.kong_server_url.label": "Kong admin API server URL", + "access.form.kong_server_url.placeholder": "Please enter Kong admin API server URL", + "access.form.kong_api_token.label": "Kong admin API token (Optional)", + "access.form.kong_api_token.placeholder": "Please enter Kong admin API token", + "access.form.kong_api_token.tooltip": "For more information, see https://developer.konghq.com/admin-api/", "access.form.larkbot_webhook_url.label": "Lark bot Webhook URL", "access.form.larkbot_webhook_url.placeholder": "Please enter Lark bot Webhook URL", "access.form.larkbot_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index fe76a727..9c7d0053 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -101,6 +101,7 @@ "provider.jdcloud.dns": "JD Cloud - DNS", "provider.jdcloud.live": "JD Cloud - Live Video", "provider.jdcloud.vod": "JD Cloud - VOD (Video on Demand)", + "provider.kong": "Kong", "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.larkbot": "Lark Bot", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index caf55597..bc5a16a1 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -268,8 +268,8 @@ "workflow_node.deploy.form.aliyun_oss_bucket.label": "Alibaba Cloud OSS bucket", "workflow_node.deploy.form.aliyun_oss_bucket.placeholder": "Please enter Alibaba Cloud OSS bucket name", "workflow_node.deploy.form.aliyun_oss_bucket.tooltip": "For more information, see https://oss.console.aliyun.com", - "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS domain", - "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS domain name", + "workflow_node.deploy.form.aliyun_oss_domain.label": "Alibaba Cloud OSS custom domain", + "workflow_node.deploy.form.aliyun_oss_domain.placeholder": "Please enter Alibaba Cloud OSS bucket custom domain name", "workflow_node.deploy.form.aliyun_oss_domain.tooltip": "For more information, see https://oss.console.aliyun.com", "workflow_node.deploy.form.aliyun_vod_region.label": "Alibaba Cloud VOD region", "workflow_node.deploy.form.aliyun_vod_region.placeholder": "Please enter Alibaba Cloud VOD region (e.g. cn-hangzhou)", @@ -528,6 +528,15 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "Please enter Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "For more information, see https://kubernetes.io/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.kong_resource_type.label": "Resource type", + "workflow_node.deploy.form.kong_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.kong_resource_type.option.certificate.label": "SSL certificate", + "workflow_node.deploy.form.kong_workspace.label": "Kong workspace (Optional)", + "workflow_node.deploy.form.kong_workspace.placeholder": "Please enter Kong workspace", + "workflow_node.deploy.form.kong_workspace.tooltip": "You can find it on Kong dashboard.", + "workflow_node.deploy.form.kong_certificate_id.label": "Kong certificate ID", + "workflow_node.deploy.form.kong_certificate_id.placeholder": "Please enter Kong certificate ID", + "workflow_node.deploy.form.kong_certificate_id.tooltip": "You can find it on Kong dashboard.", "workflow_node.deploy.form.lecdn_resource_type.label": "Resource type", "workflow_node.deploy.form.lecdn_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.lecdn_resource_type.option.certificate.label": "Certificate", @@ -592,8 +601,8 @@ "workflow_node.deploy.form.qiniu_cdn_domain.label": "Qiniu CDN domain", "workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "Please enter Qiniu CDN domain name", "workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "For more information, see https://portal.qiniu.com/cdn", - "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo bucket domain", - "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket domain name", + "workflow_node.deploy.form.qiniu_kodo_domain.label": "Qiniu Kodo custom domain", + "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "Please enter Qiniu Kodo bucket custom domain name", "workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "For more information, see https://portal.qiniu.com/kodo", "workflow_node.deploy.form.qiniu_pili_hub.label": "Qiniu Pili hub", "workflow_node.deploy.form.qiniu_pili_hub.placeholder": "Please enter Qiniu Pili hub name", @@ -697,8 +706,8 @@ "workflow_node.deploy.form.tencentcloud_cos_bucket.label": "Tencent Cloud COS bucket", "workflow_node.deploy.form.tencentcloud_cos_bucket.placeholder": "Please enter Tencent Cloud COS bucket name", "workflow_node.deploy.form.tencentcloud_cos_bucket.tooltip": "For more information, see https://console.tencentcloud.com/cos", - "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS domain", - "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS domain name", + "workflow_node.deploy.form.tencentcloud_cos_domain.label": "Tencent Cloud COS custom domain", + "workflow_node.deploy.form.tencentcloud_cos_domain.placeholder": "Please enter Tencent Cloud COS bucket custom domain name", "workflow_node.deploy.form.tencentcloud_cos_domain.tooltip": "For more information, see https://console.tencentcloud.com/cos", "workflow_node.deploy.form.tencentcloud_css_endpoint.label": "Tencent Cloud CSS API endpoint (Optional)", "workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder": "Please enter Tencent Cloud CSS API endpoint (e.g. live.intl.tencentcloudapi.com)", @@ -718,9 +727,11 @@ "workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "Tencent Cloud EdgeOne zone ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", - "workflow_node.deploy.form.tencentcloud_eo_domain.label": "Tencent Cloud EdgeOne domain", - "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "Please enter Tencent Cloud EdgeOne domain name", - "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.label": "Tencent Cloud EdgeOne domains", + "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "Please enter Tencent Cloud EdgeOne domain names (separated by semicolons)", + "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "Change Tencent Cloud EdgeOne domain", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "Please enter Tencent Cloud EdgeOne domain name", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "Tencent Cloud GAAP API endpoint (Optional)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "
  • gaap.intl.tencentcloudapi.com for Tencent Cloud International
  • gaap.tencentcloudapi.com for Tencent Cloud in China
", @@ -770,15 +781,16 @@ "workflow_node.deploy.form.tencentcloud_ssl_update_certificate_id.tooltip": "For more information, see https://console.cloud.tencent.com/certoverview", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.label": "Tencent Cloud resource types", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.placeholder": "Please enter Tencent Cloud resource types (separated by semicolons)", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "For more information, see https://www.tencentcloud.com/document/product/1007/57981", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "For more information, see https://www.tencentcloud.com/document/product/1007/57981 or https://www.tencentcloud.com/document/product/1007/70503", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.multiple_input_modal.title": "Change Tencent Cloud resource types", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.multiple_input_modal.placeholder": "Please enter Tencent Cloud resource type", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.label": "Tencent Cloud resource regions (Optional)", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder": "Please enter Tencent Cloud resource regions (separated by semicolons)", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.tooltip": "For more information, see https://www.tencentcloud.com/document/product/1007/57981", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.tooltip": "For more information, see https://www.tencentcloud.com/document/product/1007/57981 or https://www.tencentcloud.com/document/product/1007/70503", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.title": "Change Tencent Cloud resource regions", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.placeholder": "Please enter Tencent Cloud resource region", "workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.label": "Renewal certificate (certificate ID unchanged)", + "workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.tooltip": "When unchecked, it will invoke UpdateCertificateInstance; otherwise, it will invoke UploadUpdateCertificateInstance.", "workflow_node.deploy.form.tencentcloud_vod_endpoint.label": "Tencent Cloud VOD API endpoint (Optional)", "workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder": "Please enter Tencent Cloud VOD API endpoint (e.g. vod.intl.tencentcloudapi.com)", "workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip": "
  • vod.intl.tencentcloudapi.com for Tencent Cloud International
  • vod.tencentcloudapi.com for Tencent Cloud in China
", @@ -812,8 +824,8 @@ "workflow_node.deploy.form.ucloud_us3_bucket.label": "UCloud US3 bucket", "workflow_node.deploy.form.ucloud_us3_bucket.placeholder": "Please enter UCloud US3 bucket name", "workflow_node.deploy.form.ucloud_us3_bucket.tooltip": "For more information, see https://console.ucloud-global.com/ufile", - "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 domain", - "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 domain name", + "workflow_node.deploy.form.ucloud_us3_domain.label": "UCloud US3 custom domain", + "workflow_node.deploy.form.ucloud_us3_domain.placeholder": "Please enter UCloud US3 bucket custom domain name", "workflow_node.deploy.form.ucloud_us3_domain.tooltip": "For more information, see https://console.ucloud-global.com/ufile", "workflow_node.deploy.form.unicloud_webhost.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the uniCloud, please create a GitHub Issue.", "workflow_node.deploy.form.unicloud_webhost_space_provider.label": "uniCloud space provider", @@ -830,8 +842,8 @@ "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "Please enter UPYUN CDN domain name", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "For more information, see https://console.upyun.com/services/cdn/", "workflow_node.deploy.form.upyun_file.guide": "Tips: This uses webpage simulator login and does not guarantee stability. If there are any changes to the UPYUN, please create a GitHub Issue.", - "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN bucket domain", - "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN bucket domain name", + "workflow_node.deploy.form.upyun_file_domain.label": "UPYUN USS custom domain", + "workflow_node.deploy.form.upyun_file_domain.placeholder": "Please enter UPYUN USS bucket custom domain name", "workflow_node.deploy.form.upyun_file_domain.tooltip": "For more information, see https://console.upyun.com/services/file/", "workflow_node.deploy.form.volcengine_alb_region.label": "VolcEngine ALB region", "workflow_node.deploy.form.volcengine_alb_region.placeholder": "Please enter VolcEngine ALB region (e.g. cn-beijing)", @@ -876,8 +888,8 @@ "workflow_node.deploy.form.volcengine_imagex_service_id.label": "VolcEngine ImageX service ID", "workflow_node.deploy.form.volcengine_imagex_service_id.placeholder": "Please enter VolcEngine ImageX service ID", "workflow_node.deploy.form.volcengine_imagex_service_id.tooltip": "For more information, see https://console.volcengine.com/imagex", - "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX domain", - "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX domain name", + "workflow_node.deploy.form.volcengine_imagex_domain.label": "VolcEngine ImageX custom domain", + "workflow_node.deploy.form.volcengine_imagex_domain.placeholder": "Please enter VolcEngine ImageX custom domain name", "workflow_node.deploy.form.volcengine_imagex_domain.tooltip": "For more information, see https://console.volcengine.com/imagex", "workflow_node.deploy.form.volcengine_live_domain.label": "VolcEngine Live streaming domain", "workflow_node.deploy.form.volcengine_live_domain.placeholder": "Please enter VolcEngine Live streaming domain name", @@ -888,8 +900,8 @@ "workflow_node.deploy.form.volcengine_tos_bucket.label": "VolcEngine TOS bucket", "workflow_node.deploy.form.volcengine_tos_bucket.placeholder": "Please enter VolcEngine TOS bucket name", "workflow_node.deploy.form.volcengine_tos_bucket.tooltip": "For more information, see https://console.volcengine.com/tos", - "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain", - "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name", + "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS custom domain", + "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS bucket custom domain name", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos", "workflow_node.deploy.form.wangsu_cdn_domains.label": "Wangsu Cloud CDN domains", "workflow_node.deploy.form.wangsu_cdn_domains.placeholder": "Please enter Wangsu Cloud CDN domain names (separated by semicolons)", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 837ccc17..b6a4a0be 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -286,6 +286,11 @@ "access.form.k8s_kubeconfig.label": "KubeConfig", "access.form.k8s_kubeconfig.placeholder": "请输入 KubeConfig 文件内容", "access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/

为空时,将使用 Pod 的 ServiceAccount 作为凭证。", + "access.form.kong_server_url.label": "Kong Admin API 服务地址", + "access.form.kong_server_url.placeholder": "请输入 Kong Admin API 服务地址", + "access.form.kong_api_token.label": "Kong Admin API Token(可选)", + "access.form.kong_api_token.placeholder": "请输入 Kong Admin API Token", + "access.form.kong_api_token.tooltip": "这是什么?请参阅 https://developer.konghq.com/admin-api/", "access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址", "access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址", "access.form.larkbot_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index fa16398a..73a3b63c 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -101,6 +101,7 @@ "provider.jdcloud.dns": "京东云 - 云解析 DNS", "provider.jdcloud.live": "京东云 - 视频直播", "provider.jdcloud.vod": "京东云 - 视频点播", + "provider.kong": "Kong", "provider.kubernetes": "Kubernetes", "provider.kubernetes.secret": "Kubernetes - Secret", "provider.larkbot": "飞书群机器人", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 4e20f18c..09fc27e9 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -526,6 +526,15 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret 数据键(用于存放私钥的字段)", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "请输入 Kubernetes Secret 中用于存放私钥的数据键", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.kong_resource_type.label": "证书部署方式", + "workflow_node.deploy.form.kong_resource_type.placeholder": "请选择证书部署方式", + "workflow_node.deploy.form.kong_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.kong_workspace.label": "Kong 工作空间(可选)", + "workflow_node.deploy.form.kong_workspace.placeholder": "请输入 Kong 工作空间", + "workflow_node.deploy.form.kong_workspace.tooltip": "请登录 Kong 控制台查看。", + "workflow_node.deploy.form.kong_certificate_id.label": "Kong 证书 ID", + "workflow_node.deploy.form.kong_certificate_id.placeholder": "请输入 Kong 证书 ID", + "workflow_node.deploy.form.kong_certificate_id.tooltip": "请登录 Kong 控制台查看。", "workflow_node.deploy.form.lecdn_resource_type.label": "证书部署方式", "workflow_node.deploy.form.lecdn_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.lecdn_resource_type.option.certificate.label": "替换指定证书", @@ -590,8 +599,8 @@ "workflow_node.deploy.form.qiniu_cdn_domain.label": "七牛云 CDN 加速域名", "workflow_node.deploy.form.qiniu_cdn_domain.placeholder": "请输入七牛云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.qiniu_cdn_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/cdn", - "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储加速域名", - "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储加速域名", + "workflow_node.deploy.form.qiniu_kodo_domain.label": "七牛云对象存储自定义域名", + "workflow_node.deploy.form.qiniu_kodo_domain.placeholder": "请输入七牛云对象存储自定义域名", "workflow_node.deploy.form.qiniu_kodo_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/kodo", "workflow_node.deploy.form.qiniu_pili_hub.label": "七牛云视频直播空间名", "workflow_node.deploy.form.qiniu_pili_hub.placeholder": "请输入七牛云视频直播空间名", @@ -661,13 +670,13 @@ "workflow_node.deploy.form.ssh_use_scp.tooltip": "如果你的远程服务器不支持 SFTP,请开启此选项回退为 SCP。", "workflow_node.deploy.form.tencentcloud_cdn_endpoint.label": "腾讯云 CDN 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder": "请输入腾讯云 CDN 接口端点(例如:cdn.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/228/30976", + "workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/228/30976

国际站用户请填写 cdn.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_cdn_domain.label": "腾讯云 CDN 加速域名", "workflow_node.deploy.form.tencentcloud_cdn_domain.placeholder": "请输入腾讯云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/cdn", "workflow_node.deploy.form.tencentcloud_clb_endpoint.label": "腾讯云 CLB 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder": "请输入腾讯云 CLB 接口端点(例如:clb.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/214/30669", + "workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/214/30669

国际站用户请填写 clb.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_clb_region.label": "腾讯云 CLB 产品地域", "workflow_node.deploy.form.tencentcloud_clb_region.placeholder": "请输入腾讯云 CLB 服务地域(例如:ap-guangzhou)", "workflow_node.deploy.form.tencentcloud_clb_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/214/33415", @@ -700,28 +709,30 @@ "workflow_node.deploy.form.tencentcloud_cos_domain.tooltip": "这是什么?请参阅 see https://console.cloud.tencent.com/cos", "workflow_node.deploy.form.tencentcloud_css_endpoint.label": "腾讯云云直播接口端点(可选)", "workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder": "请输入腾讯云云直播接口端点(例如:live.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/267/20458", + "workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/267/20458

国际站用户请填写 live.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_css_domain.label": "腾讯云云直播播放域名", "workflow_node.deploy.form.tencentcloud_css_domain.placeholder": "请输入腾讯云云直播播放域名", "workflow_node.deploy.form.tencentcloud_css_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/live", "workflow_node.deploy.form.tencentcloud_ecdn_endpoint.label": "腾讯云 ECDN 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder": "请输入腾讯云 ECDN 接口端点(例如:cdn.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/214/30669", + "workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/214/30669

国际站用户请填写 cdn.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_ecdn_domain.label": "腾讯云 ECDN 加速域名", "workflow_node.deploy.form.tencentcloud_ecdn_domain.placeholder": "请输入腾讯云 ECDN 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_ecdn_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/cdn", "workflow_node.deploy.form.tencentcloud_eo_endpoint.label": "腾讯云 EdgeOne 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder": "请输入腾讯云 EdgeOne 接口端点(例如:teo.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/1552/80723", + "workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/1552/80723

国际站用户请填写 teo.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_eo_zone_id.label": "腾讯云 EdgeOne 站点 ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID", "workflow_node.deploy.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", - "workflow_node.deploy.form.tencentcloud_eo_domain.label": "腾讯云 EdgeOne 加速域名", - "workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)", - "workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.label": "腾讯云 EdgeOne 加速域名", + "workflow_node.deploy.form.tencentcloud_eo_domains.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名;多个值请用半角分号隔开)", + "workflow_node.deploy.form.tencentcloud_eo_domains.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title": "修改腾讯云 EdgeOne 加速域名", + "workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.label": "腾讯云 GAAP 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/608/36934", + "workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/608/36934

国际站用户请填写 gaap.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_gaap_resource_type.label": "证书部署方式", "workflow_node.deploy.form.tencentcloud_gaap_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label": "替换指定监听器的证书", @@ -733,7 +744,7 @@ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/gaap", "workflow_node.deploy.form.tencentcloud_scf_endpoint.label": "腾讯云 SCF 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder": "请输入腾讯云 SCF 接口端点(例如:scf.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/583/17237", + "workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/583/17237

国际站用户请填写 scf.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_scf_region.label": "腾讯云 SCF 产品地域", "workflow_node.deploy.form.tencentcloud_scf_region.placeholder": "输入腾讯云 SCF 产品地域(例如:ap-guangzhou)", "workflow_node.deploy.form.tencentcloud_scf_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/583/17299", @@ -742,11 +753,11 @@ "workflow_node.deploy.form.tencentcloud_scf_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/scf", "workflow_node.deploy.form.tencentcloud_ssl_endpoint.label": "腾讯云 SSL 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder": "请输入腾讯云 SSL 接口端点(例如:ssl.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659", + "workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659

国际站用户请填写 ssl.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_ssl_deploy.guide": "小贴士:将通过腾讯云 OpenAPI DeployCertificateInstance 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。", "workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.label": "腾讯云 SSL 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.placeholder": "请输入腾讯云 SSL 接口端点(例如:ssl.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659", + "workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659

国际站用户请填写 ssl.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.label": "腾讯云云产品地域", "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.placeholder": "请输入腾讯云云产品地域(例如:ap-guangzhou)", "workflow_node.deploy.form.tencentcloud_ssl_deploy_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659", @@ -762,24 +773,25 @@ "workflow_node.deploy.form.tencentcloud_ssl_update.guide": "小贴士:将通过腾讯云 OpenAPI UpdateCertificateInstanceUploadUpdateCertificateInstance 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。", "workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.label": "腾讯云 SSL 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.placeholder": "请输入腾讯云 SSL 接口端点(例如:ssl.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659", + "workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/41659

国际站用户请填写 ssl.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_ssl_update_certificate_id.label": "腾讯云原证书 ID", "workflow_node.deploy.form.tencentcloud_ssl_update_certificate_id.placeholder": "请输入腾讯云原证书 ID", "workflow_node.deploy.form.tencentcloud_ssl_update_certificate_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/certoverview", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.label": "腾讯云云产品资源类型", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.placeholder": "请输入腾讯云云产品资源类型(多个值请用半角分号隔开)", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91649", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91649https://cloud.tencent.com/document/product/400/119791

注意,这两个接口的所支持的云产品资源类型有所不同,具体请查看腾讯云官方文档。", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.multiple_input_modal.title": "修改腾讯云云产品资源类型", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.multiple_input_modal.placeholder": "请输入腾讯云云产品资源类型", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.label": "腾讯云云产品部署地域(可选)", "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.placeholder": "请输入腾讯云云产品部署地域(多个值请用半角分号隔开)", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91649", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.title": "修改腾讯云云产品资源类型", - "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.placeholder": "请输入腾讯云云产品资源类型", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/400/91649https://cloud.tencent.com/document/product/400/119791", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.title": "修改腾讯云云产品部署地域", + "workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.multiple_input_modal.placeholder": "请输入腾讯云云产品部署地域", "workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.label": "是否更新原证书(即证书 ID 保持不变)", + "workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.tooltip": "不勾选时,将调用腾讯云 OpenAPI UpdateCertificateInstance 接口;否则,将调用腾讯云 OpenAPI UploadUpdateCertificateInstance 接口。", "workflow_node.deploy.form.tencentcloud_vod_endpoint.label": "腾讯云云点播接口端点(可选)", "workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder": "请输入腾讯云云点播接口端点(例如:vod.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/266/31755", + "workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/266/31755

国际站用户请填写 vod.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_vod_sub_app_id.label": "腾讯云云点播应用 ID", "workflow_node.deploy.form.tencentcloud_vod_sub_app_id.placeholder": "请输入腾讯云云点播应用 ID", "workflow_node.deploy.form.tencentcloud_vod_sub_app_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/vod", @@ -788,7 +800,7 @@ "workflow_node.deploy.form.tencentcloud_vod_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/vod", "workflow_node.deploy.form.tencentcloud_waf_endpoint.label": "腾讯云 WAF 接口端点(可选)", "workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder": "请输入腾讯云 WAF 接口端点(例如:waf.tencentcloudapi.com)", - "workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/627/53611", + "workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/627/53611

国际站用户请填写 waf.intl.tencentcloudapi.com。", "workflow_node.deploy.form.tencentcloud_waf_region.label": "腾讯云 WAF 产品地域", "workflow_node.deploy.form.tencentcloud_waf_region.placeholder": "请输入腾讯云 WAF 产品地域(例如:ap-guangzhou)", "workflow_node.deploy.form.tencentcloud_waf_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/627/47525", @@ -828,8 +840,8 @@ "workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/cdn/", "workflow_node.deploy.form.upyun_file.guide": "小贴士:由于又拍云未公开相关 API,这里将使用网页模拟登录方式部署,但无法保证稳定性。如遇又拍云接口变更,请到 GitHub 发起 Issue 告知。", - "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储加速域名", - "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储加速域名", + "workflow_node.deploy.form.upyun_file_domain.label": "又拍云云存储自定义域名", + "workflow_node.deploy.form.upyun_file_domain.placeholder": "请输入又拍云云存储自定义域名", "workflow_node.deploy.form.upyun_file_domain.tooltip": "这是什么?请参阅 https://console.upyun.com/services/file/", "workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书部署方式", "workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书部署方式",