diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index e14d3e5a..fa701b03 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -58,7 +58,7 @@ jobs:
run: |
mkdir -p dist/linux
for ARCH in amd64 arm64 armv7; do
- if [ "$ARCH" = "armv7" ]; then
+ if [ "$ARCH" == "armv7" ]; then
export GOARM=7
fi
go build -ldflags="-s -w -X github.com/usual2970/certimate.Version=${GITHUB_REF#refs/tags/}" -o dist/linux/certimate_${GITHUB_REF#refs/tags/}_linux_$ARCH
diff --git a/.goreleaser.linux.yml b/.goreleaser.linux.yml
deleted file mode 100644
index edde23c3..00000000
--- a/.goreleaser.linux.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# .goreleaser.linux.yml
-project_name: certimate
-
-dist: .builds/linux
-
-before:
- hooks:
- - go mod tidy
-
-builds:
- - id: build_linux
- main: ./
- binary: certimate
- ldflags:
- - -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
- env:
- - CGO_ENABLED=0
- goos:
- - linux
- goarch:
- - amd64
- - arm64
- - arm
- goarm:
- - 7
-
-release:
- draft: true
- ids:
- - linux
-
-archives:
- - id: archive_linux
- builds: [build_linux]
- format: "zip"
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
- files:
- - CHANGELOG.md
- - LICENSE.md
- - README.md
-
-checksum:
- name_template: "checksums_linux.txt"
-
-snapshot:
- name_template: "{{ incpatch .Version }}-next"
-
-changelog:
- sort: asc
- filters:
- exclude:
- - "^ui:"
\ No newline at end of file
diff --git a/.goreleaser.macos.yml b/.goreleaser.macos.yml
deleted file mode 100644
index b4f97c0e..00000000
--- a/.goreleaser.macos.yml
+++ /dev/null
@@ -1,49 +0,0 @@
-# .goreleaser.macos.yml
-project_name: certimate
-
-dist: .builds/macos
-
-before:
- hooks:
- - go mod tidy
-
-builds:
- - id: build_macos
- main: ./
- binary: certimate
- ldflags:
- - -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
- env:
- - CGO_ENABLED=0
- goos:
- - darwin
- goarch:
- - amd64
- - arm64
-
-release:
- draft: true
- ids:
- - macos
-
-archives:
- - id: archive_macos
- builds: [build_macos]
- format: "zip"
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
- files:
- - CHANGELOG.md
- - LICENSE.md
- - README.md
-
-checksum:
- name_template: "checksums_macos.txt"
-
-snapshot:
- name_template: "{{ incpatch .Version }}-next"
-
-changelog:
- sort: asc
- filters:
- exclude:
- - "^ui:"
\ No newline at end of file
diff --git a/.goreleaser.windows.yml b/.goreleaser.windows.yml
deleted file mode 100644
index 821527d7..00000000
--- a/.goreleaser.windows.yml
+++ /dev/null
@@ -1,52 +0,0 @@
-# .goreleaser.windows.yml
-project_name: certimate
-
-dist: .builds/windows
-
-before:
- hooks:
- - go mod tidy
-
-builds:
- - id: build_windows
- main: ./
- binary: certimate
- ldflags:
- - -s -w -X github.com/usual2970/certimate.Version={{ .Version }}
- env:
- - CGO_ENABLED=0
- goos:
- - windows
- goarch:
- - amd64
- - arm64
- ignore:
- - goos: windows
- goarch: arm
-
-release:
- draft: true
- ids:
- - windows
-
-archives:
- - id: archive_windows
- builds: [build_windows]
- format: "zip"
- name_template: "{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}"
- files:
- - CHANGELOG.md
- - LICENSE.md
- - README.md
-
-checksum:
- name_template: "checksums_windows.txt"
-
-snapshot:
- name_template: "{{ incpatch .Version }}-next"
-
-changelog:
- sort: asc
- filters:
- exclude:
- - "^ui:"
\ No newline at end of file
diff --git a/README.md b/README.md
index 7ca8c5bb..020e023c 100644
--- a/README.md
+++ b/README.md
@@ -39,7 +39,7 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决
- 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法;
- 支持 PEM、PFX、JKS 等多种格式输出证书;
- 支持 30+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers));
-- 支持 90+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers));
+- 支持 100+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-hosting-providers));
- 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道;
- 支持 Let's Encrypt、Buypass、Google Trust Services、SSL.com、ZeroSSL 等多种 ACME 证书颁发机构;
- 更多特性等待探索。
diff --git a/README_EN.md b/README_EN.md
index 67bab154..864ce5e3 100644
--- a/README_EN.md
+++ b/README_EN.md
@@ -39,7 +39,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate
- Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC.
- Supports various certificate formats such as PEM, PFX, JKS.
- Supports more than 30+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers));
-- Supports more than 90+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-hosting-providers));
+- Supports more than 100+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-hosting-providers));
- Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more;
- Supports multiple ACME CAs including Let's Encrypt, Buypass, Google Trust Services,SSL.com, ZeroSSL, and more;
- More features waiting to be discovered.
diff --git a/go.mod b/go.mod
index bca64be5..ee794b86 100644
--- a/go.mod
+++ b/go.mod
@@ -30,6 +30,7 @@ require (
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
github.com/aws/aws-sdk-go-v2/service/acm v1.32.0
github.com/aws/aws-sdk-go-v2/service/cloudfront v1.46.1
+ github.com/aws/aws-sdk-go-v2/service/iam v1.42.0
github.com/baidubce/bce-sdk-go v0.9.228
github.com/blinkbean/dingtalk v1.1.3
github.com/byteplus-sdk/byteplus-sdk-golang v1.0.46
@@ -51,6 +52,7 @@ require (
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1155
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173
+ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap v1.0.1163
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.1169
@@ -85,7 +87,6 @@ require (
github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
- github.com/aws/aws-sdk-go-v2/service/iam v1.42.0 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.50.0 // indirect
github.com/buger/goterm v1.0.4 // indirect
github.com/diskfs/go-diskfs v1.5.0 // indirect
diff --git a/go.sum b/go.sum
index 404e21e4..eb4dc407 100644
--- a/go.sum
+++ b/go.sum
@@ -836,6 +836,7 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1166/go.mod h1
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1128/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1150/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1155/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
+github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1163/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1164/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1166/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1169/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0=
@@ -845,6 +846,8 @@ github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173 h1:W5b
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1173/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.1150 h1:RQQYfZOFYlkxKR2+xp8el3+8xs9DhxBy+ajlHtapqtQ=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/live v1.0.1150/go.mod h1:zpfr6EBWy7ClASTGUgIy01Gn4R79UXf+2QGQeyR124A=
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/scf v1.0.1172 h1:6SUO0hTie3zxnUEMxmhnS1iRIXpAukSZV27Nrx4NwIk=
diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go
index fbf24742..3dbfd79e 100644
--- a/internal/applicant/providers.go
+++ b/internal/applicant/providers.go
@@ -17,6 +17,7 @@ import (
pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns"
pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud"
pConstellix "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/constellix"
+ pCTCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud"
pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec"
pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean"
pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla"
@@ -220,7 +221,7 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
- case domain.ACMEDns01ProviderTypeCMCCCloud:
+ case domain.ACMEDns01ProviderTypeCMCCCloud, domain.ACMEDns01ProviderTypeCMCCCloudDNS:
{
access := domain.AccessConfigForCMCCCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@@ -252,6 +253,22 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi
return applicant, err
}
+ case domain.ACMEDns01ProviderTypeCTCCCloud, domain.ACMEDns01ProviderTypeCTCCCloudSmartDNS:
+ {
+ access := domain.AccessConfigForCTCCCloud{}
+ if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ applicant, err := pCTCCCloud.NewChallengeProvider(&pCTCCCloud.ChallengeProviderConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ DnsPropagationTimeout: options.DnsPropagationTimeout,
+ DnsTTL: options.DnsTTL,
+ })
+ return applicant, err
+ }
+
case domain.ACMEDns01ProviderTypeDeSEC:
{
access := domain.AccessConfigForDeSEC{}
diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go
index 06239710..8479cade 100644
--- a/internal/deployer/providers.go
+++ b/internal/deployer/providers.go
@@ -25,6 +25,7 @@ import (
pAliyunOSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-oss"
pAliyunVOD "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-vod"
pAliyunWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aliyun-waf"
+ pAPISIX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/apisix"
pAWSACM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-acm"
pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront"
pAWSIAM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-iam"
@@ -42,6 +43,12 @@ import (
pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn"
pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly"
pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly"
+ pCTCCCloudAO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao"
+ pCTCCCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn"
+ pCTCCCloudCMS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cms"
+ pCTCCCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-elb"
+ pCTCCCloudICDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn"
+ pCTCCCloudLVDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn"
pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn"
pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications"
pFlexCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn"
@@ -73,6 +80,7 @@ import (
pTencentCloudCSS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-css"
pTencentCloudECDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ecdn"
pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-eo"
+ pTencentCloudGAAP "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-gaap"
pTencentCloudSCF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-scf"
pTencentCloudSSL "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ssl"
pTencentCloudSSLDeploy "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-ssl-deploy"
@@ -332,6 +340,23 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
}
}
+ case domain.DeploymentProviderTypeAPISIX:
+ {
+ access := domain.AccessConfigForAPISIX{}
+ if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ deployer, err := pAPISIX.NewDeployer(&pAPISIX.DeployerConfig{
+ ServerUrl: access.ServerUrl,
+ ApiKey: access.ApiKey,
+ AllowInsecureConnections: access.AllowInsecureConnections,
+ ResourceType: pAPISIX.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
+ CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"),
+ })
+ return deployer, err
+ }
+
case domain.DeploymentProviderTypeAWSACM, domain.DeploymentProviderTypeAWSCloudFront, domain.DeploymentProviderTypeAWSIAM:
{
access := domain.AccessConfigForAWS{}
@@ -602,6 +627,69 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
return deployer, err
}
+ case domain.DeploymentProviderTypeCTCCCloudAO, domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudCMS, domain.DeploymentProviderTypeCTCCCloudELB, domain.DeploymentProviderTypeCTCCCloudICDN, domain.DeploymentProviderTypeCTCCCloudLVDN:
+ {
+ access := domain.AccessConfigForCTCCCloud{}
+ if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
+ return nil, fmt.Errorf("failed to populate provider access config: %w", err)
+ }
+
+ switch options.Provider {
+ case domain.DeploymentProviderTypeCTCCCloudAO:
+ deployer, err := pCTCCCloudAO.NewDeployer(&pCTCCCloudAO.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
+ })
+ return deployer, err
+
+ case domain.DeploymentProviderTypeCTCCCloudCDN:
+ deployer, err := pCTCCCloudCDN.NewDeployer(&pCTCCCloudCDN.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
+ })
+ return deployer, err
+
+ case domain.DeploymentProviderTypeCTCCCloudCMS:
+ deployer, err := pCTCCCloudCMS.NewDeployer(&pCTCCCloudCMS.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ })
+ return deployer, err
+
+ case domain.DeploymentProviderTypeCTCCCloudELB:
+ deployer, err := pCTCCCloudELB.NewDeployer(&pCTCCCloudELB.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ RegionId: maputil.GetString(options.ProviderServiceConfig, "regionId"),
+ ResourceType: pCTCCCloudELB.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
+ LoadbalancerId: maputil.GetString(options.ProviderServiceConfig, "loadbalancerId"),
+ ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"),
+ })
+ return deployer, err
+
+ case domain.DeploymentProviderTypeCTCCCloudICDN:
+ deployer, err := pCTCCCloudICDN.NewDeployer(&pCTCCCloudICDN.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
+ })
+ return deployer, err
+
+ case domain.DeploymentProviderTypeCTCCCloudLVDN:
+ deployer, err := pCTCCCloudLVDN.NewDeployer(&pCTCCCloudLVDN.DeployerConfig{
+ AccessKeyId: access.AccessKeyId,
+ SecretAccessKey: access.SecretAccessKey,
+ Domain: maputil.GetString(options.ProviderServiceConfig, "domain"),
+ })
+ return deployer, err
+
+ default:
+ break
+ }
+ }
+
case domain.DeploymentProviderTypeDogeCloudCDN:
{
access := domain.AccessConfigForDogeCloud{}
@@ -1030,7 +1118,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
return deployer, err
}
- case domain.DeploymentProviderTypeTencentCloudCDN, domain.DeploymentProviderTypeTencentCloudCLB, domain.DeploymentProviderTypeTencentCloudCOS, domain.DeploymentProviderTypeTencentCloudCSS, domain.DeploymentProviderTypeTencentCloudECDN, domain.DeploymentProviderTypeTencentCloudEO, domain.DeploymentProviderTypeTencentCloudSCF, domain.DeploymentProviderTypeTencentCloudSSL, domain.DeploymentProviderTypeTencentCloudSSLDeploy, domain.DeploymentProviderTypeTencentCloudVOD, domain.DeploymentProviderTypeTencentCloudWAF:
+ case domain.DeploymentProviderTypeTencentCloudCDN, domain.DeploymentProviderTypeTencentCloudCLB, domain.DeploymentProviderTypeTencentCloudCOS, domain.DeploymentProviderTypeTencentCloudCSS, domain.DeploymentProviderTypeTencentCloudECDN, domain.DeploymentProviderTypeTencentCloudEO, domain.DeploymentProviderTypeTencentCloudGAAP, domain.DeploymentProviderTypeTencentCloudSCF, domain.DeploymentProviderTypeTencentCloudSSL, domain.DeploymentProviderTypeTencentCloudSSLDeploy, domain.DeploymentProviderTypeTencentCloudVOD, domain.DeploymentProviderTypeTencentCloudWAF:
{
access := domain.AccessConfigForTencentCloud{}
if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil {
@@ -1093,6 +1181,16 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer
})
return deployer, err
+ case domain.DeploymentProviderTypeTencentCloudGAAP:
+ deployer, err := pTencentCloudGAAP.NewDeployer(&pTencentCloudGAAP.DeployerConfig{
+ SecretId: access.SecretId,
+ SecretKey: access.SecretKey,
+ ResourceType: pTencentCloudGAAP.ResourceType(maputil.GetString(options.ProviderServiceConfig, "resourceType")),
+ ProxyId: maputil.GetString(options.ProviderServiceConfig, "proxyId"),
+ ListenerId: maputil.GetString(options.ProviderServiceConfig, "listenerId"),
+ })
+ return deployer, err
+
case domain.DeploymentProviderTypeTencentCloudSCF:
deployer, err := pTencentCloudSCF.NewDeployer(&pTencentCloudSCF.DeployerConfig{
SecretId: access.SecretId,
diff --git a/internal/domain/access.go b/internal/domain/access.go
index c6071aef..29d07513 100644
--- a/internal/domain/access.go
+++ b/internal/domain/access.go
@@ -41,6 +41,12 @@ type AccessConfigForAliyun struct {
ResourceGroupId string `json:"resourceGroupId,omitempty"`
}
+type AccessConfigForAPISIX struct {
+ ServerUrl string `json:"serverUrl"`
+ ApiKey string `json:"apiKey"`
+ AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
+}
+
type AccessConfigForAWS struct {
AccessKeyId string `json:"accessKeyId"`
SecretAccessKey string `json:"secretAccessKey"`
@@ -114,6 +120,11 @@ type AccessConfigForConstellix struct {
SecretKey string `json:"secretKey"`
}
+type AccessConfigForCTCCCloud struct {
+ AccessKeyId string `json:"accessKeyId"`
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
type AccessConfigForDeSEC struct {
Token string `json:"token"`
}
@@ -162,6 +173,7 @@ type AccessConfigForEmail struct {
Username string `json:"username"`
Password string `json:"password"`
DefaultSenderAddress string `json:"defaultSenderAddress,omitempty"`
+ DefaultSenderName string `json:"defaultSenderName,omitempty"`
DefaultReceiverAddress string `json:"defaultReceiverAddress,omitempty"`
}
diff --git a/internal/domain/provider.go b/internal/domain/provider.go
index 560b08da..47ba0e72 100644
--- a/internal/domain/provider.go
+++ b/internal/domain/provider.go
@@ -14,6 +14,7 @@ const (
AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq")
AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留)
AccessProviderTypeAliyun = AccessProviderType("aliyun")
+ AccessProviderTypeAPISIX = AccessProviderType("apisix")
AccessProviderTypeAWS = AccessProviderType("aws")
AccessProviderTypeAzure = AccessProviderType("azure")
AccessProviderTypeBaiduCloud = AccessProviderType("baiducloud")
@@ -29,7 +30,7 @@ const (
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
AccessProviderTypeConstellix = AccessProviderType("constellix")
- AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留)
+ AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud")
AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留)
AccessProviderTypeDeSEC = AccessProviderType("desec")
AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean")
@@ -118,51 +119,54 @@ ACME DNS-01 提供商常量值。
NOTICE: If you add new constant, please keep ASCII order.
*/
const (
- ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
- ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
- ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
- ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
- ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
- ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
- ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
- ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
- ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
- ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
- ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
- ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
- ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
- ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud)
- ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
- ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
- ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
- ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
- ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
- ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
- ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
- ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
- ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
- ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
- ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
- ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
- ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
- ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
- ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
- ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
- ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
- ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
- ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
- ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
- ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
- ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
- ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
- ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
- ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
- ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
- ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
- ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
- ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
- ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
- ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
+ ACMEDns01ProviderTypeACMEHttpReq = ACMEDns01ProviderType(AccessProviderTypeACMEHttpReq)
+ ACMEDns01ProviderTypeAliyun = ACMEDns01ProviderType(AccessProviderTypeAliyun) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAliyunDNS]
+ ACMEDns01ProviderTypeAliyunDNS = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-dns")
+ ACMEDns01ProviderTypeAliyunESA = ACMEDns01ProviderType(AccessProviderTypeAliyun + "-esa")
+ ACMEDns01ProviderTypeAWS = ACMEDns01ProviderType(AccessProviderTypeAWS) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAWSRoute53]
+ ACMEDns01ProviderTypeAWSRoute53 = ACMEDns01ProviderType(AccessProviderTypeAWS + "-route53")
+ ACMEDns01ProviderTypeAzure = ACMEDns01ProviderType(AccessProviderTypeAzure) // 兼容旧值,等同于 [ACMEDns01ProviderTypeAzure]
+ ACMEDns01ProviderTypeAzureDNS = ACMEDns01ProviderType(AccessProviderTypeAzure + "-dns")
+ ACMEDns01ProviderTypeBaiduCloud = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeBaiduCloudDNS]
+ ACMEDns01ProviderTypeBaiduCloudDNS = ACMEDns01ProviderType(AccessProviderTypeBaiduCloud + "-dns")
+ ACMEDns01ProviderTypeBunny = ACMEDns01ProviderType(AccessProviderTypeBunny)
+ ACMEDns01ProviderTypeCloudflare = ACMEDns01ProviderType(AccessProviderTypeCloudflare)
+ ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
+ ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]
+ ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns")
+ ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
+ ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]
+ ACMEDns01ProviderTypeCTCCCloudSmartDNS = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud + "-smartdns")
+ ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC)
+ ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean)
+ ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA)
+ ACMEDns01ProviderTypeDuckDNS = ACMEDns01ProviderType(AccessProviderTypeDuckDNS)
+ ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6)
+ ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore)
+ ACMEDns01ProviderTypeGname = ACMEDns01ProviderType(AccessProviderTypeGname)
+ ACMEDns01ProviderTypeGoDaddy = ACMEDns01ProviderType(AccessProviderTypeGoDaddy)
+ ACMEDns01ProviderTypeHetzner = ACMEDns01ProviderType(AccessProviderTypeHetzner)
+ ACMEDns01ProviderTypeHuaweiCloud = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeHuaweiCloudDNS]
+ ACMEDns01ProviderTypeHuaweiCloudDNS = ACMEDns01ProviderType(AccessProviderTypeHuaweiCloud + "-dns")
+ ACMEDns01ProviderTypeJDCloud = ACMEDns01ProviderType(AccessProviderTypeJDCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeJDCloudDNS]
+ ACMEDns01ProviderTypeJDCloudDNS = ACMEDns01ProviderType(AccessProviderTypeJDCloud + "-dns")
+ ACMEDns01ProviderTypeNamecheap = ACMEDns01ProviderType(AccessProviderTypeNamecheap)
+ ACMEDns01ProviderTypeNameDotCom = ACMEDns01ProviderType(AccessProviderTypeNameDotCom)
+ ACMEDns01ProviderTypeNameSilo = ACMEDns01ProviderType(AccessProviderTypeNameSilo)
+ ACMEDns01ProviderTypeNetcup = ACMEDns01ProviderType(AccessProviderTypeNetcup)
+ ACMEDns01ProviderTypeNetlify = ACMEDns01ProviderType(AccessProviderTypeNetlify)
+ ACMEDns01ProviderTypeNS1 = ACMEDns01ProviderType(AccessProviderTypeNS1)
+ ACMEDns01ProviderTypePorkbun = ACMEDns01ProviderType(AccessProviderTypePorkbun)
+ ACMEDns01ProviderTypePowerDNS = ACMEDns01ProviderType(AccessProviderTypePowerDNS)
+ ACMEDns01ProviderTypeRainYun = ACMEDns01ProviderType(AccessProviderTypeRainYun)
+ ACMEDns01ProviderTypeTencentCloud = ACMEDns01ProviderType(AccessProviderTypeTencentCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeTencentCloudDNS]
+ ACMEDns01ProviderTypeTencentCloudDNS = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-dns")
+ ACMEDns01ProviderTypeTencentCloudEO = ACMEDns01ProviderType(AccessProviderTypeTencentCloud + "-eo")
+ ACMEDns01ProviderTypeUCloudUDNR = ACMEDns01ProviderType(AccessProviderTypeUCloud + "-udnr")
+ ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel)
+ ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS]
+ ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns")
+ ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn)
)
type DeploymentProviderType string
@@ -193,6 +197,7 @@ const (
DeploymentProviderTypeAliyunOSS = DeploymentProviderType(AccessProviderTypeAliyun + "-oss")
DeploymentProviderTypeAliyunVOD = DeploymentProviderType(AccessProviderTypeAliyun + "-vod")
DeploymentProviderTypeAliyunWAF = DeploymentProviderType(AccessProviderTypeAliyun + "-waf")
+ DeploymentProviderTypeAPISIX = DeploymentProviderType(AccessProviderTypeAWS + "-apisix")
DeploymentProviderTypeAWSACM = DeploymentProviderType(AccessProviderTypeAWS + "-acm")
DeploymentProviderTypeAWSCloudFront = DeploymentProviderType(AccessProviderTypeAWS + "-cloudfront")
DeploymentProviderTypeAWSIAM = DeploymentProviderType(AccessProviderTypeAWS + "-iam")
@@ -210,6 +215,12 @@ const (
DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn")
DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly)
DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly)
+ DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao")
+ DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn")
+ DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms")
+ DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb")
+ DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn")
+ DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn")
DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn")
DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications")
DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN)
@@ -242,6 +253,7 @@ const (
DeploymentProviderTypeTencentCloudCSS = DeploymentProviderType(AccessProviderTypeTencentCloud + "-css")
DeploymentProviderTypeTencentCloudECDN = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ecdn")
DeploymentProviderTypeTencentCloudEO = DeploymentProviderType(AccessProviderTypeTencentCloud + "-eo")
+ DeploymentProviderTypeTencentCloudGAAP = DeploymentProviderType(AccessProviderTypeTencentCloud + "-gaap")
DeploymentProviderTypeTencentCloudSCF = DeploymentProviderType(AccessProviderTypeTencentCloud + "-scf")
DeploymentProviderTypeTencentCloudSSL = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssl")
DeploymentProviderTypeTencentCloudSSLDeploy = DeploymentProviderType(AccessProviderTypeTencentCloud + "-ssldeploy")
diff --git a/internal/notify/providers.go b/internal/notify/providers.go
index 7dc63465..808d1717 100644
--- a/internal/notify/providers.go
+++ b/internal/notify/providers.go
@@ -71,6 +71,7 @@ func createNotifierProvider(options *notifierProviderOptions) (notifier.Notifier
Username: access.Username,
Password: access.Password,
SenderAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderAddress", access.DefaultSenderAddress),
+ SenderName: maputil.GetOrDefaultString(options.ProviderServiceConfig, "senderName", access.DefaultSenderName),
ReceiverAddress: maputil.GetOrDefaultString(options.ProviderServiceConfig, "receiverAddress", access.DefaultReceiverAddress),
})
}
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go
index ba0721fd..83425f2d 100644
--- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/cmcccloud.go
@@ -1,7 +1,6 @@
package cmcccloud
import (
- "errors"
"time"
"github.com/go-acme/lego/v4/challenge"
@@ -18,7 +17,7 @@ type ChallengeProviderConfig struct {
func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
if config == nil {
- return nil, errors.New("config is nil")
+ panic("config is nil")
}
providerConfig := internal.NewDefaultConfig()
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go
index 6bccb1dc..b4d6b971 100644
--- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go
@@ -18,8 +18,9 @@ import (
const (
envNamespace = "CMCCCLOUD_"
- EnvAccessKey = envNamespace + "ACCESS_KEY"
- EnvSecretKey = envNamespace + "SECRET_KEY"
+ EnvAccessKey = envNamespace + "ACCESS_KEY"
+ EnvSecretKey = envNamespace + "SECRET_KEY"
+
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
@@ -30,13 +31,14 @@ const (
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
- AccessKey string
- SecretKey string
- ReadTimeOut int
- ConnectTimeout int
+ AccessKey string
+ SecretKey string
+
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int32
+ ReadTimeOut int
+ ConnectTimeout int
}
type DNSProvider struct {
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go
new file mode 100644
index 00000000..8b3d494a
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/ctcccloud.go
@@ -0,0 +1,39 @@
+package ctcccloud
+
+import (
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+
+ "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal"
+)
+
+type ChallengeProviderConfig struct {
+ AccessKeyId string `json:"accessKeyId"`
+ SecretAccessKey string `json:"secretAccessKey"`
+ DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"`
+ DnsTTL int32 `json:"dnsTTL,omitempty"`
+}
+
+func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ providerConfig := internal.NewDefaultConfig()
+ providerConfig.AccessKeyId = config.AccessKeyId
+ providerConfig.SecretAccessKey = config.SecretAccessKey
+ if config.DnsTTL != 0 {
+ providerConfig.TTL = int(config.DnsTTL)
+ }
+ if config.DnsPropagationTimeout != 0 {
+ providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
+ }
+
+ provider, err := internal.NewDNSProviderConfig(providerConfig)
+ if err != nil {
+ return nil, err
+ }
+
+ return provider, nil
+}
diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go
new file mode 100644
index 00000000..1dd7f2e0
--- /dev/null
+++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/ctcccloud/internal/lego.go
@@ -0,0 +1,203 @@
+package internal
+
+import (
+ "errors"
+ "fmt"
+ "time"
+
+ "github.com/go-acme/lego/v4/challenge"
+ "github.com/go-acme/lego/v4/challenge/dns01"
+ "github.com/go-acme/lego/v4/platform/config/env"
+
+ ctyundns "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/dns"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+const (
+ envNamespace = "CTYUNSMARTDNS_"
+
+ EnvAccessKeyID = envNamespace + "ACCESS_KEY_ID"
+ EnvSecretAccessKey = envNamespace + "SECRET_ACCESS_KEY"
+
+ EnvTTL = envNamespace + "TTL"
+ EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
+ EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
+ EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
+)
+
+var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
+
+type Config struct {
+ AccessKeyId string
+ SecretAccessKey string
+
+ PropagationTimeout time.Duration
+ PollingInterval time.Duration
+ TTL int
+ HTTPTimeout time.Duration
+}
+
+type DNSProvider struct {
+ client *ctyundns.Client
+ config *Config
+}
+
+func NewDefaultConfig() *Config {
+ return &Config{
+ TTL: env.GetOrDefaultInt(EnvTTL, 600),
+ PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute),
+ HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
+ }
+}
+
+func NewDNSProvider() (*DNSProvider, error) {
+ values, err := env.Get(EnvAccessKeyID, EnvSecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("ctyun: %w", err)
+ }
+
+ config := NewDefaultConfig()
+ config.AccessKeyId = values[EnvAccessKeyID]
+ config.SecretAccessKey = values[EnvSecretAccessKey]
+
+ return NewDNSProviderConfig(config)
+}
+
+func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
+ if config == nil {
+ return nil, errors.New("ctyun: the configuration of the DNS provider is nil")
+ }
+
+ client, err := ctyundns.NewClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, err
+ } else {
+ client.SetTimeout(config.HTTPTimeout)
+ }
+
+ return &DNSProvider{
+ client: client,
+ config: config,
+ }, nil
+}
+
+func (d *DNSProvider) Present(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("ctyun: %w", err)
+ }
+
+ if err := d.addOrUpdateDNSRecord(dns01.UnFqdn(authZone), subDomain, info.Value); err != nil {
+ return fmt.Errorf("ctyun: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
+ info := dns01.GetChallengeInfo(domain, keyAuth)
+
+ authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
+ if err != nil {
+ return fmt.Errorf("ctyun: could not find zone for domain %q: %w", domain, err)
+ }
+
+ subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
+ if err != nil {
+ return fmt.Errorf("ctyun: %w", err)
+ }
+
+ if err := d.removeDNSRecord(dns01.UnFqdn(authZone), subDomain); err != nil {
+ return fmt.Errorf("ctyun: %w", err)
+ }
+
+ return nil
+}
+
+func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
+ return d.config.PropagationTimeout, d.config.PollingInterval
+}
+
+func (d *DNSProvider) findDNSRecordId(zoneName, subDomain string) (int32, error) {
+ // 查询解析记录列表
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11264&data=181&isNormal=1&vid=259
+ request := &ctyundns.QueryRecordListRequest{}
+ request.Domain = typeutil.ToPtr(zoneName)
+ request.Host = typeutil.ToPtr(subDomain)
+ request.Type = typeutil.ToPtr("TXT")
+
+ response, err := d.client.QueryRecordList(request)
+ if err != nil {
+ return 0, err
+ }
+
+ if response.ReturnObj == nil || response.ReturnObj.Records == nil || len(response.ReturnObj.Records) == 0 {
+ return 0, nil
+ }
+
+ return response.ReturnObj.Records[0].RecordId, nil
+}
+
+func (d *DNSProvider) addOrUpdateDNSRecord(zoneName, subDomain, value string) error {
+ recordId, err := d.findDNSRecordId(zoneName, subDomain)
+ if err != nil {
+ return err
+ }
+
+ if recordId == 0 {
+ // 新增解析记录
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11259&data=181&isNormal=1&vid=259
+ request := &ctyundns.AddRecordRequest{
+ Domain: typeutil.ToPtr(zoneName),
+ Host: typeutil.ToPtr(subDomain),
+ Type: typeutil.ToPtr("TXT"),
+ LineCode: typeutil.ToPtr("Default"),
+ Value: typeutil.ToPtr(value),
+ State: typeutil.ToPtr(int32(1)),
+ TTL: typeutil.ToPtr(int32(d.config.TTL)),
+ }
+ _, err := d.client.AddRecord(request)
+ return err
+ } else {
+ // 修改解析记录
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11261&data=181&isNormal=1&vid=259
+ request := &ctyundns.UpdateRecordRequest{
+ RecordId: typeutil.ToPtr(recordId),
+ Domain: typeutil.ToPtr(zoneName),
+ Host: typeutil.ToPtr(subDomain),
+ Type: typeutil.ToPtr("TXT"),
+ LineCode: typeutil.ToPtr("Default"),
+ Value: typeutil.ToPtr(value),
+ State: typeutil.ToPtr(int32(1)),
+ TTL: typeutil.ToPtr(int32(d.config.TTL)),
+ }
+ _, err := d.client.UpdateRecord(request)
+ return err
+ }
+}
+
+func (d *DNSProvider) removeDNSRecord(zoneName, subDomain string) error {
+ recordId, err := d.findDNSRecordId(zoneName, subDomain)
+ if err != nil {
+ return err
+ }
+
+ if recordId == 0 {
+ return nil
+ } else {
+ // 删除解析记录
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=122&api=11262&data=181&isNormal=1&vid=259
+ request := &ctyundns.DeleteRecordRequest{
+ RecordId: typeutil.ToPtr(recordId),
+ }
+ _, err = d.client.DeleteRecord(request)
+ return err
+ }
+}
diff --git a/internal/pkg/core/deployer/providers/apisix/apisix.go b/internal/pkg/core/deployer/providers/apisix/apisix.go
new file mode 100644
index 00000000..922b3597
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/apisix/apisix.go
@@ -0,0 +1,125 @@
+package apisix
+
+import (
+ "context"
+ "crypto/tls"
+ "errors"
+ "fmt"
+ "log/slog"
+ "net/url"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ apisixsdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/apisix"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // APISIX 服务地址。
+ ServerUrl string `json:"serverUrl"`
+ // APISIX Admin API Key。
+ ApiKey string `json:"apiKey"`
+ // 是否允许不安全的连接。
+ AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"`
+ // 部署资源类型。
+ ResourceType ResourceType `json:"resourceType"`
+ // 证书 ID。
+ // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。
+ CertificateId string `json:"certificateId,omitempty"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *apisixsdk.Client
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.ServerUrl, config.ApiKey, config.AllowInsecureConnections)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, 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 &deployer.DeployResult{}, nil
+}
+
+func (d *DeployerProvider) deployToCertificate(ctx context.Context, certPEM string, privkeyPEM string) error {
+ if d.config.CertificateId == "" {
+ return errors.New("config `certificateId` is required")
+ }
+
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return err
+ }
+
+ // 更新 SSL 证书
+ // REF: https://apisix.apache.org/zh/docs/apisix/admin-api/#ssl
+ updateSSLReq := &apisixsdk.UpdateSSLRequest{
+ ID: d.config.CertificateId,
+ Cert: typeutil.ToPtr(certPEM),
+ Key: typeutil.ToPtr(privkeyPEM),
+ SNIs: typeutil.ToPtr(certX509.DNSNames),
+ Type: typeutil.ToPtr("server"),
+ Status: typeutil.ToPtr(int32(1)),
+ }
+ updateSSLResp, err := d.sdkClient.UpdateSSL(updateSSLReq)
+ d.logger.Debug("sdk request 'apisix.UpdateSSL'", slog.Any("request", updateSSLReq), slog.Any("response", updateSSLResp))
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'apisix.UpdateSSL': %w", err)
+ }
+
+ return nil
+}
+
+func createSdkClient(serverUrl, apiKey string, skipTlsVerify bool) (*apisixsdk.Client, error) {
+ if _, err := url.Parse(serverUrl); err != nil {
+ return nil, errors.New("invalid apisix server url")
+ }
+
+ if apiKey == "" {
+ return nil, errors.New("invalid apisix api key")
+ }
+
+ client := apisixsdk.NewClient(serverUrl, apiKey)
+ if skipTlsVerify {
+ client.WithTLSConfig(&tls.Config{InsecureSkipVerify: true})
+ }
+
+ return client, nil
+}
diff --git a/internal/pkg/core/deployer/providers/apisix/apisix_test.go b/internal/pkg/core/deployer/providers/apisix/apisix_test.go
new file mode 100644
index 00000000..d7d7dffd
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/apisix/apisix_test.go
@@ -0,0 +1,77 @@
+package apisix_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/apisix"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fServerUrl string
+ fApiKey string
+ fCertificateId string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_APISIX_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "")
+ flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "")
+ flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./apisix_test.go -args \
+ --CERTIMATE_DEPLOYER_APISIX_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_APISIX_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_APISIX_SERVERURL="http://127.0.0.1:9080" \
+ --CERTIMATE_DEPLOYER_APISIX_APIKEY="your-api-key" \
+ --CERTIMATE_DEPLOYER_APISIX_CERTIFICATEID="your-cerficiate-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("APIKEY: %v", fApiKey),
+ fmt.Sprintf("CERTIFICATEID: %v", fCertificateId),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ ServerUrl: fServerUrl,
+ ApiKey: fApiKey,
+ AllowInsecureConnections: true,
+ ResourceType: provider.RESOURCE_TYPE_CERTIFICATE,
+ CertificateId: fCertificateId,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/apisix/consts.go b/internal/pkg/core/deployer/providers/apisix/consts.go
new file mode 100644
index 00000000..75aa1b60
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/apisix/consts.go
@@ -0,0 +1,8 @@
+package apisix
+
+type ResourceType string
+
+const (
+ // 资源类型:替换指定证书。
+ RESOURCE_TYPE_CERTIFICATE = ResourceType("certificate")
+)
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go
new file mode 100644
index 00000000..027bcd69
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go
@@ -0,0 +1,113 @@
+package ctcccloudao
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-ao"
+ ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 加速域名(支持泛域名)。
+ Domain string `json:"domain"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *ctyunao.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ if d.config.Domain == "" {
+ return nil, errors.New("config `domain` is required")
+ }
+
+ // 上传证书到 AccessOne
+ upres, err := d.sslUploader.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))
+ }
+
+ // 域名基础及加速配置查询
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13412&data=174&isNormal=1&vid=167
+ getDomainConfigReq := &ctyunao.GetDomainConfigRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ }
+ getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq)
+ d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainConfig': %w", err)
+ }
+
+ // 域名基础及加速配置修改
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167
+ modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ ProductCode: typeutil.ToPtr(getDomainConfigResp.ReturnObj.ProductCode),
+ Origin: getDomainConfigResp.ReturnObj.Origin,
+ HttpsStatus: typeutil.ToPtr("on"),
+ CertName: typeutil.ToPtr(upres.CertName),
+ }
+ modifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfig(modifyDomainConfigReq)
+ d.logger.Debug("sdk request 'cdn.ModifyDomainConfig'", slog.Any("request", modifyDomainConfigReq), slog.Any("response", modifyDomainConfigResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainConfig': %w", err)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {
+ return ctyunao.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go
new file mode 100644
index 00000000..3cc42cb3
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go
@@ -0,0 +1,75 @@
+package ctcccloudao_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fDomain string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDAO_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_ao_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDAO_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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ Domain: fDomain,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn.go b/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn.go
new file mode 100644
index 00000000..8cc9edaf
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn.go
@@ -0,0 +1,111 @@
+package ctcccloudcdn
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cdn"
+ ctyuncdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cdn"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 加速域名(支持泛域名)。
+ Domain string `json:"domain"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *ctyuncdn.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ if d.config.Domain == "" {
+ return nil, errors.New("config `domain` is required")
+ }
+
+ // 上传证书到 CDN
+ upres, err := d.sslUploader.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))
+ }
+
+ // 查询域名配置信息
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11304&data=161&isNormal=1&vid=154
+ queryDomainDetailReq := &ctyuncdn.QueryDomainDetailRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ }
+ queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
+ d.logger.Debug("sdk request 'cdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryDomainDetail': %w", err)
+ }
+
+ // 修改域名配置
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
+ updateDomainReq := &ctyuncdn.UpdateDomainRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ HttpsStatus: typeutil.ToPtr("on"),
+ CertName: typeutil.ToPtr(upres.CertName),
+ }
+ updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
+ d.logger.Debug("sdk request 'cdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.UpdateDomain': %w", err)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {
+ return ctyuncdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn_test.go b/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn_test.go
new file mode 100644
index 00000000..7a754305
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-cdn/ctcccloud_cdn_test.go
@@ -0,0 +1,75 @@
+package ctcccloudcdn_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fDomain string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_cdn_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ Domain: fDomain,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms.go b/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms.go
new file mode 100644
index 00000000..62b4084b
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms.go
@@ -0,0 +1,67 @@
+package ctcccloudcms
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cms"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ // 上传证书到 CMS
+ upres, err := d.sslUploader.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))
+ }
+
+ return &deployer.DeployResult{}, nil
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms_test.go b/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms_test.go
new file mode 100644
index 00000000..65c3dade
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-cms/ctcccloud_cms_test.go
@@ -0,0 +1,70 @@
+package ctcccloudcms_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cms"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCMS_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_cms_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-elb/consts.go b/internal/pkg/core/deployer/providers/ctcccloud-elb/consts.go
new file mode 100644
index 00000000..263e66ed
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-elb/consts.go
@@ -0,0 +1,10 @@
+package ctcccloudelb
+
+type ResourceType string
+
+const (
+ // 资源类型:部署到指定负载均衡器。
+ RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer")
+ // 资源类型:部署到指定监听器。
+ RESOURCE_TYPE_LISTENER = ResourceType("listener")
+)
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb.go b/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb.go
new file mode 100644
index 00000000..f1dfb3f6
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb.go
@@ -0,0 +1,199 @@
+package ctcccloudelb
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+ "strings"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-elb"
+ ctyunelb "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/elb"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 天翼云资源池 ID。
+ RegionId string `json:"regionId"`
+ // 部署资源类型。
+ ResourceType ResourceType `json:"resourceType"`
+ // 负载均衡实例 ID。
+ // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER] 时必填。
+ LoadbalancerId string `json:"loadbalancerId,omitempty"`
+ // 负载均衡监听器 ID。
+ // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
+ ListenerId string `json:"listenerId,omitempty"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *ctyunelb.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ RegionId: config.RegionId,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ // 上传证书到 ELB
+ upres, err := d.sslUploader.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))
+ }
+
+ // 根据部署资源类型决定部署方式
+ switch d.config.ResourceType {
+ case RESOURCE_TYPE_LOADBALANCER:
+ if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil {
+ return nil, err
+ }
+
+ case RESOURCE_TYPE_LISTENER:
+ if err := d.deployToListener(ctx, upres.CertId); err != nil {
+ return nil, err
+ }
+
+ default:
+ return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error {
+ if d.config.LoadbalancerId == "" {
+ return errors.New("config `loadbalancerId` is required")
+ }
+
+ // 查询监听列表
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5654&data=88&isNormal=1&vid=82
+ listenerIds := make([]string, 0)
+ for {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ }
+
+ listListenerReq := &ctyunelb.ListListenerRequest{
+ RegionID: typeutil.ToPtr(d.config.RegionId),
+ LoadBalancerID: typeutil.ToPtr(d.config.LoadbalancerId),
+ }
+ listListenerResp, err := d.sdkClient.ListListener(listListenerReq)
+ d.logger.Debug("sdk request 'elb.ListListener'", slog.Any("request", listListenerReq), slog.Any("response", listListenerResp))
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.ListListener': %w", err)
+ }
+
+ for _, listener := range listListenerResp.ReturnObj {
+ if strings.EqualFold(listener.Protocol, "HTTPS") {
+ listenerIds = append(listenerIds, listener.ID)
+ }
+ }
+
+ break
+ }
+
+ // 遍历更新监听证书
+ if len(listenerIds) == 0 {
+ d.logger.Info("no elb listeners to deploy")
+ } else {
+ d.logger.Info("found https listeners to deploy", slog.Any("listenerIds", listenerIds))
+ var errs []error
+
+ for _, listenerId := range listenerIds {
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ if err := d.updateListenerCertificate(ctx, listenerId, cloudCertId); err != nil {
+ errs = append(errs, err)
+ }
+ }
+ }
+
+ if len(errs) > 0 {
+ return errors.Join(errs...)
+ }
+ }
+
+ return nil
+}
+
+func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
+ if d.config.ListenerId == "" {
+ return errors.New("config `listenerId` is required")
+ }
+
+ // 更新监听
+ if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudListenerId string, cloudCertId string) error {
+ // 更新监听器
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5652&data=88&isNormal=1&vid=82
+ setLoadBalancerHTTPSListenerAttributeReq := &ctyunelb.UpdateListenerRequest{
+ RegionID: typeutil.ToPtr(d.config.RegionId),
+ ListenerID: typeutil.ToPtr(cloudListenerId),
+ CertificateID: typeutil.ToPtr(cloudCertId),
+ }
+ setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.UpdateListener(setLoadBalancerHTTPSListenerAttributeReq)
+ d.logger.Debug("sdk request 'elb.UpdateListener'", slog.Any("request", setLoadBalancerHTTPSListenerAttributeReq), slog.Any("response", setLoadBalancerHTTPSListenerAttributeResp))
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err)
+ }
+
+ return nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {
+ return ctyunelb.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb_test.go b/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb_test.go
new file mode 100644
index 00000000..86a23a2f
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-elb/ctcccloud_elb_test.go
@@ -0,0 +1,118 @@
+package ctcccloudelb_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-elb"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fRegionId string
+ fLoadbalancerId string
+ fListenerId string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDELB_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "")
+ flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "")
+ flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_elb_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_REGIONID="your-region-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_LOADBALANCERID="your-elb-instance-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDELB_LISTENERID="your-elb-listener-id"
+*/
+func TestDeploy(t *testing.T) {
+ flag.Parse()
+
+ t.Run("Deploy_ToLoadbalancer", 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("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("REGIONID: %v", fRegionId),
+ fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ RegionId: fRegionId,
+ ResourceType: provider.RESOURCE_TYPE_LOADBALANCER,
+ LoadbalancerId: fLoadbalancerId,
+ })
+ 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)
+ })
+
+ t.Run("Deploy_ToListener", 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("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("REGIONID: %v", fRegionId),
+ fmt.Sprintf("LISTENERID: %v", fListenerId),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ RegionId: fRegionId,
+ ResourceType: provider.RESOURCE_TYPE_LISTENER,
+ ListenerId: fListenerId,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go
new file mode 100644
index 00000000..621bd698
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go
@@ -0,0 +1,111 @@
+package ctcccloudicdn
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-icdn"
+ ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 加速域名(支持泛域名)。
+ Domain string `json:"domain"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *ctyunicdn.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ if d.config.Domain == "" {
+ return nil, errors.New("config `domain` is required")
+ }
+
+ // 上传证书到 ICDN
+ upres, err := d.sslUploader.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))
+ }
+
+ // 查询域名配置信息
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10849&data=173&isNormal=1&vid=166
+ queryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ }
+ queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
+ d.logger.Debug("sdk request 'icdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryDomainDetail': %w", err)
+ }
+
+ // 修改域名配置
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166
+ updateDomainReq := &ctyunicdn.UpdateDomainRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ HttpsStatus: typeutil.ToPtr("on"),
+ CertName: typeutil.ToPtr(upres.CertName),
+ }
+ updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
+ d.logger.Debug("sdk request 'icdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'icdn.UpdateDomain': %w", err)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {
+ return ctyunicdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go
new file mode 100644
index 00000000..df514ea6
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go
@@ -0,0 +1,75 @@
+package ctcccloudicdn_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fDomain string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_cdn_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ Domain: fDomain,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go
new file mode 100644
index 00000000..b655c697
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go
@@ -0,0 +1,113 @@
+package ctcccloudlvdn
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-lvdn"
+ ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 加速域名(不支持泛域名)。
+ Domain string `json:"domain"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *ctyunlvdn.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ AccessKeyId: config.AccessKeyId,
+ SecretAccessKey: config.SecretAccessKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ if d.config.Domain == "" {
+ return nil, errors.New("config `domain` is required")
+ }
+
+ // 上传证书到 CDN
+ upres, err := d.sslUploader.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))
+ }
+
+ // 查询域名配置信息
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261
+ queryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ ProductCode: typeutil.ToPtr("005"),
+ }
+ queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq)
+ d.logger.Debug("sdk request 'lvdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryDomainDetail': %w", err)
+ }
+
+ // 修改域名配置
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154
+ updateDomainReq := &ctyunlvdn.UpdateDomainRequest{
+ Domain: typeutil.ToPtr(d.config.Domain),
+ ProductCode: typeutil.ToPtr("005"),
+ HttpsSwitch: typeutil.ToPtr(int32(1)),
+ CertName: typeutil.ToPtr(upres.CertName),
+ }
+ updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq)
+ d.logger.Debug("sdk request 'lvdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'lvdn.UpdateDomain': %w", err)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {
+ return ctyunlvdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go
new file mode 100644
index 00000000..84257a0f
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go
@@ -0,0 +1,75 @@
+package ctcccloudlvdn_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fDomain string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_lvdn_test.go -args \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("DOMAIN: %v", fDomain),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ Domain: fDomain,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/deployer/providers/ssh/ssh.go b/internal/pkg/core/deployer/providers/ssh/ssh.go
index 782b1332..2d6aa8fd 100644
--- a/internal/pkg/core/deployer/providers/ssh/ssh.go
+++ b/internal/pkg/core/deployer/providers/ssh/ssh.go
@@ -8,6 +8,7 @@ import (
"net"
"os"
"path/filepath"
+ "strconv"
"strings"
"github.com/pkg/sftp"
@@ -139,9 +140,9 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
var jumpConn net.Conn
// 第一个连接是主机发起,后续通过跳板机发起
if jumpClient == nil {
- jumpConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
+ jumpConn, err = net.Dial("tcp", net.JoinHostPort(jumpServerConf.SshHost, strconv.Itoa(int(jumpServerConf.SshPort))))
} else {
- jumpConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", jumpServerConf.SshHost, jumpServerConf.SshPort))
+ jumpConn, err = jumpClient.DialContext(ctx, "tcp", net.JoinHostPort(jumpServerConf.SshHost, strconv.Itoa(int(jumpServerConf.SshPort))))
}
if err != nil {
return nil, fmt.Errorf("failed to connect to jump server [%d]: %w", i+1, err)
@@ -168,13 +169,13 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPE
}
// 通过跳板机发起 TCP 连接到目标服务器
- targetConn, err = jumpClient.DialContext(ctx, "tcp", fmt.Sprintf("%s:%d", d.config.SshHost, d.config.SshPort))
+ targetConn, err = jumpClient.DialContext(ctx, "tcp", net.JoinHostPort(d.config.SshHost, strconv.Itoa(int(d.config.SshPort))))
if err != nil {
return nil, fmt.Errorf("failed to connect to target server: %w", err)
}
} else {
// 直接发起 TCP 连接到目标服务器
- targetConn, err = net.Dial("tcp", fmt.Sprintf("%s:%d", d.config.SshHost, d.config.SshPort))
+ targetConn, err = net.Dial("tcp", net.JoinHostPort(d.config.SshHost, strconv.Itoa(int(d.config.SshPort))))
if err != nil {
return nil, fmt.Errorf("failed to connect to target server: %w", err)
}
@@ -340,7 +341,8 @@ func createSshClient(conn net.Conn, host string, port int32, authMethod string,
return nil, fmt.Errorf("unsupported auth method '%s'", authMethod)
}
- sshConn, chans, reqs, err := ssh.NewClientConn(conn, fmt.Sprintf("%s:%d", host, port), &ssh.ClientConfig{
+ addr := net.JoinHostPort(host, strconv.Itoa(int(port)))
+ sshConn, chans, reqs, err := ssh.NewClientConn(conn, addr, &ssh.ClientConfig{
User: username,
Auth: authentications,
HostKeyCallback: ssh.InsecureIgnoreHostKey(),
diff --git a/internal/pkg/core/deployer/providers/tencentcloud-gaap/consts.go b/internal/pkg/core/deployer/providers/tencentcloud-gaap/consts.go
new file mode 100644
index 00000000..37a8a94a
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/tencentcloud-gaap/consts.go
@@ -0,0 +1,8 @@
+package tencentcloudgaap
+
+type ResourceType string
+
+const (
+ // 资源类型:部署到指定监听器。
+ RESOURCE_TYPE_LISTENER = ResourceType("listener")
+)
diff --git a/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap.go b/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap.go
new file mode 100644
index 00000000..2cc076f0
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap.go
@@ -0,0 +1,154 @@
+package tencentcloudgaap
+
+import (
+ "context"
+ "errors"
+ "fmt"
+ "log/slog"
+
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common"
+ "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile"
+ tcgaap "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529"
+
+ "github.com/usual2970/certimate/internal/pkg/core/deployer"
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type DeployerConfig struct {
+ // 腾讯云 SecretId。
+ SecretId string `json:"secretId"`
+ // 腾讯云 SecretKey。
+ SecretKey string `json:"secretKey"`
+ // 部署资源类型。
+ ResourceType ResourceType `json:"resourceType"`
+ // 通道 ID。
+ // 选填。
+ ProxyId string `json:"proxyId,omitempty"`
+ // 负载均衡监听 ID。
+ // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。
+ ListenerId string `json:"listenerId,omitempty"`
+}
+
+type DeployerProvider struct {
+ config *DeployerConfig
+ logger *slog.Logger
+ sdkClient *tcgaap.Client
+ sslUploader uploader.Uploader
+}
+
+var _ deployer.Deployer = (*DeployerProvider)(nil)
+
+func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClients(config.SecretId, config.SecretKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{
+ SecretId: config.SecretId,
+ SecretKey: config.SecretKey,
+ })
+ if err != nil {
+ return nil, fmt.Errorf("failed to create ssl uploader: %w", err)
+ }
+
+ return &DeployerProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ sslUploader: uploader,
+ }, nil
+}
+
+func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer {
+ if logger == nil {
+ d.logger = slog.New(slog.DiscardHandler)
+ } else {
+ d.logger = logger
+ }
+ d.sslUploader.WithLogger(logger)
+ return d
+}
+
+func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) {
+ // 上传证书到 SSL
+ upres, err := d.sslUploader.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))
+ }
+
+ // 根据部署资源类型决定部署方式
+ switch d.config.ResourceType {
+ case RESOURCE_TYPE_LISTENER:
+ if err := d.deployToListener(ctx, upres.CertId); err != nil {
+ return nil, err
+ }
+
+ default:
+ return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType)
+ }
+
+ return &deployer.DeployResult{}, nil
+}
+
+func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error {
+ if d.config.ListenerId == "" {
+ return errors.New("config `listenerId` is required")
+ }
+
+ // 更新监听器证书
+ if err := d.modifyHttpsListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (d *DeployerProvider) modifyHttpsListenerCertificate(ctx context.Context, cloudListenerId, cloudCertId string) error {
+ // 查询 HTTPS 监听器信息
+ // REF: https://cloud.tencent.com/document/product/608/37001
+ describeHTTPSListenersReq := tcgaap.NewDescribeHTTPSListenersRequest()
+ describeHTTPSListenersReq.ListenerId = common.StringPtr(cloudListenerId)
+ describeHTTPSListenersReq.Offset = common.Uint64Ptr(0)
+ describeHTTPSListenersReq.Limit = common.Uint64Ptr(1)
+ describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(describeHTTPSListenersReq)
+ d.logger.Debug("sdk request 'gaap.DescribeHTTPSListeners'", slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp))
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'gaap.DescribeHTTPSListeners': %w", err)
+ } else if len(describeHTTPSListenersResp.Response.ListenerSet) == 0 {
+ return errors.New("listener not found")
+ }
+
+ // 修改 HTTPS 监听器配置
+ // REF: https://cloud.tencent.com/document/product/608/36996
+ modifyHTTPSListenerAttributeReq := tcgaap.NewModifyHTTPSListenerAttributeRequest()
+ modifyHTTPSListenerAttributeReq.ProxyId = typeutil.ToPtrOrZeroNil(d.config.ProxyId)
+ modifyHTTPSListenerAttributeReq.ListenerId = common.StringPtr(cloudListenerId)
+ modifyHTTPSListenerAttributeReq.CertificateId = common.StringPtr(cloudCertId)
+ modifyHTTPSListenerAttributeResp, err := d.sdkClient.ModifyHTTPSListenerAttribute(modifyHTTPSListenerAttributeReq)
+ d.logger.Debug("sdk request 'gaap.ModifyHTTPSListenerAttribute'", slog.Any("request", modifyHTTPSListenerAttributeReq), slog.Any("response", modifyHTTPSListenerAttributeResp))
+ if err != nil {
+ return fmt.Errorf("failed to execute sdk request 'gaap.ModifyHTTPSListenerAttribute': %w", err)
+ }
+
+ return nil
+}
+
+func createSdkClients(secretId, secretKey string) (*tcgaap.Client, error) {
+ credential := common.NewCredential(secretId, secretKey)
+
+ client, err := tcgaap.NewClient(credential, "", profile.NewClientProfile())
+ if err != nil {
+ return nil, err
+ }
+
+ return client, nil
+}
diff --git a/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap_test.go b/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap_test.go
new file mode 100644
index 00000000..32943362
--- /dev/null
+++ b/internal/pkg/core/deployer/providers/tencentcloud-gaap/tencentcloud_gaap_test.go
@@ -0,0 +1,86 @@
+package tencentcloudgaap_test
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-gaap"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fSecretId string
+ fSecretKey string
+ fProxyGroupId string
+ fProxyId string
+ fListenerId string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_DEPLOYER_TENCENTCLOUDCDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fSecretId, argsPrefix+"SECRETID", "", "")
+ flag.StringVar(&fSecretKey, argsPrefix+"SECRETKEY", "", "")
+ flag.StringVar(&fProxyGroupId, argsPrefix+"PROXYGROUPID", "", "")
+ flag.StringVar(&fProxyId, argsPrefix+"PROXYID", "", "")
+ flag.StringVar(&fListenerId, argsPrefix+"LISTENERID", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./tencentcloud_gaap_test.go -args \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_SECRETID="your-secret-id" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_SECRETKEY="your-secret-key" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_PROXYGROUPID="your-gaap-group-id" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_PROXYID="your-gaap-group-id" \
+ --CERTIMATE_DEPLOYER_TENCENTCLOUDGAAP_LISTENERID="your-clb-listener-id"
+*/
+func TestDeploy(t *testing.T) {
+ flag.Parse()
+
+ t.Run("Deploy_ToListener", func(t *testing.T) {
+ t.Log(strings.Join([]string{
+ "args:",
+ fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath),
+ fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath),
+ fmt.Sprintf("SECRETID: %v", fSecretId),
+ fmt.Sprintf("SECRETKEY: %v", fSecretKey),
+ fmt.Sprintf("PROXYGROUPID: %v", fProxyGroupId),
+ fmt.Sprintf("PROXYID: %v", fProxyId),
+ fmt.Sprintf("LISTENERID: %v", fListenerId),
+ }, "\n"))
+
+ deployer, err := provider.NewDeployer(&provider.DeployerConfig{
+ SecretId: fSecretId,
+ SecretKey: fSecretKey,
+ ResourceType: provider.RESOURCE_TYPE_LISTENER,
+ ProxyGroupId: fProxyGroupId,
+ ProxyId: fProxyId,
+ ListenerId: fListenerId,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ t.Logf("ok: %v", res)
+ })
+}
diff --git a/internal/pkg/core/notifier/providers/email/email.go b/internal/pkg/core/notifier/providers/email/email.go
index c8405554..bd7e8880 100644
--- a/internal/pkg/core/notifier/providers/email/email.go
+++ b/internal/pkg/core/notifier/providers/email/email.go
@@ -3,9 +3,10 @@ package email
import (
"context"
"crypto/tls"
- "fmt"
"log/slog"
+ "net"
"net/smtp"
+ "strconv"
"github.com/domodwyer/mailyak/v3"
@@ -26,6 +27,8 @@ type NotifierConfig struct {
Password string `json:"password"`
// 发件人邮箱。
SenderAddress string `json:"senderAddress"`
+ // 发件人显示名称。
+ SenderName string `json:"senderName,omitempty"`
// 收件人邮箱。
ReceiverAddress string `json:"receiverAddress"`
}
@@ -66,12 +69,12 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
var smtpAddr string
if n.config.SmtpPort == 0 {
if n.config.SmtpTls {
- smtpAddr = fmt.Sprintf("%s:465", n.config.SmtpHost)
+ smtpAddr = net.JoinHostPort(n.config.SmtpHost, "465")
} else {
- smtpAddr = fmt.Sprintf("%s:25", n.config.SmtpHost)
+ smtpAddr = net.JoinHostPort(n.config.SmtpHost, "25")
}
} else {
- smtpAddr = fmt.Sprintf("%s:%d", n.config.SmtpHost, n.config.SmtpPort)
+ smtpAddr = net.JoinHostPort(n.config.SmtpHost, strconv.Itoa(int(n.config.SmtpPort)))
}
var yak *mailyak.MailYak
@@ -86,6 +89,7 @@ func (n *NotifierProvider) Notify(ctx context.Context, subject string, message s
}
yak.From(n.config.SenderAddress)
+ yak.FromName(n.config.SenderName)
yak.To(n.config.ReceiverAddress)
yak.Subject(subject)
yak.Plain().Set(message)
diff --git a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go
index 7391129d..77ce16f0 100644
--- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go
+++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go
@@ -61,7 +61,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
// 遍历证书列表,避免重复上传
- if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
+ if res, err := u.findCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
@@ -85,7 +85,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
// 遍历证书列表,获取刚刚上传证书 ID
- if res, err := u.getCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
+ if res, err := u.findCertIfExists(ctx, certPEM, privkeyPEM); err != nil {
return nil, err
} else if res == nil {
return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage())
@@ -94,7 +94,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
}
-func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
searchWebsiteSSLPageNumber := int32(1)
searchWebsiteSSLPageSize := int32(100)
for {
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go
new file mode 100644
index 00000000..fa09e3e5
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go
@@ -0,0 +1,171 @@
+package ctcccloudao
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyunao.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询用户名下证书列表,避免重复上传
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13175&data=174&isNormal=1&vid=167
+ listCertPage := int32(1)
+ listCertPerPage := int32(1000)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ listCertReq := &ctyunao.ListCertRequest{
+ Page: typeutil.ToPtr(listCertPage),
+ PerPage: typeutil.ToPtr(listCertPerPage),
+ UsageMode: typeutil.ToPtr(int32(0)),
+ }
+ listCertResp, err := u.sdkClient.ListCert(listCertReq)
+ u.logger.Debug("sdk request 'ao.ListCert'", slog.Any("request", listCertReq), slog.Any("response", listCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'ao.ListCert': %w", err)
+ }
+
+ if listCertResp.ReturnObj != nil {
+ for _, certRecord := range listCertResp.ReturnObj.Results {
+ // 对比证书通用名称
+ if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
+ continue
+ }
+
+ // 对比证书扩展名称
+ if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
+ continue
+ }
+
+ // 对比证书有效期
+ if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
+ continue
+ } else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
+ continue
+ }
+
+ // 查询证书详情
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13015&data=174&isNormal=1&vid=167
+ queryCertReq := &ctyunao.QueryCertRequest{
+ Id: typeutil.ToPtr(certRecord.Id),
+ }
+ queryCertResp, err := u.sdkClient.QueryCert(queryCertReq)
+ u.logger.Debug("sdk request 'ao.QueryCert'", slog.Any("request", queryCertReq), slog.Any("response", queryCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'ao.QueryCert': %w", err)
+ } else if queryCertResp.ReturnObj != nil && queryCertResp.ReturnObj.Result != nil {
+ var isSameCert bool
+ if queryCertResp.ReturnObj.Result.Certs == certPEM {
+ isSameCert = true
+ } else {
+ oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertResp.ReturnObj.Result.Certs)
+ if err != nil {
+ continue
+ }
+
+ isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
+ }
+
+ // 如果已存在相同证书,直接返回
+ if isSameCert {
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", queryCertResp.ReturnObj.Result.Id),
+ CertName: queryCertResp.ReturnObj.Result.Name,
+ }, nil
+ }
+ }
+ }
+ }
+
+ if listCertResp.ReturnObj == nil || len(listCertResp.ReturnObj.Results) < int(listCertPerPage) {
+ break
+ } else {
+ listCertPage++
+ }
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+
+ // 创建证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13014&data=174&isNormal=1&vid=167
+ createCertReq := &ctyunao.CreateCertRequest{
+ Name: typeutil.ToPtr(certName),
+ Certs: typeutil.ToPtr(certPEM),
+ Key: typeutil.ToPtr(privkeyPEM),
+ }
+ createCertResp, err := u.sdkClient.CreateCert(createCertReq)
+ u.logger.Debug("sdk request 'ao.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'ao.CreateCert': %w", err)
+ }
+
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
+ CertName: certName,
+ }, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) {
+ return ctyunao.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go
new file mode 100644
index 00000000..53d2eee6
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go
@@ -0,0 +1,72 @@
+package ctcccloudao_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-ao"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDAO_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_ao_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn.go b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn.go
new file mode 100644
index 00000000..6281ede8
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn.go
@@ -0,0 +1,171 @@
+package ctcccloudcdn
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyuncdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cdn"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyuncdn.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询证书列表,避免重复上传
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10901&data=161&isNormal=1&vid=154
+ queryCertListPage := int32(1)
+ queryCertListPerPage := int32(1000)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ queryCertListReq := &ctyuncdn.QueryCertListRequest{
+ Page: typeutil.ToPtr(queryCertListPage),
+ PerPage: typeutil.ToPtr(queryCertListPerPage),
+ UsageMode: typeutil.ToPtr(int32(0)),
+ }
+ queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
+ u.logger.Debug("sdk request 'cdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertList': %w", err)
+ }
+
+ if queryCertListResp.ReturnObj != nil {
+ for _, certRecord := range queryCertListResp.ReturnObj.Results {
+ // 对比证书通用名称
+ if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
+ continue
+ }
+
+ // 对比证书扩展名称
+ if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
+ continue
+ }
+
+ // 对比证书有效期
+ if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
+ continue
+ } else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
+ continue
+ }
+
+ // 查询证书详情
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10899&data=161&isNormal=1&vid=154
+ queryCertDetailReq := &ctyuncdn.QueryCertDetailRequest{
+ Id: typeutil.ToPtr(certRecord.Id),
+ }
+ queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
+ u.logger.Debug("sdk request 'cdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.QueryCertDetail': %w", err)
+ } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
+ var isSameCert bool
+ if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
+ isSameCert = true
+ } else {
+ oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
+ if err != nil {
+ continue
+ }
+
+ isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
+ }
+
+ // 如果已存在相同证书,直接返回
+ if isSameCert {
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
+ CertName: queryCertDetailResp.ReturnObj.Result.Name,
+ }, nil
+ }
+ }
+ }
+ }
+
+ if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
+ break
+ } else {
+ queryCertListPage++
+ }
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+
+ // 创建证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=10893&data=161&isNormal=1&vid=154
+ createCertReq := &ctyuncdn.CreateCertRequest{
+ Name: typeutil.ToPtr(certName),
+ Certs: typeutil.ToPtr(certPEM),
+ Key: typeutil.ToPtr(privkeyPEM),
+ }
+ createCertResp, err := u.sdkClient.CreateCert(createCertReq)
+ u.logger.Debug("sdk request 'cdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cdn.CreateCert': %w", err)
+ }
+
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
+ CertName: certName,
+ }, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncdn.Client, error) {
+ return ctyuncdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go
new file mode 100644
index 00000000..72ee6dfa
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go
@@ -0,0 +1,72 @@
+package ctcccloudcdn_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDCDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_cdn_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms.go b/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms.go
new file mode 100644
index 00000000..a72c11c6
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms.go
@@ -0,0 +1,186 @@
+package ctcccloudcms
+
+import (
+ "context"
+ "crypto/sha1"
+ "encoding/hex"
+ "errors"
+ "fmt"
+ "log/slog"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyuncms "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/cms"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyuncms.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 遍历证书列表,避免重复上传
+ if res, _ := u.findCertIfExists(ctx, certPEM); res != nil {
+ return res, nil
+ }
+
+ // 提取服务器证书
+ serverCertPEM, intermediaCertPEM, err := certutil.ExtractCertificatesFromPEM(certPEM)
+ if err != nil {
+ return nil, fmt.Errorf("failed to extract certs: %w", err)
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("cm%d", time.Now().Unix())
+
+ // 上传证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17243&data=204&isNormal=1&vid=283
+ uploadCertificateReq := &ctyuncms.UploadCertificateRequest{
+ Name: typeutil.ToPtr(certName),
+ Certificate: typeutil.ToPtr(serverCertPEM),
+ CertificateChain: typeutil.ToPtr(intermediaCertPEM),
+ PrivateKey: typeutil.ToPtr(privkeyPEM),
+ EncryptionStandard: typeutil.ToPtr("INTERNATIONAL"),
+ }
+ uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq)
+ u.logger.Debug("sdk request 'cms.UploadCertificate'", slog.Any("request", uploadCertificateReq), slog.Any("response", uploadCertificateResp))
+ if err != nil {
+ if uploadCertificateResp != nil && uploadCertificateResp.GetError() == "CCMS_100000067" {
+ if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
+ return nil, err
+ } else if res == nil {
+ return nil, errors.New("ctyun cms: no certificate found")
+ } else {
+ u.logger.Info("ssl certificate already exists")
+ return res, nil
+ }
+ }
+
+ return nil, fmt.Errorf("failed to execute sdk request 'cms.UploadCertificate': %w", err)
+ }
+
+ // 遍历证书列表,获取刚刚上传证书 ID
+ if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
+ return nil, err
+ } else if res == nil {
+ return nil, fmt.Errorf("no ssl certificate found, may be upload failed")
+ } else {
+ return res, nil
+ }
+}
+
+func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询用户证书列表
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=152&api=17233&data=204&isNormal=1&vid=283
+ getCertificateListPageNum := int32(1)
+ getCertificateListPageSize := int32(10)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ getCertificateListReq := &ctyuncms.GetCertificateListRequest{
+ PageNum: typeutil.ToPtr(getCertificateListPageNum),
+ PageSize: typeutil.ToPtr(getCertificateListPageSize),
+ Keyword: typeutil.ToPtr(certX509.Subject.CommonName),
+ Origin: typeutil.ToPtr("UPLOAD"),
+ }
+ getCertificateListResp, err := u.sdkClient.GetCertificateList(getCertificateListReq)
+ u.logger.Debug("sdk request 'cms.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'cms.GetCertificateList': %w", err)
+ }
+
+ if getCertificateListResp.ReturnObj != nil {
+ fingerprint := sha1.Sum(certX509.Raw)
+ fingerprintHex := hex.EncodeToString(fingerprint[:])
+
+ for _, certRecord := range getCertificateListResp.ReturnObj.List {
+ // 对比证书名称
+ if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), certRecord.DomainName) {
+ continue
+ }
+
+ // 对比证书有效期
+ oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certRecord.IssueTime)
+ oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certRecord.ExpireTime)
+ if !certX509.NotBefore.Equal(oldCertNotBefore) {
+ continue
+ } else if !certX509.NotAfter.Equal(oldCertNotAfter) {
+ continue
+ }
+
+ // 对比证书指纹
+ if !strings.EqualFold(fingerprintHex, certRecord.Fingerprint) {
+ continue
+ }
+
+ // 如果以上信息都一致,则视为已存在相同证书,直接返回
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: string(*&certRecord.Id),
+ CertName: certRecord.Name,
+ }, nil
+ }
+ }
+
+ if getCertificateListResp.ReturnObj == nil || len(getCertificateListResp.ReturnObj.List) < int(getCertificateListPageSize) {
+ break
+ } else {
+ getCertificateListPageNum++
+ }
+ }
+
+ return nil, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyuncms.Client, error) {
+ return ctyuncms.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms_test.go b/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms_test.go
new file mode 100644
index 00000000..3fedfe4b
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-cms/ctcccloud_cms_test.go
@@ -0,0 +1,72 @@
+package ctcccloudcms_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-cms"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDCMS_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_cms_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCMS_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCMS_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCMS_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDCMS_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb.go b/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb.go
new file mode 100644
index 00000000..f6fa16c9
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb.go
@@ -0,0 +1,133 @@
+package ctcccloudelb
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "time"
+
+ "github.com/google/uuid"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyunelb "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/elb"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+ // 天翼云资源池 ID。
+ RegionId string `json:"regionId"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyunelb.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询证书列表,避免重复上传
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5692&data=88&isNormal=1&vid=82
+ listCertificateReq := &ctyunelb.ListCertificateRequest{
+ RegionID: typeutil.ToPtr(u.config.RegionId),
+ }
+ listCertificateResp, err := u.sdkClient.ListCertificate(listCertificateReq)
+ u.logger.Debug("sdk request 'elb.ListCertificate'", slog.Any("request", listCertificateReq), slog.Any("response", listCertificateResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificate': %w", err)
+ } else {
+ for _, certRecord := range listCertificateResp.ReturnObj {
+ var isSameCert bool
+ if certRecord.Certificate == certPEM {
+ isSameCert = true
+ } else {
+ oldCertX509, err := certutil.ParseCertificateFromPEM(certRecord.Certificate)
+ if err != nil {
+ continue
+ }
+
+ isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
+ }
+
+ // 如果已存在相同证书,直接返回
+ if isSameCert {
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: certRecord.ID,
+ CertName: certRecord.Name,
+ }, nil
+ }
+ }
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+
+ // 创建证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=24&api=5685&data=88&isNormal=1&vid=82
+ createCertificateReq := &ctyunelb.CreateCertificateRequest{
+ ClientToken: typeutil.ToPtr(generateClientToken()),
+ RegionID: typeutil.ToPtr(u.config.RegionId),
+ Name: typeutil.ToPtr(certName),
+ Description: typeutil.ToPtr("upload from certimate"),
+ Type: typeutil.ToPtr("Server"),
+ Certificate: typeutil.ToPtr(certPEM),
+ PrivateKey: typeutil.ToPtr(privkeyPEM),
+ }
+ createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq)
+ u.logger.Debug("sdk request 'elb.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err)
+ }
+
+ return &uploader.UploadResult{
+ CertId: createCertificateResp.ReturnObj.ID,
+ CertName: certName,
+ }, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunelb.Client, error) {
+ return ctyunelb.NewClient(accessKeyId, secretAccessKey)
+}
+
+func generateClientToken() string {
+ return uuid.New().String()
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb_test.go b/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb_test.go
new file mode 100644
index 00000000..a3c1c752
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-elb/ctcccloud_elb_test.go
@@ -0,0 +1,77 @@
+package ctcccloudelb_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-elb"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+ fRegionId string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDELB_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+ flag.StringVar(&fRegionId, argsPrefix+"REGIONID", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_elb_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDELB_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDELB_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDELB_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDELB_SECRETACCESSKEY="your-secret-access-key" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDELB_REGIONID="your-region-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ fmt.Sprintf("REGIONID: %v", fRegionId),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ RegionId: fRegionId,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go
new file mode 100644
index 00000000..95f497e1
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go
@@ -0,0 +1,171 @@
+package ctcccloudicdn
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyunicdn.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询证书列表,避免重复上传
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10838&data=173&isNormal=1&vid=166
+ queryCertListPage := int32(1)
+ queryCertListPerPage := int32(1000)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ queryCertListReq := &ctyunicdn.QueryCertListRequest{
+ Page: typeutil.ToPtr(queryCertListPage),
+ PerPage: typeutil.ToPtr(queryCertListPerPage),
+ UsageMode: typeutil.ToPtr(int32(0)),
+ }
+ queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
+ u.logger.Debug("sdk request 'icdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertList': %w", err)
+ }
+
+ if queryCertListResp.ReturnObj != nil {
+ for _, certRecord := range queryCertListResp.ReturnObj.Results {
+ // 对比证书通用名称
+ if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
+ continue
+ }
+
+ // 对比证书扩展名称
+ if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
+ continue
+ }
+
+ // 对比证书有效期
+ if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
+ continue
+ } else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
+ continue
+ }
+
+ // 查询证书详情
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10837&data=173&isNormal=1&vid=166
+ queryCertDetailReq := &ctyunicdn.QueryCertDetailRequest{
+ Id: typeutil.ToPtr(certRecord.Id),
+ }
+ queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
+ u.logger.Debug("sdk request 'icdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryCertDetail': %w", err)
+ } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
+ var isSameCert bool
+ if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
+ isSameCert = true
+ } else {
+ oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
+ if err != nil {
+ continue
+ }
+
+ isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
+ }
+
+ // 如果已存在相同证书,直接返回
+ if isSameCert {
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
+ CertName: queryCertDetailResp.ReturnObj.Result.Name,
+ }, nil
+ }
+ }
+ }
+ }
+
+ if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
+ break
+ } else {
+ queryCertListPage++
+ }
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+
+ // 创建证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10835&data=173&isNormal=1&vid=166
+ createCertReq := &ctyunicdn.CreateCertRequest{
+ Name: typeutil.ToPtr(certName),
+ Certs: typeutil.ToPtr(certPEM),
+ Key: typeutil.ToPtr(privkeyPEM),
+ }
+ createCertResp, err := u.sdkClient.CreateCert(createCertReq)
+ u.logger.Debug("sdk request 'icdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'icdn.CreateCert': %w", err)
+ }
+
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
+ CertName: certName,
+ }, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) {
+ return ctyunicdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go
new file mode 100644
index 00000000..6bbf627f
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go
@@ -0,0 +1,72 @@
+package ctcccloudicdn_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-icdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDICDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_icdn_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDICDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDICDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go
new file mode 100644
index 00000000..53453b1c
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go
@@ -0,0 +1,171 @@
+package ctcccloudlvdn
+
+import (
+ "context"
+ "fmt"
+ "log/slog"
+ "slices"
+ "strings"
+ "time"
+
+ "github.com/usual2970/certimate/internal/pkg/core/uploader"
+ ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn"
+ certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
+ typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
+)
+
+type UploaderConfig struct {
+ // 天翼云 AccessKeyId。
+ AccessKeyId string `json:"accessKeyId"`
+ // 天翼云 SecretAccessKey。
+ SecretAccessKey string `json:"secretAccessKey"`
+}
+
+type UploaderProvider struct {
+ config *UploaderConfig
+ logger *slog.Logger
+ sdkClient *ctyunlvdn.Client
+}
+
+var _ uploader.Uploader = (*UploaderProvider)(nil)
+
+func NewUploader(config *UploaderConfig) (*UploaderProvider, error) {
+ if config == nil {
+ panic("config is nil")
+ }
+
+ client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey)
+ if err != nil {
+ return nil, fmt.Errorf("failed to create sdk client: %w", err)
+ }
+
+ return &UploaderProvider{
+ config: config,
+ logger: slog.Default(),
+ sdkClient: client,
+ }, nil
+}
+
+func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
+ if logger == nil {
+ u.logger = slog.New(slog.DiscardHandler)
+ } else {
+ u.logger = logger
+ }
+ return u
+}
+
+func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
+ // 解析证书内容
+ certX509, err := certutil.ParseCertificateFromPEM(certPEM)
+ if err != nil {
+ return nil, err
+ }
+
+ // 查询证书列表,避免重复上传
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11452&data=183&isNormal=1&vid=261
+ queryCertListPage := int32(1)
+ queryCertListPerPage := int32(1000)
+ for {
+ select {
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ default:
+ }
+
+ queryCertListReq := &ctyunlvdn.QueryCertListRequest{
+ Page: typeutil.ToPtr(queryCertListPage),
+ PerPage: typeutil.ToPtr(queryCertListPerPage),
+ UsageMode: typeutil.ToPtr(int32(0)),
+ }
+ queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq)
+ u.logger.Debug("sdk request 'lvdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertList': %w", err)
+ }
+
+ if queryCertListResp.ReturnObj != nil {
+ for _, certRecord := range queryCertListResp.ReturnObj.Results {
+ // 对比证书通用名称
+ if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) {
+ continue
+ }
+
+ // 对比证书扩展名称
+ if !slices.Equal(certX509.DNSNames, certRecord.SANs) {
+ continue
+ }
+
+ // 对比证书有效期
+ if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) {
+ continue
+ } else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) {
+ continue
+ }
+
+ // 查询证书详情
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11449&data=183&isNormal=1&vid=261
+ queryCertDetailReq := &ctyunlvdn.QueryCertDetailRequest{
+ Id: typeutil.ToPtr(certRecord.Id),
+ }
+ queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq)
+ u.logger.Debug("sdk request 'lvdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertDetail': %w", err)
+ } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil {
+ var isSameCert bool
+ if queryCertDetailResp.ReturnObj.Result.Certs == certPEM {
+ isSameCert = true
+ } else {
+ oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs)
+ if err != nil {
+ continue
+ }
+
+ isSameCert = certutil.EqualCertificate(certX509, oldCertX509)
+ }
+
+ // 如果已存在相同证书,直接返回
+ if isSameCert {
+ u.logger.Info("ssl certificate already exists")
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id),
+ CertName: queryCertDetailResp.ReturnObj.Result.Name,
+ }, nil
+ }
+ }
+ }
+ }
+
+ if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) {
+ break
+ } else {
+ queryCertListPage++
+ }
+ }
+
+ // 生成新证书名(需符合天翼云命名规则)
+ certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli())
+
+ // 创建证书
+ // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11436&data=183&isNormal=1&vid=261
+ createCertReq := &ctyunlvdn.CreateCertRequest{
+ Name: typeutil.ToPtr(certName),
+ Certs: typeutil.ToPtr(certPEM),
+ Key: typeutil.ToPtr(privkeyPEM),
+ }
+ createCertResp, err := u.sdkClient.CreateCert(createCertReq)
+ u.logger.Debug("sdk request 'lvdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp))
+ if err != nil {
+ return nil, fmt.Errorf("failed to execute sdk request 'lvdn.CreateCert': %w", err)
+ }
+
+ return &uploader.UploadResult{
+ CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id),
+ CertName: certName,
+ }, nil
+}
+
+func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) {
+ return ctyunlvdn.NewClient(accessKeyId, secretAccessKey)
+}
diff --git a/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go
new file mode 100644
index 00000000..3bcedfdd
--- /dev/null
+++ b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go
@@ -0,0 +1,72 @@
+package ctcccloudlvdn_test
+
+import (
+ "context"
+ "encoding/json"
+ "flag"
+ "fmt"
+ "os"
+ "strings"
+ "testing"
+
+ provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-lvdn"
+)
+
+var (
+ fInputCertPath string
+ fInputKeyPath string
+ fAccessKeyId string
+ fSecretAccessKey string
+)
+
+func init() {
+ argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDLVDN_"
+
+ flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
+ flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
+ flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "")
+ flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "")
+}
+
+/*
+Shell command to run this test:
+
+ go test -v ./ctcccloud_lvdn_test.go -args \
+ --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-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("ACCESSKEYID: %v", fAccessKeyId),
+ fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey),
+ }, "\n"))
+
+ uploader, err := provider.NewUploader(&provider.UploaderConfig{
+ AccessKeyId: fAccessKeyId,
+ SecretAccessKey: fSecretAccessKey,
+ })
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ fInputCertData, _ := os.ReadFile(fInputCertPath)
+ fInputKeyData, _ := os.ReadFile(fInputKeyPath)
+ res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData))
+ if err != nil {
+ t.Errorf("err: %+v", err)
+ return
+ }
+
+ sres, _ := json.Marshal(res)
+ t.Logf("ok: %s", string(sres))
+ })
+}
diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go
index 613fc7a9..cd3771e7 100644
--- a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go
+++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go
@@ -52,7 +52,8 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader {
}
func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) {
- if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
+ // 遍历证书列表,避免重复上传
+ if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res != nil {
u.logger.Info("ssl certificate already exists")
@@ -71,7 +72,8 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
return nil, fmt.Errorf("failed to execute sdk request 'sslcenter.Create': %w", err)
}
- if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
+ // 遍历证书列表,获取刚刚上传证书 ID
+ if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("rainyun sslcenter: no certificate found")
@@ -80,14 +82,14 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}
}
-func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
+func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
- // 遍历 SSL 证书列表,避免重复上传
+ // 遍历 SSL 证书列表
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046
// REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048
sslCenterListPage := int32(1)
diff --git a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go
index acfbb214..2ceab189 100644
--- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go
+++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go
@@ -88,7 +88,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp))
if err != nil {
if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 {
- if res, err := u.getCertIfExists(ctx, certPEM); err != nil {
+ if res, err := u.findCertIfExists(ctx, certPEM); err != nil {
return nil, err
} else if res == nil {
return nil, errors.New("ucloud ssl: no certificate found")
@@ -111,14 +111,14 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPE
}, nil
}
-func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
+func (u *UploaderProvider) findCertIfExists(ctx context.Context, certPEM string) (*uploader.UploadResult, error) {
// 解析证书内容
certX509, err := certutil.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, err
}
- // 遍历获取用户证书列表,避免重复上传
+ // 遍历获取用户证书列表
// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list
// REF: https://docs.ucloud.cn/api/usslcertificate-api/download_certificate
getCertificateListPage := int(1)
diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go
index 6a12ceda..2a20f4e8 100644
--- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go
+++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go
@@ -1,4 +1,4 @@
-package jdcloudssl
+package wangsucertificate
import (
"context"
@@ -9,9 +9,8 @@ import (
"strings"
"time"
- wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate"
-
"github.com/usual2970/certimate/internal/pkg/core/uploader"
+ wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate"
certutil "github.com/usual2970/certimate/internal/pkg/utils/cert"
typeutil "github.com/usual2970/certimate/internal/pkg/utils/type"
)
diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go
index bdec8cfe..7bbf63c6 100644
--- a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go
+++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go
@@ -1,4 +1,4 @@
-package jdcloudssl_test
+package wangsucertificate_test
import (
"context"
@@ -20,7 +20,7 @@ var (
)
func init() {
- argsPrefix := "CERTIMATE_UPLOADER_JDCLOUDSSL_"
+ argsPrefix := "CERTIMATE_UPLOADER_WANGSUCERTIFICATE_"
flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "")
flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "")
@@ -34,8 +34,8 @@ Shell command to run this test:
go test -v ./wangsu_certificate_test.go -args \
--CERTIMATE_UPLOADER_WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \
--CERTIMATE_UPLOADER_WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \
- --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \
- --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret"
+ --CERTIMATE_UPLOADER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \
+ --CERTIMATE_UPLOADER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret"
*/
func TestDeploy(t *testing.T) {
flag.Parse()
diff --git a/internal/pkg/sdk3rd/apisix/api.go b/internal/pkg/sdk3rd/apisix/api.go
new file mode 100644
index 00000000..7ebfba04
--- /dev/null
+++ b/internal/pkg/sdk3rd/apisix/api.go
@@ -0,0 +1,16 @@
+package apisix
+
+import (
+ "fmt"
+ "net/http"
+)
+
+func (c *Client) UpdateSSL(req *UpdateSSLRequest) (*UpdateSSLResponse, error) {
+ if req.ID == "" {
+ return nil, fmt.Errorf("1panel api error: invalid parameter: ID")
+ }
+
+ resp := &UpdateSSLResponse{}
+ err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/ssls/%s", req.ID), req, resp)
+ return resp, err
+}
diff --git a/internal/pkg/sdk3rd/apisix/client.go b/internal/pkg/sdk3rd/apisix/client.go
new file mode 100644
index 00000000..66784824
--- /dev/null
+++ b/internal/pkg/sdk3rd/apisix/client.go
@@ -0,0 +1,87 @@
+package apisix
+
+import (
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "strings"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+)
+
+type Client struct {
+ client *resty.Client
+}
+
+func NewClient(serverUrl, apiKey string) *Client {
+ client := resty.New().
+ SetBaseURL(strings.TrimRight(serverUrl, "/")+"/apisix/admin").
+ SetHeader("User-Agent", "certimate").
+ SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
+ req.Header.Set("X-API-KEY", apiKey)
+
+ return nil
+ })
+
+ return &Client{
+ client: client,
+ }
+}
+
+func (c *Client) WithTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) WithTLSConfig(config *tls.Config) *Client {
+ c.client.SetTLSClientConfig(config)
+ return c
+}
+
+func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) {
+ req := c.client.R()
+ if strings.EqualFold(method, http.MethodGet) {
+ qs := make(map[string]string)
+ if params != nil {
+ temp := make(map[string]any)
+ jsonb, _ := json.Marshal(params)
+ json.Unmarshal(jsonb, &temp)
+ for k, v := range temp {
+ if v != nil {
+ qs[k] = fmt.Sprintf("%v", v)
+ }
+ }
+ }
+
+ req = req.SetQueryParams(qs)
+ } else {
+ req = req.SetHeader("Content-Type", "application/json").SetBody(params)
+ }
+
+ resp, err := req.Execute(method, path)
+ if err != nil {
+ return resp, fmt.Errorf("apisix api error: failed to send request: %w", err)
+ } else if resp.IsError() {
+ return resp, fmt.Errorf("apisix api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
+ }
+
+ return resp, nil
+}
+
+func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result interface{}) error {
+ resp, err := c.sendRequest(method, path, params)
+ if err != nil {
+ if resp != nil {
+ json.Unmarshal(resp.Body(), &result)
+ }
+ return err
+ }
+
+ if err := json.Unmarshal(resp.Body(), &result); err != nil {
+ return fmt.Errorf("apisix api error: failed to unmarshal response: %w", err)
+ }
+
+ return nil
+}
diff --git a/internal/pkg/sdk3rd/apisix/models.go b/internal/pkg/sdk3rd/apisix/models.go
new file mode 100644
index 00000000..960c8489
--- /dev/null
+++ b/internal/pkg/sdk3rd/apisix/models.go
@@ -0,0 +1,12 @@
+package apisix
+
+type UpdateSSLRequest struct {
+ ID string `json:"-"`
+ Cert *string `json:"cert,omitempty"`
+ Key *string `json:"key,omitempty"`
+ SNIs *[]string `json:"snis,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Status *int32 `json:"status,omitempty"`
+}
+
+type UpdateSSLResponse struct{}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go
new file mode 100644
index 00000000..1a9ac21e
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go
@@ -0,0 +1,41 @@
+package ao
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateCertRequest struct {
+ Name *string `json:"name,omitempty"`
+ Certs *string `json:"certs,omitempty"`
+ Key *string `json:"key,omitempty"`
+}
+
+type CreateCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Id int64 `json:"id"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
+ return c.CreateCertWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/cert/create")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go b/internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go
new file mode 100644
index 00000000..6085adf6
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go
@@ -0,0 +1,54 @@
+package ao
+
+import (
+ "context"
+ "net/http"
+)
+
+type GetDomainConfigRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+}
+
+type GetDomainConfigResponse struct {
+ baseResult
+
+ 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"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {
+ return c.GetDomainConfigWithContext(context.Background(), req)
+}
+
+func (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/domain/config")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Domain != nil {
+ httpreq.SetQueryParam("domain", *req.Domain)
+ }
+ if req.ProductCode != nil {
+ httpreq.SetQueryParam("product_code", *req.ProductCode)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &GetDomainConfigResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go
new file mode 100644
index 00000000..b34c199a
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go
@@ -0,0 +1,55 @@
+package ao
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type ListCertRequest struct {
+ Page *int32 `json:"page,omitempty"`
+ PerPage *int32 `json:"per_page,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type ListCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Results []*CertRecord `json:"result,omitempty"`
+ Page int32 `json:"page,omitempty"`
+ PerPage int32 `json:"per_page,omitempty"`
+ TotalPage int32 `json:"total_page,omitempty"`
+ TotalRecords int32 `json:"total_records,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) ListCert(req *ListCertRequest) (*ListCertResponse, error) {
+ return c.ListCertWithContext(context.Background(), req)
+}
+
+func (c *Client) ListCertWithContext(ctx context.Context, req *ListCertRequest) (*ListCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/list")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Page != nil {
+ httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
+ }
+ if req.PerPage != nil {
+ httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &ListCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go b/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go
new file mode 100644
index 00000000..f28b3dc9
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go
@@ -0,0 +1,40 @@
+package ao
+
+import (
+ "context"
+ "net/http"
+)
+
+type ModifyDomainConfigRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+ Origin []*DomainOriginConfig `json:"origin,omitempty"`
+ HttpsStatus *string `json:"https_status,omitempty"`
+ HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"`
+ CertName *string `json:"cert_name,omitempty"`
+}
+
+type ModifyDomainConfigResponse struct {
+ baseResult
+}
+
+func (c *Client) ModifyDomainConfig(req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {
+ return c.ModifyDomainConfigWithContext(context.Background(), req)
+}
+
+func (c *Client) ModifyDomainConfigWithContext(ctx context.Context, req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/domain/modify_config")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &ModifyDomainConfigResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go
new file mode 100644
index 00000000..9ec2d740
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go
@@ -0,0 +1,51 @@
+package ao
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertRequest struct {
+ Id *int64 `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Result *CertDetail `json:"result,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCert(req *QueryCertRequest) (*QueryCertResponse, error) {
+ return c.QueryCertWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertWithContext(ctx context.Context, req *QueryCertRequest) (*QueryCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/query")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Id != nil {
+ httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/client.go b/internal/pkg/sdk3rd/ctyun/ao/client.go
new file mode 100644
index 00000000..dbf20321
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/client.go
@@ -0,0 +1,49 @@
+package ao
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://accessone-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ if statusCode != "" && statusCode != "100000" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/ao/types.go b/internal/pkg/sdk3rd/ctyun/ao/types.go
new file mode 100644
index 00000000..ba5582b3
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/ao/types.go
@@ -0,0 +1,101 @@
+package ao
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertRecord struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ CN string `json:"cn"`
+ SANs []string `json:"sans"`
+ UsageMode int32 `json:"usage_mode"`
+ State int32 `json:"state"`
+ ExpiresTime int64 `json:"expires"`
+ IssueTime int64 `json:"issue"`
+ Issuer string `json:"issuer"`
+ CreatedTime int64 `json:"created"`
+}
+
+type CertDetail struct {
+ CertRecord
+ Certs string `json:"certs"`
+ Key string `json:"key"`
+}
+
+type DomainOriginConfig struct {
+ Origin string `json:"origin"`
+ Role string `json:"role"`
+ Weight int32 `json:"weight"`
+}
+
+type DomainHttpsBasicConfig struct {
+ HttpsForce string `json:"https_force"`
+ ForceStatus string `json:"force_status"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/cdn/api_create_cert.go
new file mode 100644
index 00000000..9445035b
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/api_create_cert.go
@@ -0,0 +1,41 @@
+package cdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateCertRequest struct {
+ Name *string `json:"name,omitempty"`
+ Certs *string `json:"certs,omitempty"`
+ Key *string `json:"key,omitempty"`
+}
+
+type CreateCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Id int64 `json:"id"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
+ return c.CreateCertWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/cert/creat-cert")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_detail.go b/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_detail.go
new file mode 100644
index 00000000..12501f29
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_detail.go
@@ -0,0 +1,51 @@
+package cdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertDetailRequest struct {
+ Id *int64 `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Result *CertDetail `json:"result,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ return c.QueryCertDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Id != nil {
+ httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_list.go b/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_list.go
new file mode 100644
index 00000000..0f134594
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/api_query_cert_list.go
@@ -0,0 +1,55 @@
+package cdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertListRequest struct {
+ Page *int32 `json:"page,omitempty"`
+ PerPage *int32 `json:"per_page,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertListResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Results []*CertRecord `json:"result,omitempty"`
+ Page int32 `json:"page,omitempty"`
+ PerPage int32 `json:"per_page,omitempty"`
+ TotalPage int32 `json:"total_page,omitempty"`
+ TotalRecords int32 `json:"total_records,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ return c.QueryCertListWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-list")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Page != nil {
+ httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
+ }
+ if req.PerPage != nil {
+ httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertListResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/api_query_domain_detail.go b/internal/pkg/sdk3rd/ctyun/cdn/api_query_domain_detail.go
new file mode 100644
index 00000000..9a29b4f2
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/api_query_domain_detail.go
@@ -0,0 +1,64 @@
+package cdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type QueryDomainDetailRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+ FunctionNames *string `json:"function_names,omitempty"`
+}
+
+type QueryDomainDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Domain string `json:"domain"`
+ ProductCode string `json:"product_code"`
+ Status int32 `json:"status"`
+ AreaScope int32 `json:"area_scope"`
+ Cname string `json:"cname"`
+ HttpsStatus string `json:"https_status"`
+ HttpsBasic *struct {
+ HttpsForce string `json:"https_force"`
+ HttpForce string `json:"http_force"`
+ ForceStatus string `json:"force_status"`
+ OriginProtocol string `json:"origin_protocol"`
+ } `json:"https_basic,omitempty"`
+ CertName string `json:"cert_name"`
+ Ssl string `json:"ssl"`
+ SslStapling string `json:"ssl_stapling"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ return c.QueryDomainDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Domain != nil {
+ httpreq.SetQueryParam("domain", *req.Domain)
+ }
+ if req.ProductCode != nil {
+ httpreq.SetQueryParam("product_code", *req.ProductCode)
+ }
+ if req.FunctionNames != nil {
+ httpreq.SetQueryParam("function_names", *req.FunctionNames)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryDomainDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/api_update_domain.go b/internal/pkg/sdk3rd/ctyun/cdn/api_update_domain.go
new file mode 100644
index 00000000..65dd918c
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/api_update_domain.go
@@ -0,0 +1,37 @@
+package cdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type UpdateDomainRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ HttpsStatus *string `json:"https_status,omitempty"`
+ CertName *string `json:"cert_name,omitempty"`
+}
+
+type UpdateDomainResponse struct {
+ baseResult
+}
+
+func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ return c.UpdateDomainWithContext(context.Background(), req)
+}
+
+func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/domain/update-domain")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UpdateDomainResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/client.go b/internal/pkg/sdk3rd/ctyun/cdn/client.go
new file mode 100644
index 00000000..77402a4e
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/client.go
@@ -0,0 +1,49 @@
+package cdn
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://ctcdn-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ if statusCode != "" && statusCode != "100000" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cdn/types.go b/internal/pkg/sdk3rd/ctyun/cdn/types.go
new file mode 100644
index 00000000..96c97ce1
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cdn/types.go
@@ -0,0 +1,90 @@
+package cdn
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertRecord struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ CN string `json:"cn"`
+ SANs []string `json:"sans"`
+ UsageMode int32 `json:"usage_mode"`
+ State int32 `json:"state"`
+ ExpiresTime int64 `json:"expires"`
+ IssueTime int64 `json:"issue"`
+ Issuer string `json:"issuer"`
+ CreatedTime int64 `json:"created"`
+}
+
+type CertDetail struct {
+ CertRecord
+ Certs string `json:"certs"`
+ Key string `json:"key"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cms/api_get_certificate_list.go b/internal/pkg/sdk3rd/ctyun/cms/api_get_certificate_list.go
new file mode 100644
index 00000000..f410921f
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cms/api_get_certificate_list.go
@@ -0,0 +1,44 @@
+package cms
+
+import (
+ "context"
+ "net/http"
+)
+
+type GetCertificateListRequest struct {
+ Status *string `json:"status,omitempty"`
+ Keyword *string `json:"keyword,omitempty"`
+ PageNum *int32 `json:"pageNum,omitempty"`
+ PageSize *int32 `json:"pageSize,omitempty"`
+ Origin *string `json:"origin,omitempty"`
+}
+
+type GetCertificateListResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ List []*CertificateRecord `json:"list,omitempty"`
+ TotalSize int32 `json:"totalSize,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) GetCertificateList(req *GetCertificateListRequest) (*GetCertificateListResponse, error) {
+ return c.GetCertificateListWithContext(context.Background(), req)
+}
+
+func (c *Client) GetCertificateListWithContext(ctx context.Context, req *GetCertificateListRequest) (*GetCertificateListResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/certificate/list")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &GetCertificateListResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cms/api_upload_certificate.go b/internal/pkg/sdk3rd/ctyun/cms/api_upload_certificate.go
new file mode 100644
index 00000000..8044aa40
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cms/api_upload_certificate.go
@@ -0,0 +1,41 @@
+package cms
+
+import (
+ "context"
+ "net/http"
+)
+
+type UploadCertificateRequest struct {
+ Name *string `json:"name,omitempty"`
+ Certificate *string `json:"certificate,omitempty"`
+ CertificateChain *string `json:"certificateChain,omitempty"`
+ PrivateKey *string `json:"privateKey,omitempty"`
+ EncryptionStandard *string `json:"encryptionStandard,omitempty"`
+ EncCertificate *string `json:"encCertificate,omitempty"`
+ EncPrivateKey *string `json:"encPrivateKey,omitempty"`
+}
+
+type UploadCertificateResponse struct {
+ baseResult
+}
+
+func (c *Client) UploadCertificate(req *UploadCertificateRequest) (*UploadCertificateResponse, error) {
+ return c.UploadCertificateWithContext(context.Background(), req)
+}
+
+func (c *Client) UploadCertificateWithContext(ctx context.Context, req *UploadCertificateRequest) (*UploadCertificateResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/certificate/upload")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UploadCertificateResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cms/client.go b/internal/pkg/sdk3rd/ctyun/cms/client.go
new file mode 100644
index 00000000..ac94d1b6
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cms/client.go
@@ -0,0 +1,50 @@
+package cms
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://ccms-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ errorCode := result.GetError()
+ if (statusCode != "" && statusCode != "200") || errorCode != "" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/cms/types.go b/internal/pkg/sdk3rd/ctyun/cms/types.go
new file mode 100644
index 00000000..b5b0b4d7
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/cms/types.go
@@ -0,0 +1,94 @@
+package cms
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertificateRecord struct {
+ Id string `json:"id"`
+ Origin string `json:"origin"`
+ Type string `json:"type"`
+ ResourceId string `json:"resourceId"`
+ ResourceType string `json:"resourceType"`
+ CertificateId string `json:"certificateId"`
+ CertificateMode string `json:"certificateMode"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ DetailStatus string `json:"detailStatus"`
+ ManagedStatus string `json:"managedStatus"`
+ Fingerprint string `json:"fingerprint"`
+ IssueTime string `json:"issueTime"`
+ ExpireTime string `json:"expireTime"`
+ DomainType string `json:"domainType"`
+ DomainName string `json:"domainName"`
+ EncryptionStandard string `json:"encryptionStandard"`
+ EncryptionAlgorithm string `json:"encryptionAlgorithm"`
+ CreateTime string `json:"createTime"`
+ UpdateTime string `json:"updateTime"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go
new file mode 100644
index 00000000..86227e5f
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/api_add_record.go
@@ -0,0 +1,46 @@
+package dns
+
+import (
+ "context"
+ "net/http"
+)
+
+type AddRecordRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ Host *string `json:"host,omitempty"`
+ Type *string `json:"type,omitempty"`
+ LineCode *string `json:"lineCode,omitempty"`
+ Value *string `json:"value,omitempty"`
+ TTL *int32 `json:"ttl,omitempty"`
+ State *int32 `json:"state,omitempty"`
+ Remark *string `json:"remark"`
+}
+
+type AddRecordResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ RecordId int32 `json:"recordId"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) AddRecord(req *AddRecordRequest) (*AddRecordResponse, error) {
+ return c.AddRecordWithContext(context.Background(), req)
+}
+
+func (c *Client) AddRecordWithContext(ctx context.Context, req *AddRecordRequest) (*AddRecordResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v2/addRecord")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &AddRecordResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go
new file mode 100644
index 00000000..ad35a349
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/api_delete_record.go
@@ -0,0 +1,35 @@
+package dns
+
+import (
+ "context"
+ "net/http"
+)
+
+type DeleteRecordRequest struct {
+ RecordId *int32 `json:"recordId,omitempty"`
+}
+
+type DeleteRecordResponse struct {
+ baseResult
+}
+
+func (c *Client) DeleteRecord(req *DeleteRecordRequest) (*DeleteRecordResponse, error) {
+ return c.DeleteRecordWithContext(context.Background(), req)
+}
+
+func (c *Client) DeleteRecordWithContext(ctx context.Context, req *DeleteRecordRequest) (*DeleteRecordResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v2/deleteRecord")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &DeleteRecordResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go b/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go
new file mode 100644
index 00000000..d94afcde
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/api_query_record_list.go
@@ -0,0 +1,44 @@
+package dns
+
+import (
+ "context"
+ "net/http"
+)
+
+type QueryRecordListRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ Host *string `json:"host,omitempty"`
+ Type *string `json:"type,omitempty"`
+ LineCode *string `json:"lineCode,omitempty"`
+ Value *string `json:"value,omitempty"`
+ State *int32 `json:"state,omitempty"`
+}
+
+type QueryRecordListResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Records []*DnsRecord `json:"records,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryRecordList(req *QueryRecordListRequest) (*QueryRecordListResponse, error) {
+ return c.QueryRecordListWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryRecordListWithContext(ctx context.Context, req *QueryRecordListRequest) (*QueryRecordListResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v2/queryRecordList")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryRecordListResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go b/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go
new file mode 100644
index 00000000..e6dbdc7e
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/api_update_record.go
@@ -0,0 +1,47 @@
+package dns
+
+import (
+ "context"
+ "net/http"
+)
+
+type UpdateRecordRequest struct {
+ RecordId *int32 `json:"recordId,omitempty"`
+ Domain *string `json:"domain,omitempty"`
+ Host *string `json:"host,omitempty"`
+ Type *string `json:"type,omitempty"`
+ LineCode *string `json:"lineCode,omitempty"`
+ Value *string `json:"value,omitempty"`
+ TTL *int32 `json:"ttl,omitempty"`
+ State *int32 `json:"state,omitempty"`
+ Remark *string `json:"remark"`
+}
+
+type UpdateRecordResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ RecordId int32 `json:"recordId"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) UpdateRecord(req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
+ return c.UpdateRecordWithContext(context.Background(), req)
+}
+
+func (c *Client) UpdateRecordWithContext(ctx context.Context, req *UpdateRecordRequest) (*UpdateRecordResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v2/updateRecord")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UpdateRecordResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/client.go b/internal/pkg/sdk3rd/ctyun/dns/client.go
new file mode 100644
index 00000000..4e684bd8
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/client.go
@@ -0,0 +1,50 @@
+package dns
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://smartdns-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ errorCode := result.GetError()
+ if (statusCode != "" && statusCode != "200") || errorCode != "" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/dns/types.go b/internal/pkg/sdk3rd/ctyun/dns/types.go
new file mode 100644
index 00000000..225f60d7
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/dns/types.go
@@ -0,0 +1,82 @@
+package dns
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type DnsRecord struct {
+ RecordId int32 `json:"recordId"`
+ Host string `json:"host"`
+ Type string `json:"type"`
+ LineCode string `json:"lineCode"`
+ Value string `json:"value"`
+ TTL int32 `json:"ttl"`
+ State int32 `json:"state"`
+ Remark string `json:"remark"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/api_create_certificate.go b/internal/pkg/sdk3rd/ctyun/elb/api_create_certificate.go
new file mode 100644
index 00000000..af17cdb7
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/api_create_certificate.go
@@ -0,0 +1,45 @@
+package elb
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateCertificateRequest struct {
+ ClientToken *string `json:"clientToken,omitempty"`
+ RegionID *string `json:"regionID,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ Type *string `json:"type,omitempty"`
+ Certificate *string `json:"certificate,omitempty"`
+ PrivateKey *string `json:"privateKey,omitempty"`
+}
+
+type CreateCertificateResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ ID string `json:"id"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
+ return c.CreateCertificateWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v4/elb/create-certificate")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateCertificateResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/api_list_certificate.go b/internal/pkg/sdk3rd/ctyun/elb/api_list_certificate.go
new file mode 100644
index 00000000..7a55adfb
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/api_list_certificate.go
@@ -0,0 +1,56 @@
+package elb
+
+import (
+ "context"
+ "net/http"
+)
+
+type ListCertificateRequest struct {
+ ClientToken *string `json:"clientToken,omitempty"`
+ RegionID *string `json:"regionID,omitempty"`
+ IDs *string `json:"IDs,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Type *string `json:"type,omitempty"`
+}
+
+type ListCertificateResponse struct {
+ baseResult
+
+ ReturnObj []*CertificateRecord `json:"returnObj,omitempty"`
+}
+
+func (c *Client) ListCertificate(req *ListCertificateRequest) (*ListCertificateResponse, error) {
+ return c.ListCertificateWithContext(context.Background(), req)
+}
+
+func (c *Client) ListCertificateWithContext(ctx context.Context, req *ListCertificateRequest) (*ListCertificateResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/list-certificate")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.ClientToken != nil {
+ httpreq.SetQueryParam("clientToken", *req.ClientToken)
+ }
+ if req.RegionID != nil {
+ httpreq.SetQueryParam("regionID", *req.RegionID)
+ }
+ if req.IDs != nil {
+ httpreq.SetQueryParam("IDs", *req.IDs)
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.Type != nil {
+ httpreq.SetQueryParam("type", *req.Type)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &ListCertificateResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/api_list_listener.go b/internal/pkg/sdk3rd/ctyun/elb/api_list_listener.go
new file mode 100644
index 00000000..fe37deaa
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/api_list_listener.go
@@ -0,0 +1,64 @@
+package elb
+
+import (
+ "context"
+ "net/http"
+)
+
+type ListListenerRequest struct {
+ ClientToken *string `json:"clientToken,omitempty"`
+ RegionID *string `json:"regionID,omitempty"`
+ ProjectID *string `json:"projectID,omitempty"`
+ IDs *string `json:"IDs,omitempty"`
+ Name *string `json:"name,omitempty"`
+ LoadBalancerID *string `json:"loadBalancerID,omitempty"`
+ AccessControlID *string `json:"accessControlID,omitempty"`
+}
+
+type ListListenerResponse struct {
+ baseResult
+
+ ReturnObj []*ListenerRecord `json:"returnObj,omitempty"`
+}
+
+func (c *Client) ListListener(req *ListListenerRequest) (*ListListenerResponse, error) {
+ return c.ListListenerWithContext(context.Background(), req)
+}
+
+func (c *Client) ListListenerWithContext(ctx context.Context, req *ListListenerRequest) (*ListListenerResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/list-listener")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.ClientToken != nil {
+ httpreq.SetQueryParam("clientToken", *req.ClientToken)
+ }
+ if req.RegionID != nil {
+ httpreq.SetQueryParam("regionID", *req.RegionID)
+ }
+ if req.ProjectID != nil {
+ httpreq.SetQueryParam("projectID", *req.ProjectID)
+ }
+ if req.IDs != nil {
+ httpreq.SetQueryParam("IDs", *req.IDs)
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.LoadBalancerID != nil {
+ httpreq.SetQueryParam("loadBalancerID", *req.LoadBalancerID)
+ }
+ if req.LoadBalancerID != nil {
+ httpreq.SetQueryParam("accessControlID", *req.AccessControlID)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &ListListenerResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/api_show_listener.go b/internal/pkg/sdk3rd/ctyun/elb/api_show_listener.go
new file mode 100644
index 00000000..a57a8bea
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/api_show_listener.go
@@ -0,0 +1,48 @@
+package elb
+
+import (
+ "context"
+ "net/http"
+)
+
+type ShowListenerRequest struct {
+ ClientToken *string `json:"clientToken,omitempty"`
+ RegionID *string `json:"regionID,omitempty"`
+ ListenerID *string `json:"listenerID,omitempty"`
+}
+
+type ShowListenerResponse struct {
+ baseResult
+
+ ReturnObj []*ListenerRecord `json:"returnObj,omitempty"`
+}
+
+func (c *Client) ShowListener(req *ShowListenerRequest) (*ShowListenerResponse, error) {
+ return c.ShowListenerWithContext(context.Background(), req)
+}
+
+func (c *Client) ShowListenerWithContext(ctx context.Context, req *ShowListenerRequest) (*ShowListenerResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v4/elb/show-listener")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.ClientToken != nil {
+ httpreq.SetQueryParam("clientToken", *req.ClientToken)
+ }
+ if req.RegionID != nil {
+ httpreq.SetQueryParam("regionID", *req.RegionID)
+ }
+ if req.ListenerID != nil {
+ httpreq.SetQueryParam("listenerID", *req.ListenerID)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &ShowListenerResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/api_update_listener.go b/internal/pkg/sdk3rd/ctyun/elb/api_update_listener.go
new file mode 100644
index 00000000..845d9100
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/api_update_listener.go
@@ -0,0 +1,44 @@
+package elb
+
+import (
+ "context"
+ "net/http"
+)
+
+type UpdateListenerRequest struct {
+ ClientToken *string `json:"clientToken,omitempty"`
+ RegionID *string `json:"regionID,omitempty"`
+ ListenerID *string `json:"listenerID,omitempty"`
+ Name *string `json:"name,omitempty"`
+ Description *string `json:"description,omitempty"`
+ CertificateID *string `json:"certificateID,omitempty"`
+ CaEnabled *bool `json:"caEnabled,omitempty"`
+ ClientCertificateID *string `json:"clientCertificateID,omitempty"`
+}
+
+type UpdateListenerResponse struct {
+ baseResult
+
+ ReturnObj []*ListenerRecord `json:"returnObj,omitempty"`
+}
+
+func (c *Client) UpdateListener(req *UpdateListenerRequest) (*UpdateListenerResponse, error) {
+ return c.UpdateListenerWithContext(context.Background(), req)
+}
+
+func (c *Client) UpdateListenerWithContext(ctx context.Context, req *UpdateListenerRequest) (*UpdateListenerResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v4/elb/update-listener")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UpdateListenerResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/client.go b/internal/pkg/sdk3rd/ctyun/elb/client.go
new file mode 100644
index 00000000..a71effa3
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/client.go
@@ -0,0 +1,50 @@
+package elb
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://ctelb-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ errorCode := result.GetError()
+ if (statusCode != "" && statusCode != "200" && statusCode != "800") || (errorCode != "" && errorCode != "SUCCESS") {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', description='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetDescription())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/elb/types.go b/internal/pkg/sdk3rd/ctyun/elb/types.go
new file mode 100644
index 00000000..4f2971b8
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/elb/types.go
@@ -0,0 +1,104 @@
+package elb
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetDescription() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ Description *string `json:"description,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetDescription() string {
+ if r.Description == nil {
+ return ""
+ }
+
+ return *r.Description
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertificateRecord struct {
+ ID string `json:"ID"`
+ RegionID string `json:"regionID"`
+ AzName string `json:"azName"`
+ ProjectID string `json:"projectID"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Type string `json:"type"`
+ Certificate string `json:"certificate"`
+ PrivateKey string `json:"privateKey"`
+ Status string `json:"status"`
+ CreatedTime string `json:"createdTime"`
+ UpdatedTime string `json:"updatedTime"`
+}
+
+type ListenerRecord struct {
+ ID string `json:"ID"`
+ RegionID string `json:"regionID"`
+ AzName string `json:"azName"`
+ ProjectID string `json:"projectID"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ LoadBalancerID string `json:"loadBalancerID"`
+ Protocol string `json:"protocol"`
+ ProtocolPort int32 `json:"protocolPort"`
+ CertificateID string `json:"certificateID,omitempty"`
+ CaEnabled bool `json:"caEnabled"`
+ ClientCertificateID string `json:"clientCertificateID,omitempty"`
+ Status string `json:"status"`
+ CreatedTime string `json:"createdTime"`
+ UpdatedTime string `json:"updatedTime"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go
new file mode 100644
index 00000000..93c09087
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go
@@ -0,0 +1,41 @@
+package icdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateCertRequest struct {
+ Name *string `json:"name,omitempty"`
+ Certs *string `json:"certs,omitempty"`
+ Key *string `json:"key,omitempty"`
+}
+
+type CreateCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Id int64 `json:"id"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
+ return c.CreateCertWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/cert/creat-cert")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go
new file mode 100644
index 00000000..2842d1f7
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go
@@ -0,0 +1,51 @@
+package icdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertDetailRequest struct {
+ Id *int64 `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Result *CertDetail `json:"result,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ return c.QueryCertDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Id != nil {
+ httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go
new file mode 100644
index 00000000..a96ab7b1
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go
@@ -0,0 +1,55 @@
+package icdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertListRequest struct {
+ Page *int32 `json:"page,omitempty"`
+ PerPage *int32 `json:"per_page,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertListResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Results []*CertRecord `json:"result,omitempty"`
+ Page int32 `json:"page,omitempty"`
+ PerPage int32 `json:"per_page,omitempty"`
+ TotalPage int32 `json:"total_page,omitempty"`
+ TotalRecords int32 `json:"total_records,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ return c.QueryCertListWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/cert/query-cert-list")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Page != nil {
+ httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
+ }
+ if req.PerPage != nil {
+ httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertListResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go
new file mode 100644
index 00000000..2ecff27e
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go
@@ -0,0 +1,64 @@
+package icdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type QueryDomainDetailRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+ FunctionNames *string `json:"function_names,omitempty"`
+}
+
+type QueryDomainDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Domain string `json:"domain"`
+ ProductCode string `json:"product_code"`
+ Status int32 `json:"status"`
+ AreaScope int32 `json:"area_scope"`
+ Cname string `json:"cname"`
+ HttpsStatus string `json:"https_status"`
+ HttpsBasic *struct {
+ HttpsForce string `json:"https_force"`
+ HttpForce string `json:"http_force"`
+ ForceStatus string `json:"force_status"`
+ OriginProtocol string `json:"origin_protocol"`
+ } `json:"https_basic,omitempty"`
+ CertName string `json:"cert_name"`
+ Ssl string `json:"ssl"`
+ SslStapling string `json:"ssl_stapling"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ return c.QueryDomainDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/v1/domain/query-domain-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Domain != nil {
+ httpreq.SetQueryParam("domain", *req.Domain)
+ }
+ if req.ProductCode != nil {
+ httpreq.SetQueryParam("product_code", *req.ProductCode)
+ }
+ if req.FunctionNames != nil {
+ httpreq.SetQueryParam("function_names", *req.FunctionNames)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryDomainDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_update_domain.go b/internal/pkg/sdk3rd/ctyun/icdn/api_update_domain.go
new file mode 100644
index 00000000..70d3b73a
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/api_update_domain.go
@@ -0,0 +1,37 @@
+package icdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type UpdateDomainRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ HttpsStatus *string `json:"https_status,omitempty"`
+ CertName *string `json:"cert_name,omitempty"`
+}
+
+type UpdateDomainResponse struct {
+ baseResult
+}
+
+func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ return c.UpdateDomainWithContext(context.Background(), req)
+}
+
+func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/v1/domain/update-domain")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UpdateDomainResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/client.go b/internal/pkg/sdk3rd/ctyun/icdn/client.go
new file mode 100644
index 00000000..5f3e0084
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/client.go
@@ -0,0 +1,49 @@
+package icdn
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://icdn-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ if statusCode != "" && statusCode != "100000" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/icdn/types.go b/internal/pkg/sdk3rd/ctyun/icdn/types.go
new file mode 100644
index 00000000..2b2f3d95
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/icdn/types.go
@@ -0,0 +1,90 @@
+package icdn
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertRecord struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ CN string `json:"cn"`
+ SANs []string `json:"sans"`
+ UsageMode int32 `json:"usage_mode"`
+ State int32 `json:"state"`
+ ExpiresTime int64 `json:"expires"`
+ IssueTime int64 `json:"issue"`
+ Issuer string `json:"issuer"`
+ CreatedTime int64 `json:"created"`
+}
+
+type CertDetail struct {
+ CertRecord
+ Certs string `json:"certs"`
+ Key string `json:"key"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go
new file mode 100644
index 00000000..c0188d3d
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go
@@ -0,0 +1,41 @@
+package lvdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type CreateCertRequest struct {
+ Name *string `json:"name,omitempty"`
+ Certs *string `json:"certs,omitempty"`
+ Key *string `json:"key,omitempty"`
+}
+
+type CreateCertResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Id int64 `json:"id"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) {
+ return c.CreateCertWithContext(context.Background(), req)
+}
+
+func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/cert/creat-cert")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &CreateCertResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go
new file mode 100644
index 00000000..cadcc6dc
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go
@@ -0,0 +1,51 @@
+package lvdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertDetailRequest struct {
+ Id *int64 `json:"id,omitempty"`
+ Name *string `json:"name,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Result *CertDetail `json:"result,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ return c.QueryCertDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Id != nil {
+ httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id)))
+ }
+ if req.Name != nil {
+ httpreq.SetQueryParam("name", *req.Name)
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go
new file mode 100644
index 00000000..d1a7b974
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go
@@ -0,0 +1,55 @@
+package lvdn
+
+import (
+ "context"
+ "net/http"
+ "strconv"
+)
+
+type QueryCertListRequest struct {
+ Page *int32 `json:"page,omitempty"`
+ PerPage *int32 `json:"per_page,omitempty"`
+ UsageMode *int32 `json:"usage_mode,omitempty"`
+}
+
+type QueryCertListResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Results []*CertRecord `json:"result,omitempty"`
+ Page int32 `json:"page,omitempty"`
+ PerPage int32 `json:"per_page,omitempty"`
+ TotalPage int32 `json:"total_page,omitempty"`
+ TotalRecords int32 `json:"total_records,omitempty"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ return c.QueryCertListWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-list")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Page != nil {
+ httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page)))
+ }
+ if req.PerPage != nil {
+ httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage)))
+ }
+ if req.UsageMode != nil {
+ httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode)))
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryCertListResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go
new file mode 100644
index 00000000..29e5f08f
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go
@@ -0,0 +1,52 @@
+package lvdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type QueryDomainDetailRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+}
+
+type QueryDomainDetailResponse struct {
+ baseResult
+
+ ReturnObj *struct {
+ Domain string `json:"domain"`
+ ProductCode string `json:"product_code"`
+ Status int32 `json:"status"`
+ AreaScope int32 `json:"area_scope"`
+ Cname string `json:"cname"`
+ HttpsSwitch int32 `json:"https_switch"`
+ CertName string `json:"cert_name"`
+ } `json:"returnObj,omitempty"`
+}
+
+func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ return c.QueryDomainDetailWithContext(context.Background(), req)
+}
+
+func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) {
+ httpreq, err := c.newRequest(http.MethodGet, "/live/domain/query-domain-detail")
+ if err != nil {
+ return nil, err
+ } else {
+ if req.Domain != nil {
+ httpreq.SetQueryParam("domain", *req.Domain)
+ }
+ if req.ProductCode != nil {
+ httpreq.SetQueryParam("product_code", *req.ProductCode)
+ }
+
+ httpreq.SetContext(ctx)
+ }
+
+ result := &QueryDomainDetailResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go
new file mode 100644
index 00000000..d5f90306
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go
@@ -0,0 +1,38 @@
+package lvdn
+
+import (
+ "context"
+ "net/http"
+)
+
+type UpdateDomainRequest struct {
+ Domain *string `json:"domain,omitempty"`
+ ProductCode *string `json:"product_code,omitempty"`
+ HttpsSwitch *int32 `json:"https_switch,omitempty"`
+ CertName *string `json:"cert_name,omitempty"`
+}
+
+type UpdateDomainResponse struct {
+ baseResult
+}
+
+func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ return c.UpdateDomainWithContext(context.Background(), req)
+}
+
+func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) {
+ httpreq, err := c.newRequest(http.MethodPost, "/live/domain/update-domain")
+ if err != nil {
+ return nil, err
+ } else {
+ httpreq.SetBody(req)
+ httpreq.SetContext(ctx)
+ }
+
+ result := &UpdateDomainResponse{}
+ if _, err := c.doRequestWithResult(httpreq, result); err != nil {
+ return result, err
+ }
+
+ return result, nil
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/client.go b/internal/pkg/sdk3rd/ctyun/lvdn/client.go
new file mode 100644
index 00000000..5542bad9
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/client.go
@@ -0,0 +1,49 @@
+package lvdn
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi"
+)
+
+const endpoint = "https://ctlvdn-global.ctapi.ctyun.cn"
+
+type Client struct {
+ client *openapi.Client
+}
+
+func NewClient(accessKeyId, secretAccessKey string) (*Client, error) {
+ client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey)
+ if err != nil {
+ return nil, err
+ }
+
+ return &Client{client: client}, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
+ return c.client.NewRequest(method, path)
+}
+
+func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) {
+ return c.client.DoRequest(request)
+}
+
+func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) {
+ response, err := c.client.DoRequestWithResult(request, result)
+ if err == nil {
+ statusCode := result.GetStatusCode()
+ if statusCode != "" && statusCode != "100000" {
+ return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage())
+ }
+ }
+
+ return response, err
+}
diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/types.go b/internal/pkg/sdk3rd/ctyun/lvdn/types.go
new file mode 100644
index 00000000..2ddc5369
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/lvdn/types.go
@@ -0,0 +1,90 @@
+package lvdn
+
+import (
+ "bytes"
+ "encoding/json"
+ "strconv"
+)
+
+type baseResultInterface interface {
+ GetStatusCode() string
+ GetMessage() string
+ GetError() string
+ GetErrorMessage() string
+}
+
+type baseResult struct {
+ StatusCode json.RawMessage `json:"statusCode,omitempty"`
+ Message *string `json:"message,omitempty"`
+ Error *string `json:"error,omitempty"`
+ ErrorMessage *string `json:"errorMessage,omitempty"`
+ RequestId *string `json:"requestId,omitempty"`
+}
+
+func (r *baseResult) GetStatusCode() string {
+ if r.StatusCode == nil {
+ return ""
+ }
+
+ decoder := json.NewDecoder(bytes.NewReader(r.StatusCode))
+ token, err := decoder.Token()
+ if err != nil {
+ return ""
+ }
+
+ switch t := token.(type) {
+ case string:
+ return t
+ case float64:
+ return strconv.FormatFloat(t, 'f', -1, 64)
+ case json.Number:
+ return t.String()
+ default:
+ return ""
+ }
+}
+
+func (r *baseResult) GetMessage() string {
+ if r.Message == nil {
+ return ""
+ }
+
+ return *r.Message
+}
+
+func (r *baseResult) GetError() string {
+ if r.Error == nil {
+ return ""
+ }
+
+ return *r.Error
+}
+
+func (r *baseResult) GetErrorMessage() string {
+ if r.ErrorMessage == nil {
+ return ""
+ }
+
+ return *r.ErrorMessage
+}
+
+var _ baseResultInterface = (*baseResult)(nil)
+
+type CertRecord struct {
+ Id int64 `json:"id"`
+ Name string `json:"name"`
+ CN string `json:"cn"`
+ SANs []string `json:"sans"`
+ UsageMode int32 `json:"usage_mode"`
+ State int32 `json:"state"`
+ ExpiresTime int64 `json:"expires"`
+ IssueTime int64 `json:"issue"`
+ Issuer string `json:"issuer"`
+ CreatedTime int64 `json:"created"`
+}
+
+type CertDetail struct {
+ CertRecord
+ Certs string `json:"certs"`
+ Key string `json:"key"`
+}
diff --git a/internal/pkg/sdk3rd/ctyun/openapi/client.go b/internal/pkg/sdk3rd/ctyun/openapi/client.go
new file mode 100644
index 00000000..ad790dc5
--- /dev/null
+++ b/internal/pkg/sdk3rd/ctyun/openapi/client.go
@@ -0,0 +1,167 @@
+package openapi
+
+import (
+ "crypto/hmac"
+ "crypto/sha256"
+ "encoding/base64"
+ "encoding/hex"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "time"
+
+ "github.com/go-resty/resty/v2"
+ "github.com/google/uuid"
+)
+
+type Client struct {
+ client *resty.Client
+}
+
+func NewClient(endpoint, accessKeyId, secretAccessKey string) (*Client, error) {
+ if endpoint == "" {
+ return nil, fmt.Errorf("sdkerr: unset endpoint")
+ }
+ if _, err := url.Parse(endpoint); err != nil {
+ return nil, fmt.Errorf("sdkerr: invalid endpoint: %w", err)
+ }
+ if accessKeyId == "" {
+ return nil, fmt.Errorf("sdkerr: unset accessKey")
+ }
+ if secretAccessKey == "" {
+ return nil, fmt.Errorf("sdkerr: unset secretKey")
+ }
+
+ client := resty.New().
+ SetBaseURL(endpoint).
+ SetHeader("Accept", "application/json").
+ SetHeader("Content-Type", "application/json").
+ SetHeader("User-Agent", "certimate").
+ SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
+ // 生成时间戳及流水号
+ now := time.Now()
+ eopDate := now.Format("20060102T150405Z")
+ eopReqId := uuid.New().String()
+
+ // 获取查询参数
+ queryStr := ""
+ if req.URL != nil {
+ queryStr = req.URL.Query().Encode()
+ }
+
+ // 获取请求正文
+ payloadStr := ""
+ if req.Body != nil {
+ reader, err := req.GetBody()
+ if err != nil {
+ return err
+ }
+
+ defer reader.Close()
+ payload, err := io.ReadAll(reader)
+ if err != nil {
+ return err
+ }
+
+ payloadStr = string(payload)
+ }
+
+ // 构造代签字符串
+ payloadHash := sha256.Sum256([]byte(payloadStr))
+ payloadHashHex := hex.EncodeToString(payloadHash[:])
+ dataToSign := fmt.Sprintf("ctyun-eop-request-id:%s\neop-date:%s\n\n%s\n%s", eopReqId, eopDate, queryStr, payloadHashHex)
+
+ // 生成 ktime
+ hasher := hmac.New(sha256.New, []byte(secretAccessKey))
+ hasher.Write([]byte(eopDate))
+ ktime := hasher.Sum(nil)
+
+ // 生成 kak
+ hasher = hmac.New(sha256.New, ktime)
+ hasher.Write([]byte(accessKeyId))
+ kak := hasher.Sum(nil)
+
+ // 生成 kdate
+ hasher = hmac.New(sha256.New, kak)
+ hasher.Write([]byte(now.Format("20060102")))
+ kdate := hasher.Sum(nil)
+
+ // 构造签名
+ hasher = hmac.New(sha256.New, kdate)
+ hasher.Write([]byte(dataToSign))
+ sign := hasher.Sum(nil)
+ signStr := base64.StdEncoding.EncodeToString(sign)
+
+ // 设置请求头
+ req.Header.Set("ctyun-eop-request-id", eopReqId)
+ req.Header.Set("eop-date", eopDate)
+ req.Header.Set("eop-authorization", fmt.Sprintf("%s Headers=ctyun-eop-request-id;eop-date Signature=%s", accessKeyId, signStr))
+
+ return nil
+ })
+
+ return &Client{
+ client: client,
+ }, nil
+}
+
+func (c *Client) SetTimeout(timeout time.Duration) *Client {
+ c.client.SetTimeout(timeout)
+ return c
+}
+
+func (c *Client) NewRequest(method string, path string) (*resty.Request, error) {
+ if method == "" {
+ return nil, fmt.Errorf("sdkerr: unset method")
+ }
+ if path == "" {
+ return nil, fmt.Errorf("sdkerr: unset path")
+ }
+
+ req := c.client.R()
+ req.Method = method
+ req.URL = path
+ return req, nil
+}
+
+func (c *Client) DoRequest(request *resty.Request) (*resty.Response, error) {
+ if request == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ // WARN:
+ // PLEASE DO NOT USE `req.SetResult` or `req.SetError` here.
+
+ resp, err := request.Send()
+ if err != nil {
+ return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
+ } else if resp.IsError() {
+ return resp, fmt.Errorf("sdkerr: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.String())
+ }
+
+ return resp, nil
+}
+
+func (c *Client) DoRequestWithResult(request *resty.Request, result any) (*resty.Response, error) {
+ if request == nil {
+ return nil, fmt.Errorf("sdkerr: nil request")
+ }
+
+ response, err := c.DoRequest(request)
+ if err != nil {
+ if response != nil {
+ json.Unmarshal(response.Body(), &result)
+ }
+ return response, err
+ }
+
+ if len(response.Body()) != 0 {
+ if err := json.Unmarshal(response.Body(), &result); err != nil {
+ return response, fmt.Errorf("sdkerr: failed to unmarshal response: %w", err)
+ }
+ }
+
+ return response, nil
+}
diff --git a/internal/pkg/utils/slice/slice.go b/internal/pkg/utils/slice/iter.go
similarity index 100%
rename from internal/pkg/utils/slice/slice.go
rename to internal/pkg/utils/slice/iter.go
diff --git a/internal/pkg/utils/type/assert.go b/internal/pkg/utils/type/assert.go
index 509a77cc..e1413041 100644
--- a/internal/pkg/utils/type/assert.go
+++ b/internal/pkg/utils/type/assert.go
@@ -1,6 +1,8 @@
package typeutil
-import "reflect"
+import (
+ "reflect"
+)
// 判断对象是否为 nil。
// 与直接使用 `obj == nil` 不同,该函数会正确判断接口类型对象的真实值是否为空。
diff --git a/internal/pkg/utils/type/cast.go b/internal/pkg/utils/type/cast.go
index 77eb9dad..8c34dc0f 100644
--- a/internal/pkg/utils/type/cast.go
+++ b/internal/pkg/utils/type/cast.go
@@ -1,6 +1,8 @@
package typeutil
-import "reflect"
+import (
+ "reflect"
+)
// 将对象转换为指针。
//
diff --git a/internal/workflow/node-processor/monitor_node.go b/internal/workflow/node-processor/monitor_node.go
index d13e4247..b1c4ca53 100644
--- a/internal/workflow/node-processor/monitor_node.go
+++ b/internal/workflow/node-processor/monitor_node.go
@@ -35,7 +35,7 @@ func (n *monitorNode) Process(ctx context.Context) error {
nodeCfg := n.node.GetConfigForMonitor()
n.logger.Info("ready to monitor certificate ...", slog.Any("config", nodeCfg))
- targetAddr := net.JoinHostPort(nodeCfg.Host, fmt.Sprintf("%d", nodeCfg.Port))
+ targetAddr := net.JoinHostPort(nodeCfg.Host, strconv.Itoa(int(nodeCfg.Port)))
if nodeCfg.Port == 0 {
targetAddr = net.JoinHostPort(nodeCfg.Host, "443")
}
@@ -100,7 +100,13 @@ func (n *monitorNode) Process(ctx context.Context) error {
if validated {
n.logger.Info(fmt.Sprintf("the certificate is valid, and will expire in %d day(s)", daysLeft))
} else {
- n.logger.Warn(fmt.Sprintf("the certificate is invalid", validated))
+ if !isCertHostMatched {
+ n.logger.Warn("the certificate is invalid, because it is not matched the host")
+ } else if !isCertPeriodValid {
+ n.logger.Warn("the certificate is invalid, because it is either expired or not yet valid")
+ } else {
+ n.logger.Warn("the certificate is invalid")
+ }
}
}
}
diff --git a/ui/public/imgs/providers/apisix.svg b/ui/public/imgs/providers/apisix.svg
new file mode 100644
index 00000000..55b6e4f2
--- /dev/null
+++ b/ui/public/imgs/providers/apisix.svg
@@ -0,0 +1 @@
+
diff --git a/ui/public/imgs/providers/ctcccloud.svg b/ui/public/imgs/providers/ctcccloud.svg
new file mode 100644
index 00000000..b5ea5d76
--- /dev/null
+++ b/ui/public/imgs/providers/ctcccloud.svg
@@ -0,0 +1 @@
+
diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx
index 4bb1d439..91d7139f 100644
--- a/ui/src/components/access/AccessForm.tsx
+++ b/ui/src/components/access/AccessForm.tsx
@@ -15,6 +15,7 @@ import AccessForm1PanelConfig from "./AccessForm1PanelConfig";
import AccessFormACMECAConfig from "./AccessFormACMECAConfig";
import AccessFormACMEHttpReqConfig from "./AccessFormACMEHttpReqConfig";
import AccessFormAliyunConfig from "./AccessFormAliyunConfig";
+import AccessFormAPISIXConfig from "./AccessFormAPISIXConfig";
import AccessFormAWSConfig from "./AccessFormAWSConfig";
import AccessFormAzureConfig from "./AccessFormAzureConfig";
import AccessFormBaiduCloudConfig from "./AccessFormBaiduCloudConfig";
@@ -29,6 +30,7 @@ import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig";
import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig";
import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig";
import AccessFormConstellixConfig from "./AccessFormConstellixConfig";
+import AccessFormCTCCCloudConfig from "./AccessFormCTCCCloudConfig";
import AccessFormDeSECConfig from "./AccessFormDeSECConfig";
import AccessFormDigitalOceanConfig from "./AccessFormDigitalOceanConfig";
import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig";
@@ -194,6 +196,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.ALIYUN:
return ;
+ case ACCESS_PROVIDERS.APISIX:
+ return ;
case ACCESS_PROVIDERS.AWS:
return ;
case ACCESS_PROVIDERS.AZURE:
@@ -222,6 +226,8 @@ const AccessForm = forwardRef(({ className,
return ;
case ACCESS_PROVIDERS.CONSTELLIX:
return ;
+ case ACCESS_PROVIDERS.CTCCCLOUD:
+ return ;
case ACCESS_PROVIDERS.DESEC:
return ;
case ACCESS_PROVIDERS.DIGITALOCEAN:
diff --git a/ui/src/components/access/AccessFormAPISIXConfig.tsx b/ui/src/components/access/AccessFormAPISIXConfig.tsx
new file mode 100644
index 00000000..856a6565
--- /dev/null
+++ b/ui/src/components/access/AccessFormAPISIXConfig.tsx
@@ -0,0 +1,71 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Switch } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { type AccessConfigForAPISIX } from "@/domain/access";
+
+type AccessFormAPISIXConfigFieldValues = Nullish;
+
+export type AccessFormAPISIXConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormAPISIXConfigFieldValues;
+ onValuesChange?: (values: AccessFormAPISIXConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormAPISIXConfigFieldValues => {
+ return {
+ serverUrl: "http://:9180/",
+ apiKey: "",
+ };
+};
+
+const AccessFormAPISIXConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormAPISIXConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ serverUrl: z.string().url(t("common.errmsg.url_invalid")),
+ apiKey: z.string().trim().nonempty(t("access.form.apisix_api_key.placeholder")),
+ allowInsecureConnections: z.boolean().nullish(),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+ );
+};
+
+export default AccessFormAPISIXConfig;
diff --git a/ui/src/components/access/AccessFormCTCCCloudConfig.tsx b/ui/src/components/access/AccessFormCTCCCloudConfig.tsx
new file mode 100644
index 00000000..f0e9df39
--- /dev/null
+++ b/ui/src/components/access/AccessFormCTCCCloudConfig.tsx
@@ -0,0 +1,73 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+import { type AccessConfigForCTCCCloud } from "@/domain/access";
+
+type AccessFormCTCCCloudConfigFieldValues = Nullish;
+
+export type AccessFormCTCCCloudConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: AccessFormCTCCCloudConfigFieldValues;
+ onValuesChange?: (values: AccessFormCTCCCloudConfigFieldValues) => void;
+};
+
+const initFormModel = (): AccessFormCTCCCloudConfigFieldValues => {
+ return {
+ accessKeyId: "",
+ secretAccessKey: "",
+ };
+};
+
+const AccessFormCTCCCloudConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormCTCCCloudConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ accessKeyId: z
+ .string()
+ .min(1, t("access.form.ctcccloud_access_key_id.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 })),
+ secretAccessKey: z
+ .string()
+ .min(1, t("access.form.ctcccloud_secret_access_key.placeholder"))
+ .max(64, t("common.errmsg.string_max", { max: 64 })),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ }
+ >
+
+
+
+ );
+};
+
+export default AccessFormCTCCCloudConfig;
diff --git a/ui/src/components/access/AccessFormEmailConfig.tsx b/ui/src/components/access/AccessFormEmailConfig.tsx
index ae79794a..ae3ac5c9 100644
--- a/ui/src/components/access/AccessFormEmailConfig.tsx
+++ b/ui/src/components/access/AccessFormEmailConfig.tsx
@@ -54,6 +54,7 @@ const AccessFormEmailConfig = ({ form: formInst, formName, disabled, initialValu
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
+ defaultSenderName: z.string().nullish(),
defaultReceiverAddress: z
.string()
.nullish()
@@ -115,6 +116,10 @@ const AccessFormEmailConfig = ({ form: formInst, formName, disabled, initialValu
+
+
+
+
diff --git a/ui/src/components/provider/DeploymentProviderPicker.tsx b/ui/src/components/provider/DeploymentProviderPicker.tsx
index bb569acd..9b441189 100644
--- a/ui/src/components/provider/DeploymentProviderPicker.tsx
+++ b/ui/src/components/provider/DeploymentProviderPicker.tsx
@@ -72,6 +72,7 @@ const DeploymentProviderPicker = ({ className, style, autoFocus, filter, placeho
DEPLOYMENT_CATEGORIES.LOADBALANCE,
DEPLOYMENT_CATEGORIES.FIREWALL,
DEPLOYMENT_CATEGORIES.AV,
+ DEPLOYMENT_CATEGORIES.ACCELERATOR,
DEPLOYMENT_CATEGORIES.APIGATEWAY,
DEPLOYMENT_CATEGORIES.SERVERLESS,
DEPLOYMENT_CATEGORIES.WEBSITE,
diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
index 96e50911..15f627a9 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx
@@ -33,6 +33,7 @@ import DeployNodeConfigFormAliyunNLBConfig from "./DeployNodeConfigFormAliyunNLB
import DeployNodeConfigFormAliyunOSSConfig from "./DeployNodeConfigFormAliyunOSSConfig";
import DeployNodeConfigFormAliyunVODConfig from "./DeployNodeConfigFormAliyunVODConfig";
import DeployNodeConfigFormAliyunWAFConfig from "./DeployNodeConfigFormAliyunWAFConfig";
+import DeployNodeConfigFormAPISIXConfig from "./DeployNodeConfigFormAPISIXConfig";
import DeployNodeConfigFormAWSACMConfig from "./DeployNodeConfigFormAWSACMConfig";
import DeployNodeConfigFormAWSCloudFrontConfig from "./DeployNodeConfigFormAWSCloudFrontConfig";
import DeployNodeConfigFormAWSIAMConfig from "./DeployNodeConfigFormAWSIAMConfig";
@@ -47,6 +48,11 @@ import DeployNodeConfigFormBaotaWAFSiteConfig from "./DeployNodeConfigFormBaotaW
import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNConfig.tsx";
import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig";
import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig";
+import DeployNodeConfigFormCTCCCloudAOConfig from "./DeployNodeConfigFormCTCCCloudAOConfig";
+import DeployNodeConfigFormCTCCCloudCDNConfig from "./DeployNodeConfigFormCTCCCloudCDNConfig";
+import DeployNodeConfigFormCTCCCloudELBConfig from "./DeployNodeConfigFormCTCCCloudELBConfig";
+import DeployNodeConfigFormCTCCCloudICDNConfig from "./DeployNodeConfigFormCTCCCloudICDNConfig";
+import DeployNodeConfigFormCTCCCloudLVDNConfig from "./DeployNodeConfigFormCTCCCloudLVDNConfig";
import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig";
import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig";
import DeployNodeConfigFormFlexCDNConfig from "./DeployNodeConfigFormFlexCDNConfig";
@@ -77,6 +83,7 @@ import DeployNodeConfigFormTencentCloudCOSConfig from "./DeployNodeConfigFormTen
import DeployNodeConfigFormTencentCloudCSSConfig from "./DeployNodeConfigFormTencentCloudCSSConfig.tsx";
import DeployNodeConfigFormTencentCloudECDNConfig from "./DeployNodeConfigFormTencentCloudECDNConfig.tsx";
import DeployNodeConfigFormTencentCloudEOConfig from "./DeployNodeConfigFormTencentCloudEOConfig.tsx";
+import DeployNodeConfigFormTencentCloudGAAPConfig from "./DeployNodeConfigFormTencentCloudGAAPConfig.tsx";
import DeployNodeConfigFormTencentCloudSCFConfig from "./DeployNodeConfigFormTencentCloudSCFConfig";
import DeployNodeConfigFormTencentCloudSSLDeployConfig from "./DeployNodeConfigFormTencentCloudSSLDeployConfig";
import DeployNodeConfigFormTencentCloudVODConfig from "./DeployNodeConfigFormTencentCloudVODConfig";
@@ -233,6 +240,8 @@ const DeployNodeConfigForm = forwardRef ;
case DEPLOYMENT_PROVIDERS.ALIYUN_WAF:
return ;
+ case DEPLOYMENT_PROVIDERS.APISIX:
+ return ;
case DEPLOYMENT_PROVIDERS.AWS_ACM:
return ;
case DEPLOYMENT_PROVIDERS.AWS_CLOUDFRONT:
@@ -261,6 +270,16 @@ const DeployNodeConfigForm = forwardRef ;
case DEPLOYMENT_PROVIDERS.CDNFLY:
return ;
+ case DEPLOYMENT_PROVIDERS.CTCCCLOUD_AO:
+ return ;
+ case DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN:
+ return ;
+ case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ELB:
+ return ;
+ case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN:
+ return ;
+ case DEPLOYMENT_PROVIDERS.CTCCCLOUD_LVDN:
+ return ;
case DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN:
return ;
case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS:
@@ -321,6 +340,8 @@ const DeployNodeConfigForm = forwardRef ;
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_EO:
return ;
+ case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_GAAP:
+ return ;
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SCF:
return ;
case DEPLOYMENT_PROVIDERS.TENCENTCLOUD_SSL_DEPLOY:
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx
new file mode 100644
index 00000000..0fd67674
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormAPISIXConfig.tsx
@@ -0,0 +1,81 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Select } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import Show from "@/components/Show";
+
+type DeployNodeConfigFormAPISIXConfigFieldValues = Nullish<{
+ resourceType: string;
+ certificateId?: string;
+}>;
+
+export type DeployNodeConfigFormAPISIXConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormAPISIXConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormAPISIXConfigFieldValues) => void;
+};
+
+const RESOURCE_TYPE_CERTIFICATE = "certificate" as const;
+
+const initFormModel = (): DeployNodeConfigFormAPISIXConfigFieldValues => {
+ return {
+ resourceType: RESOURCE_TYPE_CERTIFICATE,
+ certificateId: "",
+ };
+};
+
+const DeployNodeConfigFormAPISIXConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAPISIXConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ resourceType: z.literal(RESOURCE_TYPE_CERTIFICATE, {
+ message: t("workflow_node.deploy.form.apisix_resource_type.placeholder"),
+ }),
+ certificateId: z
+ .string()
+ .nullish()
+ .refine((v) => fieldResourceType !== RESOURCE_TYPE_CERTIFICATE || !!v?.trim(), t("workflow_node.deploy.form.apisix_certificate_id.placeholder")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const fieldResourceType = Form.useWatch("resourceType", formInst);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+
+
+
+ {t("workflow_node.deploy.form.apisix_resource_type.option.certificate.label")}
+
+
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormAPISIXConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx
index bbfca5e6..341bfe6a 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunALBConfig.tsx
@@ -26,7 +26,9 @@ const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormAliyunALBConfigFieldValues => {
- return {};
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
};
const DeployNodeConfigFormAliyunALBConfig = ({
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx
index e666800e..fcd59569 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx
@@ -27,6 +27,7 @@ const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormAliyunCLBConfigFieldValues => {
return {
+ resourceType: RESOURCE_TYPE_LISTENER,
listenerPort: 443,
};
};
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx
index 20dd1ae1..f90652f9 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunGAConfig.tsx
@@ -25,7 +25,9 @@ const RESOURCE_TYPE_ACCELERATOR = "accelerator" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormAliyunGAConfigFieldValues => {
- return {};
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
};
const DeployNodeConfigFormAliyunGAConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: DeployNodeConfigFormAliyunGAConfigProps) => {
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx
index c37b97db..abd95843 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunNLBConfig.tsx
@@ -24,7 +24,9 @@ const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormAliyunNLBConfigFieldValues => {
- return {};
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
};
const DeployNodeConfigFormAliyunNLBConfig = ({
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx
index 875d254b..aba8bf6b 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx
@@ -27,6 +27,7 @@ const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues => {
return {
+ resourceType: RESOURCE_TYPE_LISTENER,
listenerPort: 443,
};
};
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx
index 99c0b059..fd61053c 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx
@@ -27,6 +27,7 @@ const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormBaiduCloudBLBConfigFieldValues => {
return {
+ resourceType: RESOURCE_TYPE_LISTENER,
listenerPort: 443,
};
};
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx
new file mode 100644
index 00000000..f46934cd
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx
@@ -0,0 +1,65 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { validDomainName } from "@/utils/validators";
+
+type DeployNodeConfigFormCTCCCloudAOConfigFieldValues = Nullish<{
+ domain: string;
+}>;
+
+export type DeployNodeConfigFormCTCCCloudAOConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormCTCCCloudAOConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormCTCCCloudAOConfigFieldValues) => void;
+};
+
+const initFormModel = (): DeployNodeConfigFormCTCCCloudAOConfigFieldValues => {
+ return {};
+};
+
+const DeployNodeConfigFormCTCCCloudAOConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormCTCCCloudAOConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ domain: z
+ .string({ message: t("workflow_node.deploy.form.ctcccloud_ao_domain.placeholder") })
+ .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormCTCCCloudAOConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx
new file mode 100644
index 00000000..b7f564e5
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudCDNConfig.tsx
@@ -0,0 +1,65 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { validDomainName } from "@/utils/validators";
+
+type DeployNodeConfigFormCTCCCloudCDNConfigFieldValues = Nullish<{
+ domain: string;
+}>;
+
+export type DeployNodeConfigFormCTCCCloudCDNConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormCTCCCloudCDNConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormCTCCCloudCDNConfigFieldValues) => void;
+};
+
+const initFormModel = (): DeployNodeConfigFormCTCCCloudCDNConfigFieldValues => {
+ return {};
+};
+
+const DeployNodeConfigFormCTCCCloudCDNConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormCTCCCloudCDNConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ domain: z
+ .string({ message: t("workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder") })
+ .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormCTCCCloudCDNConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx
new file mode 100644
index 00000000..6577c35c
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudELBConfig.tsx
@@ -0,0 +1,121 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Select } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import Show from "@/components/Show";
+
+type DeployNodeConfigFormCTCCCloudELBConfigFieldValues = Nullish<{
+ regionId: string;
+ resourceType: string;
+ loadbalancerId?: string;
+ listenerId?: string;
+}>;
+
+export type DeployNodeConfigFormCTCCCloudELBConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormCTCCCloudELBConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormCTCCCloudELBConfigFieldValues) => void;
+};
+
+const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
+const RESOURCE_TYPE_LISTENER = "listener" as const;
+
+const initFormModel = (): DeployNodeConfigFormCTCCCloudELBConfigFieldValues => {
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
+};
+
+const DeployNodeConfigFormCTCCCloudELBConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormCTCCCloudELBConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], {
+ message: t("workflow_node.deploy.form.ctcccloud_elb_resource_type.placeholder"),
+ }),
+ regionId: z
+ .string({ message: t("workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder") })
+ .nonempty(t("workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder")),
+ loadbalancerId: z
+ .string()
+ .max(64, t("common.errmsg.string_max", { max: 64 }))
+ .nullish()
+ .refine((v) => fieldResourceType !== RESOURCE_TYPE_LOADBALANCER || !!v?.trim(), t("workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder")),
+ listenerId: z
+ .string()
+ .max(64, t("common.errmsg.string_max", { max: 64 }))
+ .nullish()
+ .refine((v) => fieldResourceType !== RESOURCE_TYPE_LISTENER || !!v?.trim(), t("workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const fieldResourceType = Form.useWatch("resourceType", formInst);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+
+
+
+ {t("workflow_node.deploy.form.ctcccloud_elb_resource_type.option.loadbalancer.label")}
+
+
+ {t("workflow_node.deploy.form.ctcccloud_elb_resource_type.option.listener.label")}
+
+
+
+
+ }
+ >
+
+
+
+
+ }
+ >
+
+
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormCTCCCloudELBConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx
new file mode 100644
index 00000000..0e94650b
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx
@@ -0,0 +1,65 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { validDomainName } from "@/utils/validators";
+
+type DeployNodeConfigFormCTCCCloudICDNConfigFieldValues = Nullish<{
+ domain: string;
+}>;
+
+export type DeployNodeConfigFormCTCCCloudICDNConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues) => void;
+};
+
+const initFormModel = (): DeployNodeConfigFormCTCCCloudICDNConfigFieldValues => {
+ return {};
+};
+
+const DeployNodeConfigFormCTCCCloudICDNConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormCTCCCloudICDNConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ domain: z
+ .string({ message: t("workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder") })
+ .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormCTCCCloudICDNConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx
new file mode 100644
index 00000000..54f22907
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx
@@ -0,0 +1,65 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import { validDomainName } from "@/utils/validators";
+
+type DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues = Nullish<{
+ domain: string;
+}>;
+
+export type DeployNodeConfigFormCTCCCloudLVDNConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues) => void;
+};
+
+const initFormModel = (): DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues => {
+ return {};
+};
+
+const DeployNodeConfigFormCTCCCloudLVDNConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormCTCCCloudLVDNConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ domain: z
+ .string({ message: t("workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder") })
+ .refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+ }
+ >
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormCTCCCloudLVDNConfig;
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx
index 259e1f44..c3f17e2a 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormHuaweiCloudELBConfig.tsx
@@ -26,7 +26,9 @@ const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormHuaweiCloudELBConfigFieldValues => {
- return {};
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
};
const DeployNodeConfigFormHuaweiCloudELBConfig = ({
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx
index 22c5bf08..f9cfd937 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormJDCloudALBConfig.tsx
@@ -26,7 +26,9 @@ const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const;
const RESOURCE_TYPE_LISTENER = "listener" as const;
const initFormModel = (): DeployNodeConfigFormJDCloudALBConfigFieldValues => {
- return {};
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ };
};
const DeployNodeConfigFormJDCloudALBConfig = ({
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
index 49110ce9..aa528b2e 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormSSHConfig.tsx
@@ -46,12 +46,13 @@ const initFormModel = (): DeployNodeConfigFormSSHConfigFieldValues => {
};
const initPresetScript = (
- key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl",
+ key: Parameters[0] | "sh_replace_synologydsm_ssl" | "sh_replace_fnos_ssl" | "sh_replace_qnap_ssl",
params?: Parameters[1]
) => {
switch (key) {
case "sh_replace_synologydsm_ssl":
return `# *** 需要 root 权限 ***
+# 注意仅支持替换证书,需本身已开启过一次 HTTPS
# 脚本参考 https://github.com/catchdave/ssl-certs/blob/main/replace_synology_ssl_certs.sh
# 请将以下变量替换为实际值
@@ -129,6 +130,7 @@ info "Completed"
case "sh_replace_fnos_ssl":
return `# *** 需要 root 权限 ***
+# 注意仅支持替换证书,需本身已开启过一次 HTTPS
# 脚本参考 https://github.com/lfgyx/fnos_certificate_update/blob/main/src/update_cert.sh
# 请将以下变量替换为实际值
@@ -145,9 +147,9 @@ $domain = "" # 域名
cp -rf "$tmpFullchainPath" "$fnFullchainPath"
cp -rf "$tmpCertPath" "$fnCertPath"
cp -rf "$tmpKeyPath" "$fnKeyPath"
+chmod 755 "$fnFullchainPath"
chmod 755 "$fnCertPath"
chmod 755 "$fnKeyPath"
-chmod 755 "$fnFullchainPath"
# 更新数据库
NEW_EXPIRY_DATE=$(openssl x509 -enddate -noout -in "$fnCertPath" | sed "s/^.*=\\(.*\\)$/\\1/")
@@ -159,6 +161,28 @@ systemctl restart webdav.service
systemctl restart smbftpd.service
systemctl restart trim_nginx.service
`.trim();
+
+ case "sh_replace_qnap_ssl":
+ return `# *** 需要 root 权限 ***
+# 注意仅支持替换证书,需本身已开启过一次 HTTPS
+
+# 请将以下变量替换为实际值
+$tmpFullchainPath = "${params?.certPath || ""}" # 证书文件路径(与表单中保持一致)
+$tmpKeyPath = "${params?.keyPath || ""}" # 私钥文件路径(与表单中保持一致)
+
+# 复制文件
+cp -rf "$tmpFullchainPath" /etc/stunnel/backup.cert
+cp -rf "$tmpKeyPath" /etc/stunnel/backup.key
+cat /etc/stunnel/backup.key > /etc/stunnel/stunnel.pem
+cat /etc/stunnel/backup.cert >> /etc/stunnel/stunnel.pem
+chmod 600 /etc/stunnel/backup.cert
+chmod 600 /etc/stunnel/backup.key
+chmod 600 /etc/stunnel/stunnel.pem
+
+# 重启服务
+/etc/init.d/stunnel.sh restart
+/etc/init.d/reverse_proxy.sh reload
+ `.trim();
}
return _initPresetScript(key as Parameters[0], params);
@@ -286,6 +310,7 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
case "sh_replace_synologydsm_ssl":
case "sh_replace_fnos_ssl":
+ case "sh_replace_qnap_ssl":
{
const presetScriptParams = {
certPath: formInst.getFieldValue("certPath"),
@@ -461,13 +486,19 @@ const DeployNodeConfigFormSSHConfig = ({ form: formInst, formName, disabled, ini
({
- key,
- label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
- onClick: () => handlePresetPostScriptClick(key),
- })
- ),
+ items: [
+ "sh_reload_nginx",
+ "sh_replace_synologydsm_ssl",
+ "sh_replace_fnos_ssl",
+ "sh_replace_qnap_ssl",
+ "ps_binding_iis",
+ "ps_binding_netsh",
+ "ps_binding_rdp",
+ ].map((key) => ({
+ key,
+ label: t(`workflow_node.deploy.form.ssh_preset_scripts.option.${key}.label`),
+ onClick: () => handlePresetPostScriptClick(key),
+ })),
}}
trigger={["click"]}
>
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx
index 760c6fac..cd06dbd7 100644
--- a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudCLBConfig.tsx
@@ -29,7 +29,7 @@ const RESOURCE_TYPE_RULEDOMAIN = "ruledomain" as const;
const initFormModel = (): DeployNodeConfigFormTencentCloudCLBConfigFieldValues => {
return {
- resourceType: RESOURCE_TYPE_VIA_SSLDEPLOY,
+ resourceType: RESOURCE_TYPE_LISTENER,
};
};
diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx
new file mode 100644
index 00000000..1f443cdf
--- /dev/null
+++ b/ui/src/components/workflow/node/DeployNodeConfigFormTencentCloudGAAPConfig.tsx
@@ -0,0 +1,100 @@
+import { useTranslation } from "react-i18next";
+import { Form, type FormInstance, Input, Select } from "antd";
+import { createSchemaFieldRule } from "antd-zod";
+import { z } from "zod";
+
+import Show from "@/components/Show";
+
+type DeployNodeConfigFormTencentCloudGAAPConfigFieldValues = Nullish<{
+ resourceType: string;
+ proxyId?: string;
+ listenerId?: string;
+}>;
+
+export type DeployNodeConfigFormTencentCloudGAAPConfigProps = {
+ form: FormInstance;
+ formName: string;
+ disabled?: boolean;
+ initialValues?: DeployNodeConfigFormTencentCloudGAAPConfigFieldValues;
+ onValuesChange?: (values: DeployNodeConfigFormTencentCloudGAAPConfigFieldValues) => void;
+};
+
+const RESOURCE_TYPE_LISTENER = "listener" as const;
+
+const initFormModel = (): DeployNodeConfigFormTencentCloudGAAPConfigFieldValues => {
+ return {
+ resourceType: RESOURCE_TYPE_LISTENER,
+ listenerId: "",
+ };
+};
+
+const DeployNodeConfigFormTencentCloudGAAPConfig = ({
+ form: formInst,
+ formName,
+ disabled,
+ initialValues,
+ onValuesChange,
+}: DeployNodeConfigFormTencentCloudGAAPConfigProps) => {
+ const { t } = useTranslation();
+
+ const formSchema = z.object({
+ resourceType: z.literal(RESOURCE_TYPE_LISTENER, { message: t("workflow_node.deploy.form.tencentcloud_gaap_resource_type.placeholder") }),
+ proxyId: z.string().trim().nullish(),
+ listenerId: z
+ .string()
+ .trim()
+ .nullish()
+ .refine(
+ (v) => ![RESOURCE_TYPE_LISTENER].includes(fieldResourceType) || !!v?.trim(),
+ t("workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder")
+ ),
+ });
+ const formRule = createSchemaFieldRule(formSchema);
+
+ const fieldResourceType = Form.useWatch("resourceType", formInst);
+
+ const handleFormChange = (_: unknown, values: z.infer) => {
+ onValuesChange?.(values);
+ };
+
+ return (
+
+
+
+ {t("workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label")}
+
+
+
+
+ }
+ >
+
+
+
+
+ }
+ >
+
+
+
+
+ );
+};
+
+export default DeployNodeConfigFormTencentCloudGAAPConfig;
diff --git a/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx b/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx
index b6bfed17..133ee7e4 100644
--- a/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx
+++ b/ui/src/components/workflow/node/NotifyNodeConfigFormEmailConfig.tsx
@@ -33,6 +33,7 @@ const NotifyNodeConfigFormEmailConfig = ({ form: formInst, formName, disabled, i
if (!v) return true;
return validEmailAddress(v);
}, t("common.errmsg.email_invalid")),
+ senderName: z.string().nullish(),
receiverAddress: z
.string()
.nullish()
@@ -65,6 +66,15 @@ const NotifyNodeConfigFormEmailConfig = ({ form: formInst, formName, disabled, i
+ }
+ >
+
+
+
[
type,
diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json
index 23946f6d..27ada05c 100644
--- a/ui/src/i18n/locales/en/nls.access.json
+++ b/ui/src/i18n/locales/en/nls.access.json
@@ -1,15 +1,15 @@
{
"access.page.title": "Authorization",
- "access.nodata": "No accesses. Please create an authorization first.",
+ "access.nodata": "No accesses. Please create an credential first.",
"access.search.placeholder": "Search by access name ...",
- "access.action.add": "Create authorization",
- "access.action.edit": "Edit authorization",
- "access.action.duplicate": "Duplicate authorization",
- "access.action.delete": "Delete authorization",
- "access.action.delete.confirm": "Are you sure to delete this authorization?",
+ "access.action.add": "Create credential",
+ "access.action.edit": "Edit credential",
+ "access.action.duplicate": "Duplicate credential",
+ "access.action.delete": "Delete credential",
+ "access.action.delete.confirm": "Are you sure to delete this credential?",
"access.props.name": "Name",
"access.props.provider": "Provider",
@@ -25,7 +25,7 @@
"access.props.updated_at": "Updated at",
"access.form.name.label": "Name",
- "access.form.name.placeholder": "Please enter authorization name",
+ "access.form.name.placeholder": "Please enter credential name",
"access.form.provider.label": "Provider",
"access.form.provider.placeholder": "Please select a provider",
"access.form.provider.tooltip": "DNS provider: The provider that hosts your domain names and manages your DNS records. Hosting provider: The provider that hosts your servers or cloud services for deploying certificates.Cannot be edited after saving. ",
@@ -72,6 +72,11 @@
"access.form.aliyun_resource_group_id.label": "Aliyun resource group ID (Optional)",
"access.form.aliyun_resource_group_id.placeholder": "Please enter Aliyun resource group ID",
"access.form.aliyun_resource_group_id.tooltip": "For more information, see https://www.alibabacloud.com/help/en/resource-management/product-overview ",
+ "access.form.apisix_server_url.label": "APISIX server URL",
+ "access.form.apisix_server_url.placeholder": "Please enter APISIX server URL",
+ "access.form.apisix_api_key.label": "APISIX Admin API key",
+ "access.form.apisix_api_key.placeholder": "Please enter APISIX Admin API key",
+ "access.form.apisix_api_key.tooltip": "For more information, see https://apisix.apache.org/docs/apisix/admin-api/ ",
"access.form.aws_access_key_id.label": "AWS AccessKeyId",
"access.form.aws_access_key_id.placeholder": "Please enter AWS AccessKeyId",
"access.form.aws_access_key_id.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/IAM/latest/UserGuide/id_credentials_access-keys.html ",
@@ -152,6 +157,12 @@
"access.form.constellix_secret_key.label": "Constellix API secret key",
"access.form.constellix_secret_key.placeholder": "Please enter Constellix API secret key",
"access.form.constellix_secret_key.tooltip": "For more information, see https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key ",
+ "access.form.ctcccloud_access_key_id.label": "CTCC StateCloud AccessKeyId",
+ "access.form.ctcccloud_access_key_id.placeholder": "Please enter CTCC StateCloud AccessKeyId",
+ "access.form.ctcccloud_access_key_id.tooltip": "For more information, see https://www.ctyun.cn/document/10015882/10015953 ",
+ "access.form.ctcccloud_secret_access_key.label": "CTCC StateCloud SecretAccessKey",
+ "access.form.ctcccloud_secret_access_key.placeholder": "Please enter CTCC StateCloud SecretAccessKey",
+ "access.form.ctcccloud_secret_access_key.tooltip": "For more information, see https://www.ctyun.cn/document/10015882/10015953 ",
"access.form.desec_token.label": "deSEC token",
"access.form.desec_token.placeholder": "Please enter deSEC token",
"access.form.desec_token.tooltip": "For more information, see https://desec.readthedocs.io/en/latest/auth/tokens.html ",
@@ -205,6 +216,8 @@
"access.form.email_password.placeholder": "please enter password",
"access.form.email_default_sender_address.label": "Default sender email address (Optional)",
"access.form.email_default_sender_address.placeholder": "Please enter default sender email address",
+ "access.form.email_default_sender_name.label": "Default sender display name (Optional)",
+ "access.form.email_default_sender_name.placeholder": "Please enter default sender display name",
"access.form.email_default_receiver_address.label": "Default receiver email address (Optional)",
"access.form.email_default_receiver_address.placeholder": "Please enter default receiver email address",
"access.form.flexcdn_server_url.label": "FlexCDN server URL",
diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json
index bac03fee..ddf408d9 100644
--- a/ui/src/i18n/locales/en/nls.provider.json
+++ b/ui/src/i18n/locales/en/nls.provider.json
@@ -13,7 +13,7 @@
"provider.aliyun.clb": "Alibaba Cloud - CLB (Classic Load Balancer)",
"provider.aliyun.dcdn": "Alibaba Cloud - DCDN (Dynamic Route for Content Delivery Network)",
"provider.aliyun.ddos": "Alibaba Cloud - Anti-DDoS Proxy",
- "provider.aliyun.dns": "Alibaba Cloud - DNS (Domain Name Service)",
+ "provider.aliyun.dns": "Alibaba Cloud - DNS",
"provider.aliyun.esa": "Alibaba Cloud - ESA (Edge Security Acceleration)",
"provider.aliyun.fc": "Alibaba Cloud - FC (Function Compute)",
"provider.aliyun.ga": "Alibaba Cloud - GA (Global Accelerator)",
@@ -24,6 +24,7 @@
"provider.aliyun.waf": "Alibaba Cloud - WAF (Web Application Firewall)",
"provider.akamai": "Akamai",
"provider.akamai.cdn": "Akamai - CDN (Content Delivery Network)",
+ "provider.apisix": "Apache APISIX",
"provider.aws": "AWS",
"provider.aws.acm": "AWS - ACM (Amazon Certificate Manager)",
"provider.aws.cloudfront": "AWS - CloudFront",
@@ -37,7 +38,7 @@
"provider.baiducloud.blb": "Baidu Cloud - BLB (Load Balancer)",
"provider.baiducloud.cdn": "Baidu Cloud - CDN (Content Delivery Network)",
"provider.baiducloud.cert_upload": "Baidu Cloud - Upload to SSL Certificate Service",
- "provider.baiducloud.dns": "Baidu Cloud - DNS (Domain Name Service)",
+ "provider.baiducloud.dns": "Baidu Cloud - DNS",
"provider.baishan": "Baishan",
"provider.baishan.cdn": "Baishan - CDN (Content Delivery Network)",
"provider.baotapanel": "aaPanel (aka BaoTaPanel)",
@@ -55,9 +56,17 @@
"provider.cdnfly": "Cdnfly",
"provider.cloudflare": "Cloudflare",
"provider.cloudns": "ClouDNS",
- "provider.cmcccloud": "China Mobile Cloud (ECloud)",
+ "provider.cmcccloud": "China Mobile ECloud",
+ "provider.cmcccloud.dns": "China Mobile ECloud - DNS",
"provider.constellix": "Constellix",
- "provider.ctcccloud": "China Telecom Cloud (State Cloud)",
+ "provider.ctcccloud": "China Telecom StateCloud",
+ "provider.ctcccloud.ao": "China Telecom StateCloud - AccessOne",
+ "provider.ctcccloud.cdn": "China Telecom StateCloud - CDN (Content Delivery Network)",
+ "provider.ctcccloud.cms_upload": "China Telecom StateCloud - Upload to Certificate Management Service",
+ "provider.ctcccloud.elb": "China Telecom StateCloud - ELB (Elastic Load Balancing)",
+ "provider.ctcccloud.icdn": "China Telecom StateCloud - ICDN (Integrated Content Delivery Network)",
+ "provider.ctcccloud.lvdn": "China Telecom StateCloud - LVDN (Live Video Delivery Network)",
+ "provider.ctcccloud.smartdns": "China Telecom StateCloud - Smart DNS",
"provider.cucccloud": "China Unicom Cloud",
"provider.desec": "deSEC",
"provider.digitalocean": "DigitalOcean",
@@ -82,7 +91,7 @@
"provider.hetzner": "Hetzner",
"provider.huaweicloud": "Huawei Cloud",
"provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)",
- "provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)",
+ "provider.huaweicloud.dns": "Huawei Cloud - DNS",
"provider.huaweicloud.elb": "Huawei Cloud - ELB (Elastic Load Balance)",
"provider.huaweicloud.scm_upload": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)",
"provider.huaweicloud.waf": "Huawei Cloud - WAF (Web Application Firewall)",
@@ -129,9 +138,10 @@
"provider.tencentcloud.clb": "Tencent Cloud - CLB (Cloud Load Balancer)",
"provider.tencentcloud.cos": "Tencent Cloud - COS (Cloud Object Storage)",
"provider.tencentcloud.css": "Tencent Cloud - CSS (Cloud Streaming Service)",
- "provider.tencentcloud.dns": "Tencent Cloud - DNS (Domain Name Service)",
+ "provider.tencentcloud.dns": "Tencent Cloud - DNS",
"provider.tencentcloud.ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)",
"provider.tencentcloud.eo": "Tencent Cloud - EdgeOne",
+ "provider.tencentcloud.gaap": "Tencent Cloud - GAAP (Global Application Acceleration Platform)",
"provider.tencentcloud.scf": "Tencent Cloud - SCF (Serverless Cloud Function)",
"provider.tencentcloud.ssl_upload": "Tencent Cloud - Upload to SSL Certificate Service",
"provider.tencentcloud.ssl_deploy": "Tencent Cloud - Deploy via SSL Certificate Service",
@@ -153,7 +163,7 @@
"provider.volcengine.certcenter_upload": "Volcengine - Upload to Certificate Center",
"provider.volcengine.clb": "Volcengine - CLB (Cloud Load Balancer)",
"provider.volcengine.dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)",
- "provider.volcengine.dns": "Volcengine - DNS (Domain Name Service)",
+ "provider.volcengine.dns": "Volcengine - DNS",
"provider.volcengine.imagex": "Volcengine - ImageX",
"provider.volcengine.live": "Volcengine - Live",
"provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)",
@@ -172,6 +182,7 @@
"provider.category.loadbalance": "Loadbalance",
"provider.category.firewall": "Firewall",
"provider.category.av": "Audio/Video",
+ "provider.category.accelerator": "Accelerator",
"provider.category.apigw": "API Gateway",
"provider.category.serverless": "Serverless",
"provider.category.website": "Website",
diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json
index c77dcd40..561ce084 100644
--- a/ui/src/i18n/locales/en/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json
@@ -39,8 +39,8 @@
"workflow_node.apply.form.challenge_type.placeholder": "Please select challenge type",
"workflow_node.apply.form.provider.label": "DNS provider",
"workflow_node.apply.form.provider.placeholder": "Please select DNS provider of the domains",
- "workflow_node.apply.form.provider_access.label": "DNS provider authorization",
- "workflow_node.apply.form.provider_access.placeholder": "Please select an authorization of DNS provider",
+ "workflow_node.apply.form.provider_access.label": "DNS provider credential",
+ "workflow_node.apply.form.provider_access.placeholder": "Please select an credential of DNS provider",
"workflow_node.apply.form.provider_access.tooltip": "Used to manage DNS records during ACME DNS-01 challenge.",
"workflow_node.apply.form.provider_access.button": "Create",
"workflow_node.apply.form.aliyun_esa_region.label": "Alibaba Cloud ESA region",
@@ -66,8 +66,8 @@
"workflow_node.apply.form.ca_provider.placeholder": "Please select a certificate authority",
"workflow_node.apply.form.ca_provider.tooltip": "Used to issue SSL certificates.",
"workflow_node.apply.form.ca_provider.button": "Configure",
- "workflow_node.apply.form.ca_provider_access.label": "Certificate authority authorization",
- "workflow_node.apply.form.ca_provider_access.placeholder": "Please select an authorization of the certificate authority",
+ "workflow_node.apply.form.ca_provider_access.label": "Certificate authority credential",
+ "workflow_node.apply.form.ca_provider_access.placeholder": "Please select an credential of the certificate authority",
"workflow_node.apply.form.ca_provider_access.button": "Create",
"workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm",
"workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm",
@@ -115,8 +115,8 @@
"workflow_node.deploy.form.provider.label": "Deploy target",
"workflow_node.deploy.form.provider.placeholder": "Please select deploy target",
"workflow_node.deploy.form.provider.search.placeholder": "Search deploy target ...",
- "workflow_node.deploy.form.provider_access.label": "Hosting provider authorization",
- "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of Hosting provider",
+ "workflow_node.deploy.form.provider_access.label": "Hosting provider credential",
+ "workflow_node.deploy.form.provider_access.placeholder": "Please select an credential of Hosting provider",
"workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.",
"workflow_node.deploy.form.provider_access.button": "Create",
"workflow_node.deploy.form.certificate.label": "Certificate",
@@ -130,10 +130,10 @@
"workflow_node.deploy.form.1panel_site_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.1panel_site_website_id.label": "1Panel website ID",
"workflow_node.deploy.form.1panel_site_website_id.placeholder": "Please enter 1Panel website ID",
- "workflow_node.deploy.form.1panel_site_website_id.tooltip": "You can find it on 1Panel WebUI.",
+ "workflow_node.deploy.form.1panel_site_website_id.tooltip": "You can find it on 1Panel dashboard.",
"workflow_node.deploy.form.1panel_site_certificate_id.label": "1Panel certificate ID",
"workflow_node.deploy.form.1panel_site_certificate_id.placeholder": "Please enter 1Panel certificate ID",
- "workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "You can find it on 1Panel WebUI.",
+ "workflow_node.deploy.form.1panel_site_certificate_id.tooltip": "You can find it on 1Panel dashboard.",
"workflow_node.deploy.form.aliyun_alb_resource_type.label": "Resource type",
"workflow_node.deploy.form.aliyun_alb_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.aliyun_alb_resource_type.option.loadbalancer.label": "ALB load balancer",
@@ -285,6 +285,12 @@
"workflow_node.deploy.form.aliyun_waf_domain.label": "Alibaba Cloud WAF domain (Optional)",
"workflow_node.deploy.form.aliyun_waf_domain.placeholder": "Please enter Alibaba Cloud WAF domain name",
"workflow_node.deploy.form.aliyun_waf_domain.tooltip": "For more information, see https://waf.console.aliyun.com ",
+ "workflow_node.deploy.form.apisix_resource_type.label": "Resource type",
+ "workflow_node.deploy.form.apisix_resource_type.placeholder": "Please select resource type",
+ "workflow_node.deploy.form.apisix_resource_type.option.certificate.label": "SSL certificate",
+ "workflow_node.deploy.form.apisix_certificate_id.label": "APISIX certificate ID",
+ "workflow_node.deploy.form.apisix_certificate_id.placeholder": "Please enter APISIX certificate ID",
+ "workflow_node.deploy.form.apisix_certificate_id.tooltip": "You can find it on APISIX dashboard.",
"workflow_node.deploy.form.aws_acm_region.label": "AWS ACM Region",
"workflow_node.deploy.form.aws_acm_region.placeholder": "Please enter AWS ACM region (e.g. us-east-1)",
"workflow_node.deploy.form.aws_acm_region.tooltip": "For more information, see https://docs.aws.amazon.com/en_us/general/latest/gr/rande.html#regional-endpoints ",
@@ -361,16 +367,16 @@
"workflow_node.deploy.form.baotapanel_site_type.option.other.label": "Other sites",
"workflow_node.deploy.form.baotapanel_site_name.label": "aaPanel site name",
"workflow_node.deploy.form.baotapanel_site_name.placeholder": "Please enter aaPanel site name",
- "workflow_node.deploy.form.baotapanel_site_name.tooltip": "You can find it on aaPanel WebUI.",
+ "workflow_node.deploy.form.baotapanel_site_name.tooltip": "You can find it on aaPanel dashboard.",
"workflow_node.deploy.form.baotapanel_site_names.label": "aaPanel site names",
"workflow_node.deploy.form.baotapanel_site_names.placeholder": "Please enter aaPanel site names (separated by semicolons)",
"workflow_node.deploy.form.baotapanel_site_names.errmsg.invalid": "Please enter a valid aaPanel site name",
- "workflow_node.deploy.form.baotapanel_site_names.tooltip": "You can find it on aaPanel WebUI.",
+ "workflow_node.deploy.form.baotapanel_site_names.tooltip": "You can find it on aaPanel dashboard.",
"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.title": "Change aaPanel site names",
"workflow_node.deploy.form.baotapanel_site_names.multiple_input_modal.placeholder": "Please enter aaPanel site name",
"workflow_node.deploy.form.baotawaf_site_name.label": "aaWAF site name",
"workflow_node.deploy.form.baotawaf_site_name.placeholder": "Please enter aaWAF site name",
- "workflow_node.deploy.form.baotawaf_site_name.tooltip": "You can find it on aaWAF WebUI.",
+ "workflow_node.deploy.form.baotawaf_site_name.tooltip": "You can find it on aaWAF dashboard.",
"workflow_node.deploy.form.baotawaf_site_port.label": "aaWAF site SSL port",
"workflow_node.deploy.form.baotawaf_site_port.placeholder": "Please enter aaWAF SSL port",
"workflow_node.deploy.form.bunny_cdn_pull_zone_id.label": "Bunny CDN pull zone ID",
@@ -388,13 +394,39 @@
"workflow_node.deploy.form.cdnfly_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.cdnfly_site_id.label": "Cdnfly site ID",
"workflow_node.deploy.form.cdnfly_site_id.placeholder": "Please enter Cdnfly site ID",
- "workflow_node.deploy.form.cdnfly_site_id.tooltip": "You can find it on Cdnfly WebUI.",
+ "workflow_node.deploy.form.cdnfly_site_id.tooltip": "You can find it on Cdnfly dashboard.",
"workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly certificate ID",
"workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "Please enter Cdnfly certificate ID",
- "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "You can find it on Cdnfly WebUI.",
+ "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "You can find it on Cdnfly dashboard.",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.label": "CTCC StateCloud AccessOne domain",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.placeholder": "Please enter CTCC StateCloud AccessOne domain name",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.tooltip": "For more information, see https://cdn.ctyun.cn/h5/ctaccessone/ ",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.label": "CTCC StateCloud CDN domain",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder": "Please enter CTCC StateCloud CDN domain name",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.tooltip": "For more information, see https://cdn-console.ctyun.cn ",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.label": "Resource type",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.placeholder": "Please select resource type",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.option.certificate.label": "ELB certificate",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.option.loadbalancer.label": "ELB load balancer",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.option.listener.label": "ELB listener",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.label": "CTCC StateCloud ELB region ID",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder": "Please enter CTCC StateCloud ELB region ID",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.tooltip": "For more information, see https://www.ctyun.cn/document/10026755/10196575 ",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.label": "CTCC StateCloud ELB load balancer ID",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder": "Please enter CTCC StateCloud ELB load balancer ID",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.tooltip": "For more information, see https://console.ctyun.cn/network/index/#/elb/elbList ",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.label": "CTCC StateCloud ELB listener ID",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder": "Please enter CTCC StateCloud ELB listener ID",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.tooltip": "For more information, see https://console.ctyun.cn/network/index/#/elb/elbList ",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.label": "CTCC StateCloud ICDN domain",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder": "Please enter CTCC StateCloud ICDN domain name",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.tooltip": "For more information, see https://cdn-console.ctyun.cn ",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.label": "CTCC StateCloud LVDN domain",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder": "Please enter CTCC StateCloud LVDN domain name",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.tooltip": "For more information, see https://cdn.ctyun.cn/h5/live/index ",
"workflow_node.deploy.form.dogecloud_cdn_domain.label": "Doge Cloud CDN domain",
"workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "Please enter Doge Cloud CDN domain name",
- "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "For more information, see https://console.dogecloud.com/ ",
+ "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "For more information, see https://console.dogecloud.com ",
"workflow_node.deploy.form.edgio_applications_environment_id.label": "Edgio Applications environment ID",
"workflow_node.deploy.form.edgio_applications_environment_id.placeholder": "Please enter Edgio Applications environment ID",
"workflow_node.deploy.form.edgio_applications_environment_id.tooltip": "For more information, see https://edgio.app/ ",
@@ -403,7 +435,7 @@
"workflow_node.deploy.form.flexcdn_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.flexcdn_certificate_id.label": "FlexCDN certificate ID",
"workflow_node.deploy.form.flexcdn_certificate_id.placeholder": "Please enter FlexCDN certificate ID",
- "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "You can find it on FlexCDN WebUI.",
+ "workflow_node.deploy.form.flexcdn_certificate_id.tooltip": "You can find it on FlexCDN dashboard.",
"workflow_node.deploy.form.gcore_cdn_resource_id.label": "Gcore CDN resource ID",
"workflow_node.deploy.form.gcore_cdn_resource_id.placeholder": "Please enter Gcore CDN resource ID",
"workflow_node.deploy.form.gcore_cdn_resource_id.tooltip": "For more information, see https://cdn.gcore.com/resources/list ",
@@ -415,7 +447,7 @@
"workflow_node.deploy.form.goedge_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.goedge_certificate_id.label": "GoEdge certificate ID",
"workflow_node.deploy.form.goedge_certificate_id.placeholder": "Please enter GoEdge certificate ID",
- "workflow_node.deploy.form.goedge_certificate_id.tooltip": "You can find it on GoEdge WebUI.",
+ "workflow_node.deploy.form.goedge_certificate_id.tooltip": "You can find it on GoEdge dashboard.",
"workflow_node.deploy.form.huaweicloud_cdn_region.label": "Huawei Cloud CDN region",
"workflow_node.deploy.form.huaweicloud_cdn_region.placeholder": "Please enter Huawei Cloud CDN region (e.g. cn-north-1)",
"workflow_node.deploy.form.huaweicloud_cdn_region.tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint ",
@@ -471,13 +503,13 @@
"workflow_node.deploy.form.jdcloud_alb_snidomain.tooltip": "For more information, see https://cns-console.jdcloud.com/host/loadBalance/list ",
"workflow_node.deploy.form.jdcloud_cdn_domain.label": "JD Cloud CDN domain",
"workflow_node.deploy.form.jdcloud_cdn_domain.placeholder": "Please enter JD Cloud CDN domain name",
- "workflow_node.deploy.form.jdcloud_cdn_domain.tooltip": "For more information, see https://cdn-console.jdcloud.com/ ",
+ "workflow_node.deploy.form.jdcloud_cdn_domain.tooltip": "For more information, see https://cdn-console.jdcloud.com ",
"workflow_node.deploy.form.jdcloud_live_domain.label": "JD Cloud Live Video play domain",
"workflow_node.deploy.form.jdcloud_live_domain.placeholder": "Please enter JD Cloud Live Video play domain name",
- "workflow_node.deploy.form.jdcloud_live_domain.tooltip": "For more information, see https://live-console.jdcloud.com/ ",
+ "workflow_node.deploy.form.jdcloud_live_domain.tooltip": "For more information, see https://live-console.jdcloud.com ",
"workflow_node.deploy.form.jdcloud_vod_domain.label": "JD Cloud VOD domain",
"workflow_node.deploy.form.jdcloud_vod_domain.placeholder": "Please enter JD Cloud VOD domain name",
- "workflow_node.deploy.form.jdcloud_vod_domain.tooltip": "For more information, see https://vod-console.jdcloud.com/ ",
+ "workflow_node.deploy.form.jdcloud_vod_domain.tooltip": "For more information, see https://vod-console.jdcloud.com ",
"workflow_node.deploy.form.k8s_namespace.label": "Kubernetes Namespace",
"workflow_node.deploy.form.k8s_namespace.placeholder": "Please enter Kubernetes Namespace",
"workflow_node.deploy.form.k8s_namespace.tooltip": "For more information, see https://kubernetes.io/docs/concepts/overview/working-with-objects/namespaces/ ",
@@ -498,10 +530,10 @@
"workflow_node.deploy.form.lecdn_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.lecdn_certificate_id.label": "LeCDN certificate ID",
"workflow_node.deploy.form.lecdn_certificate_id.placeholder": "Please enter LeCDN certificate ID",
- "workflow_node.deploy.form.lecdn_certificate_id.tooltip": "You can find it on LeCDN WebUI.",
+ "workflow_node.deploy.form.lecdn_certificate_id.tooltip": "You can find it on LeCDN dashboard.",
"workflow_node.deploy.form.lecdn_client_id.label": "LeCDN user ID (Optional)",
"workflow_node.deploy.form.lecdn_client_id.placeholder": "Please enter LeCDN user ID",
- "workflow_node.deploy.form.lecdn_client_id.tooltip": "You can find it on LeCDN WebUI. Required when using administrator's authorization. It Must be the same as the user to which the certificate belongs.",
+ "workflow_node.deploy.form.lecdn_client_id.tooltip": "You can find it on LeCDN dashboard. Required when using administrator's authorization. It Must be the same as the user to which the certificate belongs.",
"workflow_node.deploy.form.local.guide": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.",
"workflow_node.deploy.form.local_format.label": "File format",
"workflow_node.deploy.form.local_format.placeholder": "Please select file format",
@@ -574,13 +606,13 @@
"workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "For more information, see https://app.rainyun.com/apps/rcdn/list ",
"workflow_node.deploy.form.ratpanel_site_name.label": "RatPanel site name",
"workflow_node.deploy.form.ratpanel_site_name.placeholder": "Please enter RatPanel site name",
- "workflow_node.deploy.form.ratpanel_site_name.tooltip": "You can find it on RatPanel WebUI.",
+ "workflow_node.deploy.form.ratpanel_site_name.tooltip": "You can find it on RatPanel dashboard.",
"workflow_node.deploy.form.safeline_resource_type.label": "Resource type",
"workflow_node.deploy.form.safeline_resource_type.placeholder": "Please select resource type",
"workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate",
"workflow_node.deploy.form.safeline_certificate_id.label": "SafeLine certificate ID",
"workflow_node.deploy.form.safeline_certificate_id.placeholder": "Please enter SafeLine certificate ID",
- "workflow_node.deploy.form.safeline_certificate_id.tooltip": "You can find it on SafeLine WebUI.",
+ "workflow_node.deploy.form.safeline_certificate_id.tooltip": "You can find it on SafeLine dashboard.",
"workflow_node.deploy.form.ssh_format.label": "File format",
"workflow_node.deploy.form.ssh_format.placeholder": "Please select file format",
"workflow_node.deploy.form.ssh_format.option.pem.label": "PEM (*.pem, *.crt, *.key)",
@@ -620,6 +652,7 @@
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - Reload nginx",
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_synologydsm_ssl.label": "POSIX Bash - Replace SynologyDSM SSL certificate",
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_fnos_ssl.label": "POSIX Bash - Replace fnOS SSL certificate",
+ "workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_qnap_ssl.label": "POSIX Bash - Replace QNAP SSL certificate",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - Binding IIS",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - Binding netsh",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - Binding RDP",
@@ -670,6 +703,15 @@
"workflow_node.deploy.form.tencentcloud_eo_domain.label": "Tencent Cloud EdgeOne domain",
"workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "Please enter Tencent Cloud EdgeOne domain name",
"workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "For more information, see https://console.tencentcloud.com/edgeone ",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.label": "Resource type",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.placeholder": "Please select resource type",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label": "GAAP listener",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.label": "Tencent Cloud GAAP proxy ID (Optional)",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.placeholder": "Please enter Tencent Cloud GAAP proxy ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.tooltip": "For more information, see https://console.cloud.tencent.com/gaap ",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.label": "Tencent Cloud GAAP listener ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder": "Please enter Tencent Cloud GAAP listener ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip": "For more information, see https://console.cloud.tencent.com/gaap ",
"workflow_node.deploy.form.tencentcloud_scf_region.label": "Tencent Cloud SCF region",
"workflow_node.deploy.form.tencentcloud_scf_region.placeholder": "Please enter Tencent Cloud SCF region (e.g. ap-guangzhou)",
"workflow_node.deploy.form.tencentcloud_scf_region.tooltip": "For more information, see https://www.tencentcloud.com/document/product/583/17299 ",
@@ -818,7 +860,7 @@
"workflow_node.deploy.form.wangsu_certificate_id.tooltip": "For more information, see https://cdn.console.wangsu.com/v2/index#/certificate/list ",
"workflow_node.deploy.form.webhook_data.label": "Webhook data (Optional)",
"workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data to override the default value",
- "workflow_node.deploy.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
+ "workflow_node.deploy.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the credential.",
"workflow_node.deploy.form.webhook_data.guide": "Supported variables: ${DOMAIN} : The primary domain of the certificate (CommonName ).${DOMAINS} : The domain list of the certificate (SubjectAltNames ).${CERTIFICATE} : The PEM format content of the certificate file.${SERVER_CERTIFICATE} : The PEM format content of the server certificate file.${INTERMEDIA_CERTIFICATE} : The PEM format content of the intermediate CA certificate file.${PRIVATE_KEY} : The PEM format content of the private key file. Please visit the authorization management page for addtional notes.",
"workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
"workflow_node.deploy.form.strategy_config.label": "Strategy settings",
@@ -851,31 +893,34 @@
"workflow_node.notify.form.channel.button": "Configure",
"workflow_node.notify.form.provider.label": "Notification channel",
"workflow_node.notify.form.provider.placeholder": "Please select notification channel",
- "workflow_node.notify.form.provider_access.label": "Notification provider authorization",
- "workflow_node.notify.form.provider_access.placeholder": "Please select an authorization of notification provider",
+ "workflow_node.notify.form.provider_access.label": "Notification provider credential",
+ "workflow_node.notify.form.provider_access.placeholder": "Please select an credential of notification provider",
"workflow_node.notify.form.provider_access.button": "Create",
"workflow_node.notify.form.params_config.label": "Parameter settings",
"workflow_node.notify.form.discordbot_channel_id.label": "Discord channel ID (Optional)",
"workflow_node.notify.form.discordbot_channel_id.placeholder": "Please enter Discord channel ID to override the default value",
- "workflow_node.notify.form.discordbot_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the authorization.",
+ "workflow_node.notify.form.discordbot_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the credential.",
"workflow_node.notify.form.email_sender_address.label": "Sender email address (Optional)",
"workflow_node.notify.form.email_sender_address.placeholder": "Please enter sender email address to override the default value",
- "workflow_node.notify.form.email_sender_address.tooltip": "Leave it blank to use the default sender email address provided by the authorization.",
+ "workflow_node.notify.form.email_sender_address.tooltip": "Leave it blank to use the default sender email address provided by the credential.",
+ "workflow_node.notify.form.email_sender_name.label": "Sender display name (Optional)",
+ "workflow_node.notify.form.email_sender_name.placeholder": "Please enter sender display name to override the default value",
+ "workflow_node.notify.form.email_sender_name.tooltip": "Leave it blank to use the default sender display name provided by the credential.",
"workflow_node.notify.form.email_receiver_address.label": "Receiver email address (Optional)",
"workflow_node.notify.form.email_receiver_address.placeholder": "Please enter receiver email address to override the default value",
- "workflow_node.notify.form.email_receiver_address.tooltip": "Leave it blank to use the default receiver email address provided by the selected authorization.",
+ "workflow_node.notify.form.email_receiver_address.tooltip": "Leave it blank to use the default receiver email address provided by the selected credential.",
"workflow_node.notify.form.mattermost_channel_id.label": "Mattermost channel ID (Optional)",
"workflow_node.notify.form.mattermost_channel_id.placeholder": "Please enter Mattermost channel ID to override the default value",
- "workflow_node.notify.form.mattermost_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the authorization.",
+ "workflow_node.notify.form.mattermost_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the credential.",
"workflow_node.notify.form.slackbot_channel_id.label": "Slack channel ID (Optional)",
"workflow_node.notify.form.slackbot_channel_id.placeholder": "Please enter Slack channel ID to override the default value",
- "workflow_node.notify.form.slackbot_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the authorization.",
+ "workflow_node.notify.form.slackbot_channel_id.tooltip": "Leave it blank to use the default channel ID provided by the credential.",
"workflow_node.notify.form.telegrambot_chat_id.label": "Telegram chat ID (Optional)",
"workflow_node.notify.form.telegrambot_chat_id.placeholder": "Please enter Telegram chat ID to override the default value",
- "workflow_node.notify.form.telegrambot_chat_id.tooltip": "Leave it blank to use the default chat ID provided by the selected authorization.",
+ "workflow_node.notify.form.telegrambot_chat_id.tooltip": "Leave it blank to use the default chat ID provided by the selected credential.",
"workflow_node.notify.form.webhook_data.label": "Webhook data (Optional)",
"workflow_node.notify.form.webhook_data.placeholder": "Please enter Webhook data to override the default value",
- "workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.",
+ "workflow_node.notify.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the credential.",
"workflow_node.notify.form.webhook_data.guide": "Supported variables: ${SUBJECT} : The subject of notification.${MESSAGE} : The message of notification. Please visit the authorization management page for addtional notes.",
"workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string",
"workflow_node.notify.form.strategy_config.label": "Strategy settings",
diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json
index cbf0c7db..a3852926 100644
--- a/ui/src/i18n/locales/zh/nls.access.json
+++ b/ui/src/i18n/locales/zh/nls.access.json
@@ -72,6 +72,11 @@
"access.form.aliyun_resource_group_id.label": "阿里云资源组 ID(可选)",
"access.form.aliyun_resource_group_id.placeholder": "请输入阿里云资源组 ID",
"access.form.aliyun_resource_group_id.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/resource-management/resource-group/product-overview ",
+ "access.form.apisix_server_url.label": "APISIX 服务地址",
+ "access.form.apisix_server_url.placeholder": "请输入 APISIX 服务地址",
+ "access.form.apisix_api_key.label": "APISIX Admin API Key",
+ "access.form.apisix_api_key.placeholder": "请输入 APISIX Admin API Key",
+ "access.form.apisix_api_key.tooltip": "这是什么?请参阅 https://apisix.apache.org/zh/docs/apisix/admin-api/ ",
"access.form.aws_access_key_id.label": "AWS AccessKeyId",
"access.form.aws_access_key_id.placeholder": "请输入 AWS AccessKeyId",
"access.form.aws_access_key_id.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/IAM/latest/UserGuide/id_credentials_access-keys.html ",
@@ -152,6 +157,12 @@
"access.form.constellix_secret_key.label": "Constellix Secret Key",
"access.form.constellix_secret_key.placeholder": "请输入 Constellix Secret Key",
"access.form.constellix_secret_key.tooltip": "这是什么?请参阅 https://support.constellix.com/hc/en-us/articles/34574197390491-How-to-Generate-an-API-Key ",
+ "access.form.ctcccloud_access_key_id.label": "天翼云 AccessKeyId",
+ "access.form.ctcccloud_access_key_id.placeholder": "请输入天翼云 AccessKeyId",
+ "access.form.ctcccloud_access_key_id.tooltip": "这是什么?请参阅 https://www.ctyun.cn/document/10015882/10015953 ",
+ "access.form.ctcccloud_secret_access_key.label": "天翼云 SecretAccessKey",
+ "access.form.ctcccloud_secret_access_key.placeholder": "请输入天翼云 SecretAccessKey",
+ "access.form.ctcccloud_secret_access_key.tooltip": "这是什么?请参阅 https://www.ctyun.cn/document/10015882/10015953 ",
"access.form.desec_token.label": "deSEC Token",
"access.form.desec_token.placeholder": "请输入 deSEC Token",
"access.form.desec_token.tooltip": "这是什么?请参阅 https://desec.readthedocs.io/en/latest/auth/tokens.html ",
@@ -203,10 +214,12 @@
"access.form.email_username.placeholder": "请输入用户名",
"access.form.email_password.label": "密码",
"access.form.email_password.placeholder": "请输入密码",
- "access.form.email_default_sender_address.label": "默认的发送邮箱地址(可选)",
- "access.form.email_default_sender_address.placeholder": "请输入默认的发送邮箱地址",
- "access.form.email_default_receiver_address.label": "默认的接收邮箱地址(可选)",
- "access.form.email_default_receiver_address.placeholder": "请输入默认的接收邮箱地址",
+ "access.form.email_default_sender_address.label": "默认的发件人邮箱(可选)",
+ "access.form.email_default_sender_address.placeholder": "请输入默认的发件人邮箱",
+ "access.form.email_default_sender_name.label": "默认的发件人名称(可选)",
+ "access.form.email_default_sender_name.placeholder": "请输入默认的发件人名称",
+ "access.form.email_default_receiver_address.label": "默认的收件人邮箱(可选)",
+ "access.form.email_default_receiver_address.placeholder": "请输入默认的收件人邮箱",
"access.form.flexcdn_server_url.label": "FlexCDN 服务地址",
"access.form.flexcdn_server_url.placeholder": "请输入 FlexCDN 服务地址",
"access.form.flexcdn_api_role.label": "FlexCDN 用户角色",
@@ -398,8 +411,8 @@
"access.form.sslcom_eab_kid.label": "ACME EAB KID",
"access.form.sslcom_eab_kid.placeholder": "请输入 ACME EAB KID",
"access.form.sslcom_eab_kid.tooltip": "这是什么?请参阅 https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/ ",
- "access.form.sslcom_eab_hmac_key.label": "ACME EAB HMAC key",
- "access.form.sslcom_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC key",
+ "access.form.sslcom_eab_hmac_key.label": "ACME EAB HMAC Key",
+ "access.form.sslcom_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key",
"access.form.sslcom_eab_hmac_key.tooltip": "这是什么?请参阅 https://www.ssl.com/how-to/generate-acme-credentials-for-reseller-customers/ ",
"access.form.telegrambot_token.label": "Telegram 机器人 API Token",
"access.form.telegrambot_token.placeholder": "请输入 Telegram 机器人 API Token",
diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json
index 79af14fc..865f5b48 100644
--- a/ui/src/i18n/locales/zh/nls.provider.json
+++ b/ui/src/i18n/locales/zh/nls.provider.json
@@ -24,6 +24,7 @@
"provider.aliyun.waf": "阿里云 - Web 应用防火墙 WAF",
"provider.akamai": "Akamai",
"provider.akamai.cdn": "Akamai - 内容分发网络 CDN",
+ "provider.apisix": "Apache APISIX",
"provider.aws": "AWS",
"provider.aws.acm": "AWS - ACM (Amazon Certificate Manager)",
"provider.aws.cloudfront": "AWS - CloudFront",
@@ -56,9 +57,17 @@
"provider.cloudflare": "Cloudflare",
"provider.cloudns": "ClouDNS",
"provider.cmcccloud": "移动云",
+ "provider.cmcccloud.dns": "移动云 - 云解析 DNS",
"provider.constellix": "Constellix",
- "provider.ctcccloud": "联通云",
- "provider.cucccloud": "天翼云",
+ "provider.ctcccloud": "天翼云",
+ "provider.ctcccloud.ao": "天翼云 - 边缘安全加速平台 AccessOne",
+ "provider.ctcccloud.cdn": "天翼云 - 内容分发网络 CDN",
+ "provider.ctcccloud.cms_upload": "天翼云 - 上传到证书管理服务 CMS",
+ "provider.ctcccloud.elb": "天翼云 - 弹性负载均衡 ELB",
+ "provider.ctcccloud.icdn": "天翼云 - 全站加速 ICDN",
+ "provider.ctcccloud.lvdn": "天翼云 - 视频直播 LVDN",
+ "provider.ctcccloud.smartdns": "天翼云 - 智能 DNS",
+ "provider.cucccloud": "联通云",
"provider.desec": "deSEC",
"provider.digitalocean": "DigitalOcean",
"provider.dingtalkbot": "钉钉群机器人",
@@ -132,6 +141,7 @@
"provider.tencentcloud.dns": "腾讯云 - 云解析 DNS",
"provider.tencentcloud.ecdn": "腾讯云 - 全站加速网络 ECDN",
"provider.tencentcloud.eo": "腾讯云 - 边缘安全加速平台 EdgeOne",
+ "provider.tencentcloud.gaap": "腾讯云 - 全球应用加速 GAAP",
"provider.tencentcloud.scf": "腾讯云 - 云函数 SCF",
"provider.tencentcloud.ssl_upload": "腾讯云 - 上传到 SSL 证书服务",
"provider.tencentcloud.ssl_deploy": "腾讯云 - 通过 SSL 证书服务创建部署任务",
@@ -172,6 +182,7 @@
"provider.category.loadbalance": "负载均衡",
"provider.category.firewall": "防火墙",
"provider.category.av": "音视频",
+ "provider.category.accelerator": "加速器",
"provider.category.apigw": "API 网关",
"provider.category.serverless": "Serverless",
"provider.category.website": "网站托管",
diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
index 88d2b934..f51f5b0c 100644
--- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json
+++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json
@@ -225,7 +225,7 @@
"workflow_node.deploy.form.aliyun_fc_service_version.placeholder": "请选择阿里云 FC 服务版本",
"workflow_node.deploy.form.aliyun_fc_domain.label": "阿里云 FC 自定义域名",
"workflow_node.deploy.form.aliyun_fc_domain.placeholder": "请输入阿里云 FC 自定义域名(支持泛域名)",
- "workflow_node.deploy.form.aliyun_fc_domain.tooltip": "这是什么?请参阅 see https://fcnext.console.aliyun.com/ ",
+ "workflow_node.deploy.form.aliyun_fc_domain.tooltip": "这是什么?请参阅 see https://fcnext.console.aliyun.com ",
"workflow_node.deploy.form.aliyun_ga_resource_type.label": "证书部署方式",
"workflow_node.deploy.form.aliyun_ga_resource_type.placeholder": "请选择证书部署方式",
"workflow_node.deploy.form.aliyun_ga_resource_type.option.accelerator.label": "替换指定全球加速器下的全部 HTTPS 监听的证书",
@@ -284,6 +284,12 @@
"workflow_node.deploy.form.aliyun_waf_domain.label": "阿里云 WAF 接入域名(可选)",
"workflow_node.deploy.form.aliyun_waf_domain.placeholder": "请输入阿里云 WAF 接入域名(支持泛域名)",
"workflow_node.deploy.form.aliyun_waf_domain.tooltip": "这是什么?请参阅 waf.console.aliyun.com 不填写时,将替换实例的默认证书;否则,将替换扩展域名证书。",
+ "workflow_node.deploy.form.apisix_resource_type.label": "证书部署方式",
+ "workflow_node.deploy.form.apisix_resource_type.placeholder": "请选择证书部署方式",
+ "workflow_node.deploy.form.apisix_resource_type.option.certificate.label": "替换指定证书",
+ "workflow_node.deploy.form.apisix_certificate_id.label": "APISIX 证书 ID",
+ "workflow_node.deploy.form.apisix_certificate_id.placeholder": "请输入 APISIX 证书 ID",
+ "workflow_node.deploy.form.apisix_certificate_id.tooltip": "请登录 APISIX 控制台查看。",
"workflow_node.deploy.form.aws_acm_region.label": "AWS ACM 服务区域",
"workflow_node.deploy.form.aws_acm_region.placeholder": "请输入 AWS ACM 服务区域(例如:us-east-1)",
"workflow_node.deploy.form.aws_acm_region.tooltip": "这是什么?请参阅 https://docs.aws.amazon.com/zh_cn/general/latest/gr/rande.html#regional-endpoints ",
@@ -391,6 +397,31 @@
"workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly 证书 ID",
"workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "请输入 Cdnfly 证书 ID",
"workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "请登录 Cdnfly 控制台查看。",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.label": "天翼云 AccessOne 加速域名",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.placeholder": "请输入天翼云 AccessOne 加速域名(支持泛域名)",
+ "workflow_node.deploy.form.ctcccloud_ao_domain.tooltip": "这是什么?请参阅 https://cdn.ctyun.cn/h5/ctaccessone/ ",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.label": "天翼云 CDN 加速域名",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder": "请输入天翼云 CDN 加速域名(支持泛域名)",
+ "workflow_node.deploy.form.ctcccloud_cdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.ctyun.cn ",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.label": "证书部署方式",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.placeholder": "请选择证书部署方式",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听器的证书",
+ "workflow_node.deploy.form.ctcccloud_elb_resource_type.option.listener.label": "替换指定监听器的证书",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.label": "天翼云 ELB 资源池 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.placeholder": "请输入天翼云 ELB 资源池 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_region_id.tooltip": "这是什么?请参阅 https://www.ctyun.cn/document/10026755/10196575 ",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.label": "天翼云 ELB 负载均衡器 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.placeholder": "请输入天翼云 ELB 负载均衡器 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_loadbalancer_id.tooltip": "这是什么?请参阅 https://console.ctyun.cn/network/index/#/elb/elbList ",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.label": "天翼云 ELB 监听器 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.placeholder": "请输入天翼云 ELB 监听器 ID",
+ "workflow_node.deploy.form.ctcccloud_elb_listener_id.tooltip": "这是什么?请参阅 https://console.ctyun.cn/network/index/#/elb/elbList ",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.label": "天翼云 ICDN 加速域名",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder": "请输入天翼云 ICDN 加速域名(支持泛域名)",
+ "workflow_node.deploy.form.ctcccloud_icdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.ctyun.cn ",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.label": "天翼云 LVDN 加速域名",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder": "请输入天翼云 LVDN 加速域名",
+ "workflow_node.deploy.form.ctcccloud_lvdn_domain.tooltip": "这是什么?请参阅 https://cdn.ctyun.cn/h5/live/index ",
"workflow_node.deploy.form.dogecloud_cdn_domain.label": "多吉云 CDN 加速域名",
"workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "请输入多吉云 CDN 加速域名",
"workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.dogecloud.com ",
@@ -470,13 +501,13 @@
"workflow_node.deploy.form.jdcloud_alb_snidomain.tooltip": "这是什么?请参阅 https://cns-console.jdcloud.com/host/loadBalance/list 不填写时,将替换监听器的默认证书;否则,将替换扩展域名证书。",
"workflow_node.deploy.form.jdcloud_cdn_domain.label": "京东云 CDN 加速域名",
"workflow_node.deploy.form.jdcloud_cdn_domain.placeholder": "请输入京东云 CDN 加速域名(支持泛域名)",
- "workflow_node.deploy.form.jdcloud_cdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.jdcloud.com/ ",
+ "workflow_node.deploy.form.jdcloud_cdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.jdcloud.com ",
"workflow_node.deploy.form.jdcloud_live_domain.label": "京东云视频直播播放域名",
"workflow_node.deploy.form.jdcloud_live_domain.placeholder": "请输入京东云视频直播播放域名",
"workflow_node.deploy.form.jdcloud_live_domain.tooltip": "这是什么?请参阅 https://live-console.jdcloud.com ",
"workflow_node.deploy.form.jdcloud_vod_domain.label": "京东云视频点播加速域名",
"workflow_node.deploy.form.jdcloud_vod_domain.placeholder": "请输入京东云视频点播加速域名",
- "workflow_node.deploy.form.jdcloud_vod_domain.tooltip": "这是什么?请参阅 https://vod-console.jdcloud.com/ ",
+ "workflow_node.deploy.form.jdcloud_vod_domain.tooltip": "这是什么?请参阅 https://vod-console.jdcloud.com ",
"workflow_node.deploy.form.k8s_namespace.label": "Kubernetes 命名空间",
"workflow_node.deploy.form.k8s_namespace.placeholder": "请输入 Kubernetes 命名空间",
"workflow_node.deploy.form.k8s_namespace.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/overview/working-with-objects/namespaces/ ",
@@ -619,6 +650,7 @@
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_reload_nginx.label": "POSIX Bash - 重启 nginx 进程",
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_synologydsm_ssl.label": "POSIX Bash - 替换群晖 DSM 证书",
"workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_fnos_ssl.label": "POSIX Bash - 替换飞牛 fnOS 证书",
+ "workflow_node.deploy.form.ssh_preset_scripts.option.sh_replace_qnap_ssl.label": "POSIX Bash - 替换威联通 QNAP 证书",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_iis.label": "PowerShell - 导入并绑定到 IIS",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_netsh.label": "PowerShell - 导入并绑定到 netsh",
"workflow_node.deploy.form.ssh_preset_scripts.option.ps_binding_rdp.label": "PowerShell - 导入并绑定到 RDP",
@@ -669,6 +701,15 @@
"workflow_node.deploy.form.tencentcloud_eo_domain.label": "腾讯云 EdgeOne 加速域名",
"workflow_node.deploy.form.tencentcloud_eo_domain.placeholder": "请输入腾讯云 EdgeOne 加速域名(支持泛域名)",
"workflow_node.deploy.form.tencentcloud_eo_domain.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone ",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.label": "证书部署方式",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.placeholder": "请选择证书部署方式",
+ "workflow_node.deploy.form.tencentcloud_gaap_resource_type.option.listener.label": "替换指定监听器的证书",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.label": "腾讯云 GAAP 通道 ID(可选)",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.placeholder": "请输入腾讯云 GAAP 通道 ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_proxy_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/gaap ",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.label": "腾讯云 GAAP 监听器 ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.placeholder": "请输入腾讯云 GAAP 监听器 ID",
+ "workflow_node.deploy.form.tencentcloud_gaap_listener_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/gaap ",
"workflow_node.deploy.form.tencentcloud_scf_region.label": "腾讯云 SCF 产品地域",
"workflow_node.deploy.form.tencentcloud_scf_region.placeholder": "输入腾讯云 SCF 产品地域(例如:ap-guangzhou)",
"workflow_node.deploy.form.tencentcloud_scf_region.tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/583/17299 ",
@@ -857,12 +898,15 @@
"workflow_node.notify.form.discordbot_channel_id.label": "Discord 频道 ID(可选)",
"workflow_node.notify.form.discordbot_channel_id.placeholder": "请输入 Discord 频道 ID 以覆盖默认值",
"workflow_node.notify.form.discordbot_channel_id.tooltip": "不填写时,将使用所选通知渠道授权的默认频道 ID。",
- "workflow_node.notify.form.email_sender_address.label": "发送邮箱地址(可选)",
- "workflow_node.notify.form.email_sender_address.placeholder": "请输入发送邮箱地址以覆盖默认值",
- "workflow_node.notify.form.email_sender_address.tooltip": "不填写时,将使用所选通知渠道授权的默认发送邮箱地址。",
- "workflow_node.notify.form.email_receiver_address.label": "接收邮箱地址(可选)",
- "workflow_node.notify.form.email_receiver_address.placeholder": "请输入接收邮箱地址以覆盖默认值",
- "workflow_node.notify.form.email_receiver_address.tooltip": "不填写时,将使用所选通知渠道授权的默认接收邮箱地址。",
+ "workflow_node.notify.form.email_sender_address.label": "发件人邮箱(可选)",
+ "workflow_node.notify.form.email_sender_address.placeholder": "请输入发件人邮箱以覆盖默认值",
+ "workflow_node.notify.form.email_sender_address.tooltip": "不填写时,将使用所选通知渠道授权的默认发件人邮箱。",
+ "workflow_node.notify.form.email_sender_name.label": "发件人名称(可选)",
+ "workflow_node.notify.form.email_sender_name.placeholder": "请输入发件人名称以覆盖默认值",
+ "workflow_node.notify.form.email_sender_name.tooltip": "不填写时,将使用所选通知渠道授权的默认发件人名称。",
+ "workflow_node.notify.form.email_receiver_address.label": "收件人邮箱(可选)",
+ "workflow_node.notify.form.email_receiver_address.placeholder": "请输入收件人邮箱以覆盖默认值",
+ "workflow_node.notify.form.email_receiver_address.tooltip": "不填写时,将使用所选通知渠道授权的默认收件人邮箱。",
"workflow_node.notify.form.mattermost_channel_id.label": "Mattermost 频道 ID(可选)",
"workflow_node.notify.form.mattermost_channel_id.placeholder": "请输入 Mattermost 频道 ID 以覆盖默认值",
"workflow_node.notify.form.mattermost_channel_id.tooltip": "不填写时,将使用所选通知渠道授权的默认频道 ID。",