Merge branch 'main' of https://github.com/certimate-go/certimate into certimate-go-main

This commit is contained in:
wood chen 2025-07-12 00:22:15 +08:00
commit 81879cc26b
88 changed files with 2056 additions and 325 deletions

View File

@ -14,7 +14,6 @@ env:
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4

276
.github/workflows/release_sync_gitee.py vendored Normal file
View File

@ -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()

View File

@ -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

7
go.mod
View File

@ -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

26
go.sum
View File

@ -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=

View File

@ -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

View File

@ -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 != "" }),

View File

@ -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"`
}

View File

@ -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)

View File

@ -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"),

View File

@ -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
}

View File

@ -64,7 +64,6 @@ func main() {
app.OnTerminate().BindFunc(func(e *core.TerminateEvent) error {
routes.Unregister()
slog.Info("[CERTIMATE] Exit!")
return e.Next()
})

View File

@ -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 {

View File

@ -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
})
}

View File

@ -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)

View File

@ -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)

View File

@ -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"
)

View File

@ -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
}

View File

@ -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()

View File

@ -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),
}

View File

@ -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()

View File

@ -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()

View File

@ -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

View File

@ -0,0 +1,8 @@
package kong
type ResourceType string
const (
// 资源类型:替换指定证书。
RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
)

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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()

View File

@ -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
}

View File

@ -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)
})
}

View File

@ -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()

View File

@ -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)
}

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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),

View File

@ -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))
})
}

View File

@ -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
}
}

View File

@ -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, "")

View File

@ -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()

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}
}
}

View File

@ -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
}

View File

@ -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))
})
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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"`
}

View File

@ -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"`

View File

@ -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

44
pkg/sdk3rd/qiniu/kodo.go Normal file
View File

@ -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
}

View File

@ -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
}

14
pkg/sdk3rd/qiniu/util.go Normal file
View File

@ -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
}

View File

@ -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())
}

View File

@ -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
}

View File

@ -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]))
}
}

View File

@ -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
}

View File

@ -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()
}

View File

@ -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)
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -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<AccessFormInstance, AccessFormProps>(({ className,
return <AccessFormHuaweiCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.JDCLOUD:
return <AccessFormJDCloudConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.KONG:
return <AccessFormKongConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.KUBERNETES:
return <AccessFormKubernetesConfig {...nestedFormProps} />;
case ACCESS_PROVIDERS.LARKBOT:

View File

@ -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<AccessConfigForKong>;
export type AccessFormKongConfigProps = {
form: FormInstance;
formName: string;
disabled?: boolean;
initialValues?: AccessFormKongConfigFieldValues;
onValuesChange?: (values: AccessFormKongConfigFieldValues) => void;
};
const initFormModel = (): AccessFormKongConfigFieldValues => {
return {
serverUrl: "http://<your-host-addr>: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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="serverUrl" label={t("access.form.kong_server_url.label")} rules={[formRule]}>
<Input placeholder={t("access.form.kong_server_url.placeholder")} />
</Form.Item>
<Form.Item
name="apiToken"
label={t("access.form.kong_api_token.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.kong_api_token.tooltip") }}></span>}
>
<Input.Password allowClear autoComplete="new-password" placeholder={t("access.form.kong_api_token.placeholder")} />
</Form.Item>
<Form.Item name="allowInsecureConnections" label={t("access.form.common_allow_insecure_conns.label")} rules={[formRule]}>
<Switch
checkedChildren={t("access.form.common_allow_insecure_conns.switch.on")}
unCheckedChildren={t("access.form.common_allow_insecure_conns.switch.off")}
/>
</Form.Item>
</Form>
);
};
export default AccessFormKongConfig;

View File

@ -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<DeployNodeConfigFormInstance, DeployNode
return <DeployNodeConfigFormJDCloudLiveConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.JDCLOUD_VOD:
return <DeployNodeConfigFormJDCloudVODConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.KONG:
return <DeployNodeConfigFormKongConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.KUBERNETES_SECRET:
return <DeployNodeConfigFormKubernetesSecretConfig {...nestedFormProps} />;
case DEPLOYMENT_PROVIDERS.LECDN:

View File

@ -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<typeof formSchema>) => {
onValuesChange?.(values);
};
return (
<Form
form={formInst}
disabled={disabled}
initialValues={initialValues ?? initFormModel()}
layout="vertical"
name={formName}
onValuesChange={handleFormChange}
>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.kong_resource_type.label")} rules={[formRule]}>
<Select placeholder={t("workflow_node.deploy.form.kong_resource_type.placeholder")}>
<Select.Option key={RESOURCE_TYPE_CERTIFICATE} value={RESOURCE_TYPE_CERTIFICATE}>
{t("workflow_node.deploy.form.kong_resource_type.option.certificate.label")}
</Select.Option>
</Select>
</Form.Item>
<Form.Item
name="workspace"
label={t("workflow_node.deploy.form.kong_workspace.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.kong_workspace.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.deploy.form.kong_workspace.placeholder")} />
</Form.Item>
<Show when={fieldResourceType === RESOURCE_TYPE_CERTIFICATE}>
<Form.Item
name="certificateId"
label={t("workflow_node.deploy.form.kong_certificate_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.kong_certificate_id.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.kong_certificate_id.placeholder")} />
</Form.Item>
</Show>
</Form>
);
};
export default DeployNodeConfigFormKongConfig;

View File

@ -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

View File

@ -58,7 +58,7 @@ const DeployNodeConfigFormTencentCloudCDNConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_cdn_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -92,7 +92,7 @@ const DeployNodeConfigFormTencentCloudCLBConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_clb_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -58,7 +58,7 @@ const DeployNodeConfigFormTencentCloudCSSConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_css_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -58,7 +58,7 @@ const DeployNodeConfigFormTencentCloudECDNConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_ecdn_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -4,11 +4,12 @@ import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod/v4";
import { validDomainName } from "@/utils/validators";
import MultipleSplitValueInput from "@/components/MultipleSplitValueInput";
type DeployNodeConfigFormTencentCloudEOConfigFieldValues = Nullish<{
endpoint?: string;
zoneId: string;
domain: string;
domains: string;
}>;
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={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_eo_endpoint.placeholder")} />
</Form.Item>
<Form.Item
@ -75,12 +83,17 @@ const DeployNodeConfigFormTencentCloudEOConfig = ({
</Form.Item>
<Form.Item
name="domain"
label={t("workflow_node.deploy.form.tencentcloud_eo_domain.label")}
name="domains"
label={t("workflow_node.deploy.form.tencentcloud_eo_domains.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_eo_domain.tooltip") }}></span>}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_eo_domains.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_eo_domain.placeholder")} />
<MultipleSplitValueInput
modalTitle={t("workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.title")}
placeholder={t("workflow_node.deploy.form.tencentcloud_eo_domains.placeholder")}
placeholderInModal={t("workflow_node.deploy.form.tencentcloud_eo_domains.multiple_input_modal.placeholder")}
splitOptions={{ trim: true, removeEmpty: true }}
/>
</Form.Item>
</Form>
);

View File

@ -73,7 +73,7 @@ const DeployNodeConfigFormTencentCloudGAAPConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_gaap_endpoint.placeholder")} />
</Form.Item>
<Form.Item name="resourceType" label={t("workflow_node.deploy.form.tencentcloud_gaap_resource_type.label")} rules={[formRule]}>

View File

@ -60,7 +60,7 @@ const DeployNodeConfigFormTencentCloudSCFConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_scf_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -52,7 +52,7 @@ const DeployNodeConfigFormTencentCloudSSLConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_endpoint.placeholder")} />
</Form.Item>
</Form>
);

View File

@ -75,7 +75,7 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -24,9 +24,7 @@ export type DeployNodeConfigFormTencentCloudSSLUpdateConfigProps = {
const MULTIPLE_INPUT_SEPARATOR = ";";
const initFormModel = (): DeployNodeConfigFormTencentCloudSSLUpdateConfigFieldValues => {
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={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.placeholder")} />
</Form.Item>
<Form.Item
@ -122,7 +123,12 @@ const DeployNodeConfigFormTencentCloudSSLUpdateConfig = ({
/>
</Form.Item>
<Form.Item name="isReplaced" label={t("workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.label")} rules={[formRule]}>
<Form.Item
name="isReplaced"
label={t("workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_ssl_update_is_replaced.tooltip") }}></span>}
>
<Switch />
</Form.Item>
</Form>

View File

@ -64,7 +64,7 @@ const DeployNodeConfigFormTencentCloudVODConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_vod_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -68,7 +68,7 @@ const DeployNodeConfigFormTencentCloudWAFConfig = ({
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder")} />
<Input allowClear placeholder={t("workflow_node.deploy.form.tencentcloud_waf_endpoint.placeholder")} />
</Form.Item>
<Form.Item

View File

@ -45,6 +45,7 @@ export interface AccessModel extends BaseModel {
| AccessConfigForHetzner
| AccessConfigForHuaweiCloud
| AccessConfigForJDCloud
| AccessConfigForKong
| AccessConfigForKubernetes
| AccessConfigForLarkBot
| AccessConfigForLeCDN
@ -293,6 +294,12 @@ export type AccessConfigForJDCloud = {
accessKeySecret: string;
};
export type AccessConfigForKong = {
serverUrl: string;
apiToken: string;
allowInsecureConnections?: boolean;
};
export type AccessConfigForKubernetes = {
kubeConfig?: string;
};

View File

@ -44,6 +44,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
HETZNER: "hetzner",
HUAWEICLOUD: "huaweicloud",
JDCLOUD: "jdcloud",
KONG: "kong",
KUBERNETES: "k8s",
LARKBOT: "larkbot",
LECDN: "lecdn",
@ -146,6 +147,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.CACHEFLY, "provider.cachefly", "/imgs/providers/cachefly.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.EDGIO, "provider.edgio", "/imgs/providers/edgio.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.APISIX, "provider.apisix", "/imgs/providers/apisix.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.KONG, "provider.kong", "/imgs/providers/kong.png", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.PROXMOXVE, "provider.proxmoxve", "/imgs/providers/proxmoxve.svg", [ACCESS_USAGES.HOSTING]],
[ACCESS_PROVIDERS.CLOUDFLARE, "provider.cloudflare", "/imgs/providers/cloudflare.svg", [ACCESS_USAGES.DNS]],
@ -436,6 +438,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({
JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`,
JDCLOUD_LIVE: `${ACCESS_PROVIDERS.JDCLOUD}-live`,
JDCLOUD_VOD: `${ACCESS_PROVIDERS.JDCLOUD}-vod`,
KONG: `${ACCESS_PROVIDERS.KONG}`,
KUBERNETES_SECRET: `${ACCESS_PROVIDERS.KUBERNETES}-secret`,
LECDN: `${ACCESS_PROVIDERS.LECDN}`,
LOCAL: `${ACCESS_PROVIDERS.LOCAL}`,
@ -613,6 +616,7 @@ export const deploymentProvidersMap: Map<DeploymentProvider["type"] | string, De
[DEPLOYMENT_PROVIDERS.BAOTAWAF_CONSOLE, "provider.baotawaf.console", DEPLOYMENT_CATEGORIES.OTHER],
[DEPLOYMENT_PROVIDERS.SAFELINE, "provider.safeline", DEPLOYMENT_CATEGORIES.FIREWALL],
[DEPLOYMENT_PROVIDERS.APISIX, "provider.apisix", DEPLOYMENT_CATEGORIES.APIGATEWAY],
[DEPLOYMENT_PROVIDERS.KONG, "provider.kong", DEPLOYMENT_CATEGORIES.APIGATEWAY],
[DEPLOYMENT_PROVIDERS.PROXMOXVE, "provider.proxmoxve", DEPLOYMENT_CATEGORIES.NAS],
].map(([type, name, category, builtin]) => [
type,

View File

@ -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 <a href=\"https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>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 <a href=\"https://developer.konghq.com/admin-api/\" target=\"_blank\">https://developer.konghq.com/admin-api/</a>",
"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 <a href=\"https://www.feishu.cn/hc/en-US/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/en-US/articles/807992406756</a>",

View File

@ -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",

View File

@ -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 <a href=\"https://oss.console.aliyun.com\" target=\"_blank\">https://oss.console.aliyun.com</a>",
"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 <a href=\"https://oss.console.aliyun.com\" target=\"_blank\">https://oss.console.aliyun.com</a>",
"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 <a href=\"https://kubernetes.io/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/docs/concepts/configuration/secret/</a>",
"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 <a href=\"https://portal.qiniu.com/cdn\" target=\"_blank\">https://portal.qiniu.com/cdn</a>",
"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 <a href=\"https://portal.qiniu.com/kodo\" target=\"_blank\">https://portal.qiniu.com/kodo</a>",
"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 <a href=\"https://console.tencentcloud.com/cos\" target=\"_blank\">https://console.tencentcloud.com/cos</a>",
"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 <a href=\"https://console.tencentcloud.com/cos\" target=\"_blank\">https://console.tencentcloud.com/cos</a>",
"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 <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
"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 <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
"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 <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
"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": "<ul style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>gaap.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>gaap.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>",
@ -770,15 +781,16 @@
"workflow_node.deploy.form.tencentcloud_ssl_update_certificate_id.tooltip": "For more information, see <a href=\"https://console.cloud.tencent.com/certoverview\" target=\"_blank\">https://console.cloud.tencent.com/certoverview</a>",
"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 <a href=\"https://www.tencentcloud.com/document/product/1007/57981\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/57981</a>",
"workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "For more information, see <a href=\"https://www.tencentcloud.com/document/product/1007/57981\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/57981</a> or <a href=\"https://www.tencentcloud.com/document/product/1007/70503\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/70503</a>",
"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 <a href=\"https://www.tencentcloud.com/document/product/1007/57981\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/57981</a>",
"workflow_node.deploy.form.tencentcloud_ssl_update_resource_regions.tooltip": "For more information, see <a href=\"https://www.tencentcloud.com/document/product/1007/57981\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/57981</a> or <a href=\"https://www.tencentcloud.com/document/product/1007/70503\" target=\"_blank\">https://www.tencentcloud.com/document/product/1007/70503</a>",
"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 <em>UpdateCertificateInstance</em>; otherwise, it will invoke <em>UploadUpdateCertificateInstance</em>.",
"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": "<ul style=\"margin-left: 1.25em; list-style: disc;\"><li><strong>vod.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>vod.tencentcloudapi.com</strong> for Tencent Cloud in China</li></ul>",
@ -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 <a href=\"https://console.ucloud-global.com/ufile\" target=\"_blank\">https://console.ucloud-global.com/ufile</a>",
"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 <a href=\"https://console.ucloud-global.com/ufile\" target=\"_blank\">https://console.ucloud-global.com/ufile</a>",
"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 <a href=\"https://console.upyun.com/services/cdn/\" target=\"_blank\">https://console.upyun.com/services/cdn/</a>",
"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 <a href=\"https://console.upyun.com/services/file/\" target=\"_blank\">https://console.upyun.com/services/file/</a>",
"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 <a href=\"https://console.volcengine.com/imagex\" target=\"_blank\">https://console.volcengine.com/imagex</a>",
"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 <a href=\"https://console.volcengine.com/imagex\" target=\"_blank\">https://console.volcengine.com/imagex</a>",
"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 <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
"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 <a href=\"https://console.volcengine.com/tos\" target=\"_blank\">https://console.volcengine.com/tos</a>",
"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)",

View File

@ -286,6 +286,11 @@
"access.form.k8s_kubeconfig.label": "KubeConfig",
"access.form.k8s_kubeconfig.placeholder": "请输入 KubeConfig 文件内容",
"access.form.k8s_kubeconfig.tooltip": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/organize-cluster-access-kubeconfig/</a><br><br>为空时,将使用 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": "这是什么?请参阅 <a href=\"https://developer.konghq.com/admin-api/\" target=\"_blank\">https://developer.konghq.com/admin-api/</a>",
"access.form.larkbot_webhook_url.label": "飞书群机器人 Webhook 地址",
"access.form.larkbot_webhook_url.placeholder": "请输入飞书群机器人 Webhook 地址",
"access.form.larkbot_webhook_url.tooltip": "这是什么?请参阅 <a href=\"https://www.feishu.cn/hc/zh-CN/articles/807992406756\" target=\"_blank\">https://www.feishu.cn/hc/zh-CN/articles/807992406756</a>",

View File

@ -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": "飞书群机器人",

View File

@ -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": "这是什么?请参阅 <a href=\"https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/\" target=\"_blank\">https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/</a>",
"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": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/cdn\" target=\"_blank\">https://portal.qiniu.com/cdn</a>",
"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": "这是什么?请参阅 <a href=\"https://portal.qiniu.com/kodo\" target=\"_blank\">https://portal.qiniu.com/kodo</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/228/30976\" target=\"_blank\">https://cloud.tencent.com/document/product/228/30976</a>",
"workflow_node.deploy.form.tencentcloud_cdn_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/228/30976\" target=\"_blank\">https://cloud.tencent.com/document/product/228/30976</a><br><br>国际站用户请填写 <em>cdn.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/cdn\" target=\"_blank\">https://console.cloud.tencent.com/cdn</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/30669\" target=\"_blank\">https://cloud.tencent.com/document/product/214/30669</a>",
"workflow_node.deploy.form.tencentcloud_clb_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/30669\" target=\"_blank\">https://cloud.tencent.com/document/product/214/30669</a><br><br>国际站用户请填写 <em>clb.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/33415\" target=\"_blank\">https://cloud.tencent.com/document/product/214/33415</a>",
@ -700,28 +709,30 @@
"workflow_node.deploy.form.tencentcloud_cos_domain.tooltip": "这是什么?请参阅 see <a href=\"https://console.cloud.tencent.com/cos\" target=\"_blank\">https://console.cloud.tencent.com/cos</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/267/20458\" target=\"_blank\">https://cloud.tencent.com/document/product/267/20458</a>",
"workflow_node.deploy.form.tencentcloud_css_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/267/20458\" target=\"_blank\">https://cloud.tencent.com/document/product/267/20458</a><br><br>国际站用户请填写 <em>live.intl.tencentcloudapi.com</em>。",
"workflow_node.deploy.form.tencentcloud_css_domain.label": "腾讯云云直播播放域名",
"workflow_node.deploy.form.tencentcloud_css_domain.placeholder": "请输入腾讯云云直播播放域名",
"workflow_node.deploy.form.tencentcloud_css_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/live\" target=\"_blank\">https://console.cloud.tencent.com/live</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/30669\" target=\"_blank\">https://cloud.tencent.com/document/product/214/30669</a>",
"workflow_node.deploy.form.tencentcloud_ecdn_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/214/30669\" target=\"_blank\">https://cloud.tencent.com/document/product/214/30669</a><br><br>国际站用户请填写 <em>cdn.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/cdn\" target=\"_blank\">https://console.cloud.tencent.com/cdn</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/1552/80723\" target=\"_blank\">https://cloud.tencent.com/document/product/1552/80723</a>",
"workflow_node.deploy.form.tencentcloud_eo_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/1552/80723\" target=\"_blank\">https://cloud.tencent.com/document/product/1552/80723</a><br><br>国际站用户请填写 <em>teo.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/608/36934\" target=\"_blank\">https://cloud.tencent.com/document/product/608/36934</a>",
"workflow_node.deploy.form.tencentcloud_gaap_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/608/36934\" target=\"_blank\">https://cloud.tencent.com/document/product/608/36934</a><br><br>国际站用户请填写 <em>gaap.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/gaap\" target=\"_blank\">https://console.cloud.tencent.com/gaap</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/583/17237\" target=\"_blank\">https://cloud.tencent.com/document/product/583/17237</a>",
"workflow_node.deploy.form.tencentcloud_scf_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/583/17237\" target=\"_blank\">https://cloud.tencent.com/document/product/583/17237</a><br><br>国际站用户请填写 <em>scf.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/583/17299\" target=\"_blank\">https://cloud.tencent.com/document/product/583/17299</a>",
@ -742,11 +753,11 @@
"workflow_node.deploy.form.tencentcloud_scf_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/scf\" target=\"_blank\">https://console.cloud.tencent.com/scf</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a>",
"workflow_node.deploy.form.tencentcloud_ssl_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a><br><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。",
"workflow_node.deploy.form.tencentcloud_ssl_deploy.guide": "小贴士:将通过腾讯云 OpenAPI <em>DeployCertificateInstance</em> 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a>",
"workflow_node.deploy.form.tencentcloud_ssl_deploy_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a><br><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a>",
@ -762,24 +773,25 @@
"workflow_node.deploy.form.tencentcloud_ssl_update.guide": "小贴士:将通过腾讯云 OpenAPI <em>UpdateCertificateInstance</em> 或 <em>UploadUpdateCertificateInstance</em> 接口创建异步部署任务。此部署目标若执行成功仅代表已创建部署任务,实际部署结果需要你自行前往腾讯云控制台查询。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a>",
"workflow_node.deploy.form.tencentcloud_ssl_update_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/41659\" target=\"_blank\">https://cloud.tencent.com/document/product/400/41659</a><br><br>国际站用户请填写 <em>ssl.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/certoverview\" target=\"_blank\">https://console.cloud.tencent.com/certoverview</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91649\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91649</a>",
"workflow_node.deploy.form.tencentcloud_ssl_update_resource_types.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91649\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91649</a> 或 <a href=\"https://cloud.tencent.com/document/product/400/119791\" target=\"_blank\">https://cloud.tencent.com/document/product/400/119791</a><br><br>注意,这两个接口的所支持的云产品资源类型有所不同,具体请查看腾讯云官方文档。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91649\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91649</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/400/91649\" target=\"_blank\">https://cloud.tencent.com/document/product/400/91649</a> 或 <a href=\"https://cloud.tencent.com/document/product/400/119791\" target=\"_blank\">https://cloud.tencent.com/document/product/400/119791</a>",
"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 <em>UpdateCertificateInstance</em> 接口;否则,将调用腾讯云 OpenAPI <em>UploadUpdateCertificateInstance</em> 接口。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/266/31755\" target=\"_blank\">https://cloud.tencent.com/document/product/266/31755</a>",
"workflow_node.deploy.form.tencentcloud_vod_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/266/31755\" target=\"_blank\">https://cloud.tencent.com/document/product/266/31755</a><br><br>国际站用户请填写 <em>vod.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/vod\" target=\"_blank\">https://console.cloud.tencent.com/vod</a>",
@ -788,7 +800,7 @@
"workflow_node.deploy.form.tencentcloud_vod_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/vod\" target=\"_blank\">https://console.cloud.tencent.com/vod</a>",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/627/53611\" target=\"_blank\">https://cloud.tencent.com/document/product/627/53611</a>",
"workflow_node.deploy.form.tencentcloud_waf_endpoint.tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/627/53611\" target=\"_blank\">https://cloud.tencent.com/document/product/627/53611</a><br><br>国际站用户请填写 <em>waf.intl.tencentcloudapi.com</em>。",
"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": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/627/47525\" target=\"_blank\">https://cloud.tencent.com/document/product/627/47525</a>",
@ -828,8 +840,8 @@
"workflow_node.deploy.form.upyun_cdn_domain.placeholder": "请输入又拍云 CDN 加速域名(支持泛域名)",
"workflow_node.deploy.form.upyun_cdn_domain.tooltip": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/cdn/\" target=\"_blank\">https://console.upyun.com/services/cdn/</a>",
"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": "这是什么?请参阅 <a href=\"https://console.upyun.com/services/file/\" target=\"_blank\">https://console.upyun.com/services/file/</a>",
"workflow_node.deploy.form.volcengine_alb_resource_type.label": "证书部署方式",
"workflow_node.deploy.form.volcengine_alb_resource_type.placeholder": "请选择证书部署方式",