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 ( +
+ + + + + + } + > + + + +
+ ); +}; + +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 ( +
+ + + + + } + > + + + + + } + > + + + + + + } + > + + + +
+ ); +}; + +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 ( +
+ + + + + } + > + + + + + } + > + + + +
+ ); +}; + +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:
  1. ${DOMAIN}: The primary domain of the certificate (CommonName).
  2. ${DOMAINS}: The domain list of the certificate (SubjectAltNames).
  3. ${CERTIFICATE}: The PEM format content of the certificate file.
  4. ${SERVER_CERTIFICATE}: The PEM format content of the server certificate file.
  5. ${INTERMEDIA_CERTIFICATE}: The PEM format content of the intermediate CA certificate file.
  6. ${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:
  1. ${SUBJECT}: The subject of notification.
  2. ${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。",