diff --git a/go.mod b/go.mod index a567360f..7c710d35 100644 --- a/go.mod +++ b/go.mod @@ -12,7 +12,6 @@ require ( github.com/alibabacloud-go/nlb-20220430/v2 v2.0.3 github.com/alibabacloud-go/slb-20140515/v4 v4.0.9 github.com/alibabacloud-go/tea v1.2.2 - github.com/alibabacloud-go/tea-utils/v2 v2.0.6 github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible github.com/go-acme/lego/v4 v4.19.2 github.com/gojek/heimdall/v7 v7.0.3 @@ -25,7 +24,8 @@ require ( github.com/pocketbase/pocketbase v0.22.18 github.com/qiniu/go-sdk/v7 v7.22.0 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 - github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo v1.0.1030 golang.org/x/crypto v0.28.0 @@ -41,6 +41,7 @@ require ( github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect github.com/alibabacloud-go/tea-oss-sdk v1.1.3 // indirect github.com/alibabacloud-go/tea-oss-utils v1.1.0 // indirect + github.com/alibabacloud-go/tea-utils/v2 v2.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/route53 v1.43.2 // indirect github.com/blinkbean/dingtalk v1.1.3 // indirect github.com/emicklei/go-restful/v3 v3.11.0 // indirect @@ -141,7 +142,7 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/nrdcg/namesilo v0.2.1 // indirect github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect - github.com/pkg/errors v0.9.1 // indirect + github.com/pkg/errors v0.9.1 github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/spf13/cast v1.6.0 // indirect github.com/spf13/cobra v1.8.1 // indirect diff --git a/go.sum b/go.sum index 08ae184b..d93ba3da 100644 --- a/go.sum +++ b/go.sum @@ -204,8 +204,6 @@ github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHk github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= -github.com/gabriel-vasile/mimetype v1.4.4 h1:QjV6pZ7/XZ7ryI2KuyeEDE8wnh7fHP9YnQy+R0LnH8I= -github.com/gabriel-vasile/mimetype v1.4.4/go.mod h1:JwLei5XPtWdGiMFB5Pjle1oEeoSeEuJfJE+TtfvdB/s= github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc= github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= @@ -460,11 +458,14 @@ github.com/technoweenie/multipartstreamer v1.0.1 h1:XRztA5MXiR1TIRHxH2uNxXxaIkKQ github.com/technoweenie/multipartstreamer v1.0.1/go.mod h1:jNVxdtShOxzAsukZwTSw6MDx5eUJoiEBsSvzDU9uzog= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017 h1:OymmfmyFkvHirY3WHsoRT3cdTEsqygLbMn8jM41erK4= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn v1.0.1017/go.mod h1:gnLxGXlLmF+jDqWR1/RVoF/UUwxQxomQhkc0oN7KeuI= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031 h1:/eVMCl+jadCex6HxNN6/hFbC0iWl+e8s4PSIcI8aqS4= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb v1.0.1031/go.mod h1:8Km0fRIaDS7PssuyxDFvRRFBUFmECqG+ICpViCs/Vak= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.992/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1002/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1017/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= -github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030 h1:kwiUoCkooUgy7iPyhEEbio7WT21kGJUeZ5JeJfb/dYk= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1030/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031 h1:3ouglYKE5cwhx2vwICGeW7pAlwyCLnpQd7O0l3hCSTg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.1031/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002 h1:QwE0dRkAAbdf+eACnkNULgDn9ZKUJpPWRyXdqJolP5E= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/dnspod v1.0.1002/go.mod h1:WdC0FYbqYhJwQ3kbqri6hVP5HAEp+rzX9FToItTAzUg= github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl v1.0.992 h1:A6O89OlCJQUpNxGqC/E5By04UNKBryIt5olQIGOx8mg= diff --git a/internal/deployer/aliyun_alb.go b/internal/deployer/aliyun_alb.go index b676e043..e4e131bf 100644 --- a/internal/deployer/aliyun_alb.go +++ b/internal/deployer/aliyun_alb.go @@ -6,25 +6,29 @@ import ( "errors" "fmt" - alb20200616 "github.com/alibabacloud-go/alb-20200616/v2/client" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyunAlb "github.com/alibabacloud-go/alb-20200616/v2/client" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" ) type AliyunALBDeployer struct { option *DeployerOption infos []string - sdkClient *alb20200616.Client + sdkClient *aliyunAlb.Client sslUploader uploader.Uploader } func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } client, err := (&AliyunALBDeployer{}).createSdkClient( access.AccessKeyId, @@ -32,16 +36,16 @@ func NewAliyunALBDeployer(option *DeployerOption) (Deployer, error) { option.DeployConfig.GetConfigAsString("region"), ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{ + uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create ssl uploader") } return &AliyunALBDeployer{ @@ -56,7 +60,7 @@ func (d *AliyunALBDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *AliyunALBDeployer) GetInfo() []string { +func (d *AliyunALBDeployer) GetInfos() []string { return d.infos } @@ -77,12 +81,12 @@ func (d *AliyunALBDeployer) Deploy(ctx context.Context) error { return nil } -func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*alb20200616.Client, error) { +func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunAlb.Client, error) { if region == "" { region = "cn-hangzhou" // ALB 服务默认区域:华东一杭州 } - aConfig := &openapi.Config{ + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } @@ -96,7 +100,7 @@ func (d *AliyunALBDeployer) createSdkClient(accessKeyId, accessKeySecret, region } aConfig.Endpoint = tea.String(endpoint) - client, err := alb20200616.NewClient(aConfig) + client, err := aliyunAlb.NewClient(aConfig) if err != nil { return nil, err } @@ -114,12 +118,12 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getloadbalancerattribute - getLoadBalancerAttributeReq := &alb20200616.GetLoadBalancerAttributeRequest{ + getLoadBalancerAttributeReq := &aliyunAlb.GetLoadBalancerAttributeRequest{ LoadBalancerId: tea.String(aliLoadbalancerId), } getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'alb.GetLoadBalancerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetLoadBalancerAttribute'") } d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例", getLoadBalancerAttributeResp)) @@ -130,7 +134,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { listListenersLimit := int32(100) var listListenersToken *string = nil for { - listListenersReq := &alb20200616.ListListenersRequest{ + listListenersReq := &aliyunAlb.ListListenersRequest{ MaxResults: tea.Int32(listListenersLimit), NextToken: listListenersToken, LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, @@ -138,7 +142,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'") } if listListenersResp.Body.Listeners != nil { @@ -162,7 +166,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { listListenersPage = 1 listListenersToken = nil for { - listListenersReq := &alb20200616.ListListenersRequest{ + listListenersReq := &aliyunAlb.ListListenersRequest{ MaxResults: tea.Int32(listListenersLimit), NextToken: listListenersToken, LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, @@ -170,7 +174,7 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'alb.ListListeners': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'alb.ListListeners'") } if listListenersResp.Body.Listeners != nil { @@ -190,17 +194,17 @@ func (d *AliyunALBDeployer) deployToLoadbalancer(ctx context.Context) error { d.infos = append(d.infos, toStr("已查询到 ALB 负载均衡实例下的全部 QUIC 监听", aliListenerIds)) // 上传证书到 SSL - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 批量更新监听证书 var errs []error for _, aliListenerId := range aliListenerIds { - if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { errs = append(errs, err) } } @@ -218,15 +222,15 @@ func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error { } // 上传证书到 SSL - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 更新监听 - if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { return err } @@ -236,27 +240,27 @@ func (d *AliyunALBDeployer) deployToListener(ctx context.Context) error { func (d *AliyunALBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { // 查询监听的属性 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-getlistenerattribute - getListenerAttributeReq := &alb20200616.GetListenerAttributeRequest{ + getListenerAttributeReq := &aliyunAlb.GetListenerAttributeRequest{ ListenerId: tea.String(aliListenerId), } getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'alb.GetListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'alb.GetListenerAttribute'") } d.infos = append(d.infos, toStr("已查询到 ALB 监听配置", getListenerAttributeResp)) // 修改监听的属性 // REF: https://help.aliyun.com/zh/slb/application-load-balancer/developer-reference/api-alb-2020-06-16-updatelistenerattribute - updateListenerAttributeReq := &alb20200616.UpdateListenerAttributeRequest{ + updateListenerAttributeReq := &aliyunAlb.UpdateListenerAttributeRequest{ ListenerId: tea.String(aliListenerId), - Certificates: []*alb20200616.UpdateListenerAttributeRequestCertificates{{ + Certificates: []*aliyunAlb.UpdateListenerAttributeRequestCertificates{{ CertificateId: tea.String(aliCertId), }}, } updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'alb.UpdateListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'alb.UpdateListenerAttribute'") } d.infos = append(d.infos, toStr("已更新 ALB 监听配置", updateListenerAttributeResp)) diff --git a/internal/deployer/aliyun_cdn.go b/internal/deployer/aliyun_cdn.go index 97ac0d83..a23ce8a5 100644 --- a/internal/deployer/aliyun_cdn.go +++ b/internal/deployer/aliyun_cdn.go @@ -4,39 +4,41 @@ import ( "context" "encoding/json" "fmt" + "time" - cdn20180510 "github.com/alibabacloud-go/cdn-20180510/v5/client" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - util "github.com/alibabacloud-go/tea-utils/v2/service" + aliyunCdn "github.com/alibabacloud-go/cdn-20180510/v5/client" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" ) type AliyunCDNDeployer struct { - client *cdn20180510.Client option *DeployerOption infos []string + + sdkClient *aliyunCdn.Client } -func NewAliyunCDNDeployer(option *DeployerOption) (*AliyunCDNDeployer, error) { +func NewAliyunCDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) - - d := &AliyunCDNDeployer{ - option: option, + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") } - client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret) + client, err := (&AliyunCDNDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } return &AliyunCDNDeployer{ - client: client, - option: option, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClient: client, }, nil } @@ -44,41 +46,43 @@ func (d *AliyunCDNDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *AliyunCDNDeployer) GetInfo() []string { +func (d *AliyunCDNDeployer) GetInfos() []string { return d.infos } func (d *AliyunCDNDeployer) Deploy(ctx context.Context) error { - certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6)) - setCdnDomainSSLCertificateRequest := &cdn20180510.SetCdnDomainSSLCertificateRequest{ - DomainName: tea.String(getDeployString(d.option.DeployConfig, "domain")), - CertName: tea.String(certName), + // 设置 CDN 域名域名证书 + // REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate + setCdnDomainSSLCertificateReq := &aliyunCdn.SetCdnDomainSSLCertificateRequest{ + DomainName: tea.String(d.option.DeployConfig.GetConfigAsString("domain")), + CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")), + CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), CertType: tea.String("upload"), SSLProtocol: tea.String("on"), SSLPub: tea.String(d.option.Certificate.Certificate), SSLPri: tea.String(d.option.Certificate.PrivateKey), - CertRegion: tea.String("cn-hangzhou"), } - - runtime := &util.RuntimeOptions{} - - resp, err := d.client.SetCdnDomainSSLCertificateWithOptions(setCdnDomainSSLCertificateRequest, runtime) + setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq) if err != nil { - return err + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.SetCdnDomainSSLCertificate'") } - d.infos = append(d.infos, toStr("cdn设置证书", resp)) + d.infos = append(d.infos, toStr("已设置 CDN 域名证书", setCdnDomainSSLCertificateResp)) return nil } -func (d *AliyunCDNDeployer) createClient(accessKeyId, accessKeySecret string) (_result *cdn20180510.Client, _err error) { - config := &openapi.Config{ +func (d *AliyunCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunCdn.Client, error) { + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), + Endpoint: tea.String("cdn.aliyuncs.com"), } - config.Endpoint = tea.String("cdn.aliyuncs.com") - _result = &cdn20180510.Client{} - _result, _err = cdn20180510.NewClient(config) - return _result, _err + + client, err := aliyunCdn.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil } diff --git a/internal/deployer/aliyun_clb.go b/internal/deployer/aliyun_clb.go index 11384ba8..3796a1ba 100644 --- a/internal/deployer/aliyun_clb.go +++ b/internal/deployer/aliyun_clb.go @@ -6,25 +6,29 @@ import ( "errors" "fmt" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderAliyunSlb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-slb" ) type AliyunCLBDeployer struct { option *DeployerOption infos []string - sdkClient *slb20140515.Client + sdkClient *aliyunSlb.Client sslUploader uploader.Uploader } func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } client, err := (&AliyunCLBDeployer{}).createSdkClient( access.AccessKeyId, @@ -32,16 +36,16 @@ func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) { option.DeployConfig.GetConfigAsString("region"), ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{ + uploader, err := uploaderAliyunSlb.New(&uploaderAliyunSlb.AliyunSLBUploaderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create ssl uploader") } return &AliyunCLBDeployer{ @@ -56,7 +60,7 @@ func (d *AliyunCLBDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *AliyunCLBDeployer) GetInfo() []string { +func (d *AliyunCLBDeployer) GetInfos() []string { return d.infos } @@ -77,12 +81,12 @@ func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error { return nil } -func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) { +func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) { if region == "" { region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州 } - aConfig := &openapi.Config{ + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } @@ -99,7 +103,7 @@ func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region } aConfig.Endpoint = tea.String(endpoint) - client, err := slb20140515.NewClient(aConfig) + client, err := aliyunSlb.NewClient(aConfig) if err != nil { return nil, err } @@ -109,21 +113,20 @@ func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + aliListenerPorts := make([]int32, 0) if aliLoadbalancerId == "" { return errors.New("`loadbalancerId` is required") } - aliListenerPorts := make([]int32, 0) - // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute - describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{ + describeLoadBalancerAttributeReq := &aliyunSlb.DescribeLoadBalancerAttributeRequest{ RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), LoadBalancerId: tea.String(aliLoadbalancerId), } describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerAttribute'") } d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例", describeLoadBalancerAttributeResp)) @@ -134,7 +137,7 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { listListenersLimit := int32(100) var listListenersToken *string = nil for { - describeLoadBalancerListenersReq := &slb20140515.DescribeLoadBalancerListenersRequest{ + describeLoadBalancerListenersReq := &aliyunSlb.DescribeLoadBalancerListenersRequest{ RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), MaxResults: tea.Int32(listListenersLimit), NextToken: listListenersToken, @@ -143,7 +146,7 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { } describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerListeners'") } if describeLoadBalancerListenersResp.Body.Listeners != nil { @@ -163,17 +166,17 @@ func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡实例下的全部 HTTPS 监听", aliListenerPorts)) // 上传证书到 SLB - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 批量更新监听证书 var errs []error for _, aliListenerPort := range aliListenerPorts { - if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil { errs = append(errs, err) } } @@ -196,15 +199,15 @@ func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error { } // 上传证书到 SLB - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 更新监听 - if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, upres.CertId); err != nil { return err } @@ -214,27 +217,27 @@ func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error { func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error { // 查询监听配置 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute - describeLoadBalancerHTTPSListenerAttributeReq := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{ + describeLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.DescribeLoadBalancerHTTPSListenerAttributeRequest{ LoadBalancerId: tea.String(aliLoadbalancerId), ListenerPort: tea.Int32(aliListenerPort), } describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute'") } d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)) // 查询扩展域名 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions - describeDomainExtensionsReq := &slb20140515.DescribeDomainExtensionsRequest{ + describeDomainExtensionsReq := &aliyunSlb.DescribeDomainExtensionsRequest{ RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), LoadBalancerId: tea.String(aliLoadbalancerId), ListenerPort: tea.Int32(aliListenerPort), } describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeDomainExtensions'") } d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp)) @@ -249,14 +252,14 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo break } - setDomainExtensionAttributeReq := &slb20140515.SetDomainExtensionAttributeRequest{ + setDomainExtensionAttributeReq := &aliyunSlb.SetDomainExtensionAttributeRequest{ RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), DomainExtensionId: tea.String(*domainExtension.DomainExtensionId), ServerCertificateId: tea.String(aliCertId), } _, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetDomainExtensionAttribute'") } } } @@ -265,7 +268,7 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute // // 注意修改监听配置要放在修改扩展域名之后 - setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.SetLoadBalancerHTTPSListenerAttributeRequest{ + setLoadBalancerHTTPSListenerAttributeReq := &aliyunSlb.SetLoadBalancerHTTPSListenerAttributeRequest{ RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), LoadBalancerId: tea.String(aliLoadbalancerId), ListenerPort: tea.Int32(aliListenerPort), @@ -273,7 +276,7 @@ func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLo } setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute'") } d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)) diff --git a/internal/deployer/aliyun_dcdn.go b/internal/deployer/aliyun_dcdn.go new file mode 100644 index 00000000..0f969787 --- /dev/null +++ b/internal/deployer/aliyun_dcdn.go @@ -0,0 +1,95 @@ +package deployer + +import ( + "context" + "encoding/json" + "fmt" + "strings" + "time" + + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyunDcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client" + "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/domain" +) + +type AliyunDCDNDeployer struct { + option *DeployerOption + infos []string + + sdkClient *aliyunDcdn.Client +} + +func NewAliyunDCDNDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.AliyunAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } + + client, err := (&AliyunDCDNDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &AliyunDCDNDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + }, nil +} + +func (d *AliyunDCDNDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *AliyunDCDNDeployer) GetInfos() []string { + return d.infos +} + +func (d *AliyunDCDNDeployer) Deploy(ctx context.Context) error { + // 支持泛解析域名,在 Aliyun DCDN 中泛解析域名表示为 .example.com + domain := d.option.DeployConfig.GetConfigAsString("domain") + if strings.HasPrefix(domain, "*") { + domain = strings.TrimPrefix(domain, "*") + } + + // 配置域名证书 + // REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate + setDcdnDomainSSLCertificateReq := &aliyunDcdn.SetDcdnDomainSSLCertificateRequest{ + DomainName: tea.String(domain), + CertRegion: tea.String(d.option.DeployConfig.GetConfigOrDefaultAsString("region", "cn-hangzhou")), + CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), + CertType: tea.String("upload"), + SSLProtocol: tea.String("on"), + SSLPub: tea.String(d.option.Certificate.Certificate), + SSLPri: tea.String(d.option.Certificate.PrivateKey), + } + setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'dcdn.SetDcdnDomainSSLCertificate'") + } + + d.infos = append(d.infos, toStr("已配置 DCDN 域名证书", setDcdnDomainSSLCertificateResp)) + + return nil +} + +func (d *AliyunDCDNDeployer) createSdkClient(accessKeyId, accessKeySecret string) (*aliyunDcdn.Client, error) { + aConfig := &aliyunOpen.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + Endpoint: tea.String("dcdn.aliyuncs.com"), + } + + client, err := aliyunDcdn.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/deployer/aliyun_esa.go b/internal/deployer/aliyun_esa.go deleted file mode 100644 index 012ca887..00000000 --- a/internal/deployer/aliyun_esa.go +++ /dev/null @@ -1,97 +0,0 @@ -/* - * @Author: Bin - * @Date: 2024-09-17 - * @FilePath: /certimate/internal/deployer/aliyun_esa.go - */ -package deployer - -import ( - "context" - "encoding/json" - "fmt" - "strings" - - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - dcdn20180115 "github.com/alibabacloud-go/dcdn-20180115/v3/client" - util "github.com/alibabacloud-go/tea-utils/v2/service" - "github.com/alibabacloud-go/tea/tea" - - "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" -) - -type AliyunESADeployer struct { - client *dcdn20180115.Client - option *DeployerOption - infos []string -} - -func NewAliyunESADeployer(option *DeployerOption) (*AliyunESADeployer, error) { - access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) - - d := &AliyunESADeployer{ - option: option, - } - - client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret) - if err != nil { - return nil, err - } - - return &AliyunESADeployer{ - client: client, - option: option, - infos: make([]string, 0), - }, nil -} - -func (d *AliyunESADeployer) GetID() string { - return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) -} - -func (d *AliyunESADeployer) GetInfo() []string { - return d.infos -} - -func (d *AliyunESADeployer) Deploy(ctx context.Context) error { - certName := fmt.Sprintf("%s-%s-%s", d.option.Domain, d.option.DomainId, rand.RandStr(6)) - - // 支持泛解析域名,在 Aliyun DCND 中泛解析域名表示为 .example.com - domain := getDeployString(d.option.DeployConfig, "domain") - if strings.HasPrefix(domain, "*") { - domain = strings.TrimPrefix(domain, "*") - } - - setDcdnDomainSSLCertificateRequest := &dcdn20180115.SetDcdnDomainSSLCertificateRequest{ - DomainName: tea.String(domain), - CertName: tea.String(certName), - CertType: tea.String("upload"), - SSLProtocol: tea.String("on"), - SSLPub: tea.String(d.option.Certificate.Certificate), - SSLPri: tea.String(d.option.Certificate.PrivateKey), - CertRegion: tea.String("cn-hangzhou"), - } - - runtime := &util.RuntimeOptions{} - - resp, err := d.client.SetDcdnDomainSSLCertificateWithOptions(setDcdnDomainSSLCertificateRequest, runtime) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("dcdn设置证书", resp)) - - return nil -} - -func (d *AliyunESADeployer) createClient(accessKeyId, accessKeySecret string) (_result *dcdn20180115.Client, _err error) { - config := &openapi.Config{ - AccessKeyId: tea.String(accessKeyId), - AccessKeySecret: tea.String(accessKeySecret), - } - config.Endpoint = tea.String("dcdn.aliyuncs.com") - _result = &dcdn20180115.Client{} - _result, _err = dcdn20180115.NewClient(config) - return _result, _err -} diff --git a/internal/deployer/aliyun_nlb.go b/internal/deployer/aliyun_nlb.go index 514657e6..56b06d39 100644 --- a/internal/deployer/aliyun_nlb.go +++ b/internal/deployer/aliyun_nlb.go @@ -6,25 +6,29 @@ import ( "errors" "fmt" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - nlb20220430 "github.com/alibabacloud-go/nlb-20220430/v2/client" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyunNlb "github.com/alibabacloud-go/nlb-20220430/v2/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderAliyunCas "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" ) type AliyunNLBDeployer struct { option *DeployerOption infos []string - sdkClient *nlb20220430.Client + sdkClient *aliyunNlb.Client sslUploader uploader.Uploader } func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } client, err := (&AliyunNLBDeployer{}).createSdkClient( access.AccessKeyId, @@ -32,16 +36,16 @@ func NewAliyunNLBDeployer(option *DeployerOption) (Deployer, error) { option.DeployConfig.GetConfigAsString("region"), ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - uploader, err := uploader.NewAliyunCASUploader(&uploader.AliyunCASUploaderConfig{ + uploader, err := uploaderAliyunCas.New(&uploaderAliyunCas.AliyunCASUploaderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create ssl uploader") } return &AliyunNLBDeployer{ @@ -56,7 +60,7 @@ func (d *AliyunNLBDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *AliyunNLBDeployer) GetInfo() []string { +func (d *AliyunNLBDeployer) GetInfos() []string { return d.infos } @@ -77,12 +81,12 @@ func (d *AliyunNLBDeployer) Deploy(ctx context.Context) error { return nil } -func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*nlb20220430.Client, error) { +func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunNlb.Client, error) { if region == "" { region = "cn-hangzhou" // NLB 服务默认区域:华东一杭州 } - aConfig := &openapi.Config{ + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } @@ -94,7 +98,7 @@ func (d *AliyunNLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region } aConfig.Endpoint = tea.String(endpoint) - client, err := nlb20220430.NewClient(aConfig) + client, err := aliyunNlb.NewClient(aConfig) if err != nil { return nil, err } @@ -112,12 +116,12 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { // 查询负载均衡实例的详细信息 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getloadbalancerattribute - getLoadBalancerAttributeReq := &nlb20220430.GetLoadBalancerAttributeRequest{ + getLoadBalancerAttributeReq := &aliyunNlb.GetLoadBalancerAttributeRequest{ LoadBalancerId: tea.String(aliLoadbalancerId), } getLoadBalancerAttributeResp, err := d.sdkClient.GetLoadBalancerAttribute(getLoadBalancerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'nlb.GetLoadBalancerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetLoadBalancerAttribute'") } d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例", getLoadBalancerAttributeResp)) @@ -128,7 +132,7 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { listListenersLimit := int32(100) var listListenersToken *string = nil for { - listListenersReq := &nlb20220430.ListListenersRequest{ + listListenersReq := &aliyunNlb.ListListenersRequest{ MaxResults: tea.Int32(listListenersLimit), NextToken: listListenersToken, LoadBalancerIds: []*string{tea.String(aliLoadbalancerId)}, @@ -136,7 +140,7 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'nlb.ListListeners': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'nlb.ListListeners'") } if listListenersResp.Body.Listeners != nil { @@ -156,17 +160,17 @@ func (d *AliyunNLBDeployer) deployToLoadbalancer(ctx context.Context) error { d.infos = append(d.infos, toStr("已查询到 NLB 负载均衡实例下的全部 TCPSSL 监听", aliListenerIds)) // 上传证书到 SSL - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 批量更新监听证书 var errs []error for _, aliListenerId := range aliListenerIds { - if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { errs = append(errs, err) } } @@ -184,15 +188,15 @@ func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error { } // 上传证书到 SSL - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 更新监听 - if err := d.updateListenerCertificate(ctx, aliListenerId, uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, aliListenerId, upres.CertId); err != nil { return err } @@ -202,25 +206,25 @@ func (d *AliyunNLBDeployer) deployToListener(ctx context.Context) error { func (d *AliyunNLBDeployer) updateListenerCertificate(ctx context.Context, aliListenerId string, aliCertId string) error { // 查询监听的属性 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-getlistenerattribute - getListenerAttributeReq := &nlb20220430.GetListenerAttributeRequest{ + getListenerAttributeReq := &aliyunNlb.GetListenerAttributeRequest{ ListenerId: tea.String(aliListenerId), } getListenerAttributeResp, err := d.sdkClient.GetListenerAttribute(getListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'nlb.GetListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'nlb.GetListenerAttribute'") } d.infos = append(d.infos, toStr("已查询到 NLB 监听配置", getListenerAttributeResp)) // 修改监听的属性 // REF: https://help.aliyun.com/zh/slb/network-load-balancer/developer-reference/api-nlb-2022-04-30-updatelistenerattribute - updateListenerAttributeReq := &nlb20220430.UpdateListenerAttributeRequest{ + updateListenerAttributeReq := &aliyunNlb.UpdateListenerAttributeRequest{ ListenerId: tea.String(aliListenerId), CertificateIds: []*string{tea.String(aliCertId)}, } updateListenerAttributeResp, err := d.sdkClient.UpdateListenerAttribute(updateListenerAttributeReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'nlb.UpdateListenerAttribute': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'nlb.UpdateListenerAttribute'") } d.infos = append(d.infos, toStr("已更新 NLB 监听配置", updateListenerAttributeResp)) diff --git a/internal/deployer/aliyun_oss.go b/internal/deployer/aliyun_oss.go index 9626e3bc..d0045d89 100644 --- a/internal/deployer/aliyun_oss.go +++ b/internal/deployer/aliyun_oss.go @@ -3,48 +3,62 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" "github.com/aliyun/aliyun-oss-go-sdk/oss" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" ) type AliyunOSSDeployer struct { - client *oss.Client option *DeployerOption infos []string + + sdkClient *oss.Client } func NewAliyunOSSDeployer(option *DeployerOption) (Deployer, error) { access := &domain.AliyunAccess{} - json.Unmarshal([]byte(option.Access), access) - - d := &AliyunOSSDeployer{ - option: option, - infos: make([]string, 0), + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") } - client, err := d.createClient(access.AccessKeyId, access.AccessKeySecret) + client, err := (&AliyunOSSDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + option.DeployConfig.GetConfigAsString("endpoint"), + ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - d.client = client - return d, nil + return &AliyunOSSDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + }, nil } func (d *AliyunOSSDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *AliyunOSSDeployer) GetInfo() []string { +func (d *AliyunOSSDeployer) GetInfos() []string { return d.infos } func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error { - err := d.client.PutBucketCnameWithCertificate(getDeployString(d.option.DeployConfig, "bucket"), oss.PutBucketCname{ - Cname: getDeployString(d.option.DeployConfig, "domain"), + aliBucket := d.option.DeployConfig.GetConfigAsString("bucket") + if aliBucket == "" { + return errors.New("`bucket` is required") + } + + // 为存储空间绑定自定义域名 + // REF: https://help.aliyun.com/zh/oss/developer-reference/putcname + err := d.sdkClient.PutBucketCnameWithCertificate(aliBucket, oss.PutBucketCname{ + Cname: d.option.DeployConfig.GetConfigAsString("domain"), CertificateConfiguration: &oss.CertificateConfiguration{ Certificate: d.option.Certificate.Certificate, PrivateKey: d.option.Certificate.PrivateKey, @@ -52,19 +66,21 @@ func (d *AliyunOSSDeployer) Deploy(ctx context.Context) error { }, }) if err != nil { - return fmt.Errorf("deploy aliyun oss error: %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'oss.PutBucketCnameWithCertificate'") } + return nil } -func (d *AliyunOSSDeployer) createClient(accessKeyId, accessKeySecret string) (*oss.Client, error) { - client, err := oss.New( - getDeployString(d.option.DeployConfig, "endpoint"), - accessKeyId, - accessKeySecret, - ) - if err != nil { - return nil, fmt.Errorf("create aliyun client error: %w", err) +func (d *AliyunOSSDeployer) createSdkClient(accessKeyId, accessKeySecret, endpoint string) (*oss.Client, error) { + if endpoint == "" { + endpoint = "oss.aliyuncs.com" } + + client, err := oss.New(endpoint, accessKeyId, accessKeySecret) + if err != nil { + return nil, err + } + return client, nil } diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 9dc537f3..38ee209b 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -22,7 +22,7 @@ import ( const ( targetAliyunOSS = "aliyun-oss" targetAliyunCDN = "aliyun-cdn" - targetAliyunESA = "aliyun-dcdn" + targetAliyunDCDN = "aliyun-dcdn" targetAliyunCLB = "aliyun-clb" targetAliyunALB = "aliyun-alb" targetAliyunNLB = "aliyun-nlb" @@ -52,7 +52,7 @@ type DeployerOption struct { type Deployer interface { Deploy(ctx context.Context) error - GetInfo() []string + GetInfos() []string GetID() string } @@ -112,8 +112,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return NewAliyunOSSDeployer(option) case targetAliyunCDN: return NewAliyunCDNDeployer(option) - case targetAliyunESA: - return NewAliyunESADeployer(option) + case targetAliyunDCDN: + return NewAliyunDCDNDeployer(option) case targetAliyunCLB: return NewAliyunCLBDeployer(option) case targetAliyunALB: @@ -156,41 +156,6 @@ func toStr(tag string, data any) string { return tag + ":" + string(byts) } -func getDeployString(conf domain.DeployConfig, key string) string { - if _, ok := conf.Config[key]; !ok { - return "" - } - - val, ok := conf.Config[key].(string) - if !ok { - return "" - } - - return val -} - -func getDeployVariables(conf domain.DeployConfig) map[string]string { - rs := make(map[string]string) - data, ok := conf.Config["variables"] - if !ok { - return rs - } - - bts, _ := json.Marshal(data) - - kvData := make([]domain.KV, 0) - - if err := json.Unmarshal(bts, &kvData); err != nil { - return rs - } - - for _, kv := range kvData { - rs[kv.Key] = kv.Value - } - - return rs -} - func convertPEMToPFX(certificate string, privateKey string, password string) ([]byte, error) { cert, err := x509.ParseCertificateFromPEM(certificate) if err != nil { @@ -204,7 +169,7 @@ func convertPEMToPFX(certificate string, privateKey string, password string) ([] pfxData, err := pkcs12.LegacyRC2.Encode(privkey, cert, nil, password) if err != nil { - return nil, fmt.Errorf("failed to encode as pfx %w", err) + return nil, err } return pfxData, nil diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index ab6e936b..d3d4014f 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -4,30 +4,32 @@ import ( "context" "encoding/json" "fmt" - "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderHcScm "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm" "github.com/usual2970/certimate/internal/pkg/utils/cast" + hcCdnEx "github.com/usual2970/certimate/internal/pkg/vendors/huaweicloud-cdn-sdk" ) type HuaweiCloudCDNDeployer struct { option *DeployerOption infos []string - sdkClient *hcCdn.CdnClient + sdkClient *hcCdnEx.Client sslUploader uploader.Uploader } func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.HuaweiCloudAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to get access") } client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient( @@ -36,17 +38,16 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { option.DeployConfig.GetConfigAsString("region"), ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 - uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ + uploader, err := uploaderHcScm.New(&uploaderHcScm.HuaweiCloudSCMUploaderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Region: "", }) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create ssl uploader") } return &HuaweiCloudCDNDeployer{ @@ -61,11 +62,19 @@ func (d *HuaweiCloudCDNDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *HuaweiCloudCDNDeployer) GetInfo() []string { +func (d *HuaweiCloudCDNDeployer) GetInfos() []string { return d.infos } func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { + // 上传证书到 SCM + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", upres)) + // 查询加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{ @@ -73,7 +82,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { } showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq) if err != nil { - return err + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ShowDomainFullConfig'") } d.infos = append(d.infos, toStr("已查询到加速域名配置", showDomainFullConfigResp)) @@ -81,37 +90,21 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { // 更新加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/UpdateDomainMultiCertificates.html // REF: https://support.huaweicloud.com/usermanual-cdn/cdn_01_0306.html - updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{} + updateDomainMultiCertificatesReqBodyContent := &hcCdnEx.UpdateDomainMultiCertificatesExRequestBodyContent{} updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain") updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1 - var updateDomainMultiCertificatesResp *hcCdnModel.UpdateDomainMultiCertificatesResponse - if d.option.DeployConfig.GetConfigAsBool("useSCM") { - // 上传证书到 SCM - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) - - updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2) - updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(uploadResult.CertId) - updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(uploadResult.CertName) - } else { - updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(0) - updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())) - updateDomainMultiCertificatesReqBodyContent.Certificate = cast.StringPtr(d.option.Certificate.Certificate) - updateDomainMultiCertificatesReqBodyContent.PrivateKey = cast.StringPtr(d.option.Certificate.PrivateKey) - } - updateDomainMultiCertificatesReqBodyContent = mergeHuaweiCloudCDNConfig(showDomainFullConfigResp.Configs, updateDomainMultiCertificatesReqBodyContent) - updateDomainMultiCertificatesReq := &huaweicloudCDNUpdateDomainMultiCertificatesRequest{ - Body: &huaweicloudCDNUpdateDomainMultiCertificatesRequestBody{ + updateDomainMultiCertificatesReqBodyContent.CertificateType = cast.Int32Ptr(2) + updateDomainMultiCertificatesReqBodyContent.SCMCertificateId = cast.StringPtr(upres.CertId) + updateDomainMultiCertificatesReqBodyContent.CertName = cast.StringPtr(upres.CertName) + updateDomainMultiCertificatesReqBodyContent = updateDomainMultiCertificatesReqBodyContent.MergeConfig(showDomainFullConfigResp.Configs) + updateDomainMultiCertificatesReq := &hcCdnEx.UpdateDomainMultiCertificatesExRequest{ + Body: &hcCdnEx.UpdateDomainMultiCertificatesExRequestBody{ Https: updateDomainMultiCertificatesReqBodyContent, }, } - updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(d.sdkClient, updateDomainMultiCertificatesReq) + updateDomainMultiCertificatesResp, err := d.sdkClient.UploadDomainMultiCertificatesEx(updateDomainMultiCertificatesReq) if err != nil { - return err + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadDomainMultiCertificatesEx'") } d.infos = append(d.infos, toStr("已更新加速域名配置", updateDomainMultiCertificatesResp)) @@ -119,7 +112,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { return nil } -func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, error) { +func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdnEx.Client, error) { if region == "" { region = "cn-north-1" // CDN 服务默认区域:华北一北京 } @@ -145,69 +138,6 @@ func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, r return nil, err } - client := hcCdn.NewCdnClient(hcClient) + client := hcCdnEx.NewClient(hcClient) return client, nil } - -type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct { - hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"` - - SCMCertificateId *string `json:"scm_certificate_id,omitempty"` -} - -type huaweicloudCDNUpdateDomainMultiCertificatesRequestBody struct { - Https *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent `json:"https,omitempty"` -} - -type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct { - Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"` -} - -func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *hcCdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*hcCdnModel.UpdateDomainMultiCertificatesResponse, error) { - // 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求 - // 可能需要等之后 SDK 更新 - - requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates() - - if resp, err := client.HcClient.Sync(request, requestDef); err != nil { - return nil, err - } else { - return resp.(*hcCdnModel.UpdateDomainMultiCertificatesResponse), nil - } -} - -func mergeHuaweiCloudCDNConfig(src *hcCdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent { - if src == nil { - return dest - } - - // 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去 - // 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化 - - if *src.OriginProtocol == "follow" { - dest.AccessOriginWay = cast.Int32Ptr(1) - } else if *src.OriginProtocol == "http" { - dest.AccessOriginWay = cast.Int32Ptr(2) - } else if *src.OriginProtocol == "https" { - dest.AccessOriginWay = cast.Int32Ptr(3) - } - - if src.ForceRedirect != nil { - dest.ForceRedirectConfig = &hcCdnModel.ForceRedirect{} - - if src.ForceRedirect.Status == "on" { - dest.ForceRedirectConfig.Switch = 1 - dest.ForceRedirectConfig.RedirectType = src.ForceRedirect.Type - } else { - dest.ForceRedirectConfig.Switch = 0 - } - } - - if src.Https != nil { - if *src.Https.Http2Status == "on" { - dest.Http2 = cast.Int32Ptr(1) - } - } - - return dest -} diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go index f9f26338..b7658660 100644 --- a/internal/deployer/huaweicloud_elb.go +++ b/internal/deployer/huaweicloud_elb.go @@ -16,9 +16,11 @@ import ( hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" + xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderHcElb "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-elb" "github.com/usual2970/certimate/internal/pkg/utils/cast" ) @@ -33,7 +35,7 @@ type HuaweiCloudELBDeployer struct { func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.HuaweiCloudAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to get access") } client, err := (&HuaweiCloudELBDeployer{}).createSdkClient( @@ -42,16 +44,16 @@ func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { option.DeployConfig.GetConfigAsString("region"), ) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create sdk client") } - uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{ + uploader, err := uploaderHcElb.New(&uploaderHcElb.HuaweiCloudELBUploaderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { - return nil, err + return nil, xerrors.Wrap(err, "failed to create ssl uploader") } return &HuaweiCloudELBDeployer{ @@ -66,21 +68,24 @@ func (d *HuaweiCloudELBDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *HuaweiCloudELBDeployer) GetInfo() []string { +func (d *HuaweiCloudELBDeployer) GetInfos() []string { return d.infos } func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error { switch d.option.DeployConfig.GetConfigAsString("resourceType") { case "certificate": + // 部署到指定证书 if err := d.deployToCertificate(ctx); err != nil { return err } case "loadbalancer": + // 部署到指定负载均衡器 if err := d.deployToLoadbalancer(ctx); err != nil { return err } case "listener": + // 部署到指定监听器 if err := d.deployToListener(ctx); err != nil { return err } @@ -169,7 +174,7 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { - return "", fmt.Errorf("no project found") + return "", errors.New("no project found") } return (*response.Projects)[0].Id, nil @@ -194,7 +199,7 @@ func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error } updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateCertificate'") } d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp)) @@ -217,7 +222,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowLoadBalancer'") } d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp)) @@ -235,7 +240,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListListeners'") } if listListenersResp.Listeners != nil { @@ -254,17 +259,17 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds)) // 上传证书到 SCM - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 批量更新监听器证书 var errs []error for _, hcListenerId := range hcListenerIds { - if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { + if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil { errs = append(errs, err) } } @@ -282,22 +287,22 @@ func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { } // 上传证书到 SCM - uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } - d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + d.infos = append(d.infos, toStr("已上传证书", upres)) // 更新监听器证书 - if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { + if err := d.modifyListenerCertificate(ctx, hcListenerId, upres.CertId); err != nil { return err } return nil } -func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error { +func (d *HuaweiCloudELBDeployer) modifyListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error { // 查询监听器详情 // REF: https://support.huaweicloud.com/api-elb/ShowListener.html showListenerReq := &hcElbModel.ShowListenerRequest{ @@ -305,7 +310,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, } showListenerResp, err := d.sdkClient.ShowListener(showListenerReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowListener'") } d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp)) @@ -331,7 +336,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, } listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'") } showNewCertificateReq := &hcElbModel.ShowCertificateRequest{ @@ -339,7 +344,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, } showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.ShowCertificate'") } for _, certificate := range *listOldCertificateResp.Certificates { @@ -372,7 +377,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, } updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) if err != nil { - return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'elb.UpdateListener'") } d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp)) diff --git a/internal/deployer/k8s_secret.go b/internal/deployer/k8s_secret.go index 7f042ddd..8a1c30ff 100644 --- a/internal/deployer/k8s_secret.go +++ b/internal/deployer/k8s_secret.go @@ -3,11 +3,13 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" "strings" - corev1 "k8s.io/api/core/v1" - k8sMetaV1 "k8s.io/apimachinery/pkg/apis/meta/v1" + xerrors "github.com/pkg/errors" + k8sCore "k8s.io/api/core/v1" + k8sMeta "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/clientcmd" @@ -19,12 +21,25 @@ import ( type K8sSecretDeployer struct { option *DeployerOption infos []string + + k8sClient *kubernetes.Clientset } func NewK8sSecretDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.KubernetesAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } + + client, err := (&K8sSecretDeployer{}).createK8sClient(access) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create k8s client") + } + return &K8sSecretDeployer{ - option: option, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + k8sClient: client, }, nil } @@ -32,75 +47,53 @@ func (d *K8sSecretDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *K8sSecretDeployer) GetInfo() []string { +func (d *K8sSecretDeployer) GetInfos() []string { return d.infos } func (d *K8sSecretDeployer) Deploy(ctx context.Context) error { - access := &domain.KubernetesAccess{} - if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return err - } - - client, err := d.createClient(access) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("kubeClient create success.", nil)) - - namespace := getDeployString(d.option.DeployConfig, "namespace") + namespace := d.option.DeployConfig.GetConfigAsString("namespace") + secretName := d.option.DeployConfig.GetConfigAsString("secretName") + secretDataKeyForCrt := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForCrt", "tls.crt") + secretDataKeyForKey := d.option.DeployConfig.GetConfigOrDefaultAsString("secretDataKeyForKey", "tls.key") if namespace == "" { namespace = "default" } - - secretName := getDeployString(d.option.DeployConfig, "secretName") if secretName == "" { - return fmt.Errorf("k8s secret name is empty") + return errors.New("`secretName` is required") } - secretDataKeyForCrt := getDeployString(d.option.DeployConfig, "secretDataKeyForCrt") - if secretDataKeyForCrt == "" { - namespace = "tls.crt" - } - - secretDataKeyForKey := getDeployString(d.option.DeployConfig, "secretDataKeyForKey") - if secretDataKeyForKey == "" { - namespace = "tls.key" - } - - certificate, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate) + certX509, err := x509.ParseCertificateFromPEM(d.option.Certificate.Certificate) if err != nil { - return fmt.Errorf("failed to parse certificate: %w", err) + return err } - secretPayload := corev1.Secret{ - TypeMeta: k8sMetaV1.TypeMeta{ + secretPayload := k8sCore.Secret{ + TypeMeta: k8sMeta.TypeMeta{ Kind: "Secret", APIVersion: "v1", }, - ObjectMeta: k8sMetaV1.ObjectMeta{ + ObjectMeta: k8sMeta.ObjectMeta{ Name: secretName, Annotations: map[string]string{ "certimate/domains": d.option.Domain, - "certimate/alt-names": strings.Join(certificate.DNSNames, ","), - "certimate/common-name": certificate.Subject.CommonName, - "certimate/issuer-organization": strings.Join(certificate.Issuer.Organization, ","), + "certimate/alt-names": strings.Join(certX509.DNSNames, ","), + "certimate/common-name": certX509.Subject.CommonName, + "certimate/issuer-organization": strings.Join(certX509.Issuer.Organization, ","), }, }, - Type: corev1.SecretType("kubernetes.io/tls"), + Type: k8sCore.SecretType("kubernetes.io/tls"), } - secretPayload.Data = make(map[string][]byte) secretPayload.Data[secretDataKeyForCrt] = []byte(d.option.Certificate.Certificate) secretPayload.Data[secretDataKeyForKey] = []byte(d.option.Certificate.PrivateKey) // 获取 Secret 实例 - _, err = client.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMetaV1.GetOptions{}) + _, err = d.k8sClient.CoreV1().Secrets(namespace).Get(context.TODO(), secretName, k8sMeta.GetOptions{}) if err != nil { - _, err = client.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMetaV1.CreateOptions{}) + _, err = d.k8sClient.CoreV1().Secrets(namespace).Create(context.TODO(), &secretPayload, k8sMeta.CreateOptions{}) if err != nil { - return fmt.Errorf("failed to create k8s secret: %w", err) + return xerrors.Wrap(err, "failed to create k8s secret") } else { d.infos = append(d.infos, toStr("Certificate has been created in K8s Secret", nil)) return nil @@ -108,9 +101,9 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error { } // 更新 Secret 实例 - _, err = client.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMetaV1.UpdateOptions{}) + _, err = d.k8sClient.CoreV1().Secrets(namespace).Update(context.TODO(), &secretPayload, k8sMeta.UpdateOptions{}) if err != nil { - return fmt.Errorf("failed to update k8s secret: %w", err) + return xerrors.Wrap(err, "failed to update k8s secret") } d.infos = append(d.infos, toStr("Certificate has been updated to K8s Secret", nil)) @@ -118,7 +111,7 @@ func (d *K8sSecretDeployer) Deploy(ctx context.Context) error { return nil } -func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) { +func (d *K8sSecretDeployer) createK8sClient(access *domain.KubernetesAccess) (*kubernetes.Clientset, error) { var config *rest.Config var err error if access.KubeConfig == "" { @@ -129,7 +122,6 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube return nil, err } config, err = kubeConfig.ClientConfig() - } if err != nil { return nil, err @@ -139,5 +131,6 @@ func (d *K8sSecretDeployer) createClient(access *domain.KubernetesAccess) (*kube if err != nil { return nil, err } + return client, nil } diff --git a/internal/deployer/local.go b/internal/deployer/local.go index 0c839fa4..eedec98b 100644 --- a/internal/deployer/local.go +++ b/internal/deployer/local.go @@ -3,12 +3,13 @@ package deployer import ( "bytes" "context" - "encoding/json" + "errors" "fmt" "os/exec" "runtime" - "github.com/usual2970/certimate/internal/domain" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/pkg/utils/fs" ) @@ -40,22 +41,17 @@ func (d *LocalDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *LocalDeployer) GetInfo() []string { +func (d *LocalDeployer) GetInfos() []string { return []string{} } func (d *LocalDeployer) Deploy(ctx context.Context) error { - access := &domain.LocalAccess{} - if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return err - } - // 执行前置命令 preCommand := d.option.DeployConfig.GetConfigAsString("preCommand") if preCommand != "" { stdout, stderr, err := d.execCommand(preCommand) if err != nil { - return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run pre-command, stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("执行前置命令成功", stdout)) @@ -65,13 +61,13 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error { switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { case certFormatPEM: if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { - return fmt.Errorf("failed to save certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("保存证书成功", nil)) if err := fs.WriteFileString(d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { - return fmt.Errorf("failed to save private key file: %w", err) + return err } d.infos = append(d.infos, toStr("保存私钥成功", nil)) @@ -83,11 +79,11 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("pfxPassword"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { - return fmt.Errorf("failed to save certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("保存证书成功", nil)) @@ -101,11 +97,11 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("jksStorepass"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { - return fmt.Errorf("failed to save certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("保存证书成功", nil)) @@ -116,7 +112,7 @@ func (d *LocalDeployer) Deploy(ctx context.Context) error { if command != "" { stdout, stderr, err := d.execCommand(command) if err != nil { - return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("执行命令成功", stdout)) @@ -146,7 +142,7 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) { } default: - return "", "", fmt.Errorf("unsupported shell") + return "", "", errors.New("unsupported shell") } var stdoutBuf bytes.Buffer @@ -156,8 +152,8 @@ func (d *LocalDeployer) execCommand(command string) (string, string, error) { err := cmd.Run() if err != nil { - return "", "", fmt.Errorf("failed to execute script: %w", err) + return "", "", xerrors.Wrap(err, "failed to execute shell script") } - return stdoutBuf.String(), stderrBuf.String(), err + return stdoutBuf.String(), stderrBuf.String(), nil } diff --git a/internal/deployer/qiniu_cdn.go b/internal/deployer/qiniu_cdn.go index c17c5241..a88bcf21 100644 --- a/internal/deployer/qiniu_cdn.go +++ b/internal/deployer/qiniu_cdn.go @@ -1,36 +1,54 @@ package deployer import ( - "bytes" "context" "encoding/json" "fmt" - "io" - "net/http" + xerrors "github.com/pkg/errors" "github.com/qiniu/go-sdk/v7/auth" "github.com/usual2970/certimate/internal/domain" - xhttp "github.com/usual2970/certimate/internal/utils/http" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderQiniu "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/qiniu-sslcert" + qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk" ) -const qiniuGateway = "http://api.qiniu.com" - type QiniuCDNDeployer struct { - option *DeployerOption - info []string - credentials *auth.Credentials + option *DeployerOption + infos []string + + sdkClient *qiniuEx.Client + sslUploader uploader.Uploader } -func NewQiniuCDNDeployer(option *DeployerOption) (*QiniuCDNDeployer, error) { +func NewQiniuCDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.QiniuAccess{} - json.Unmarshal([]byte(option.Access), access) + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, xerrors.Wrap(err, "failed to get access") + } + + client, err := (&QiniuCDNDeployer{}).createSdkClient( + access.AccessKey, + access.SecretKey, + ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + uploader, err := uploaderQiniu.New(&uploaderQiniu.QiniuSSLCertUploaderConfig{ + AccessKey: access.AccessKey, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &QiniuCDNDeployer{ - option: option, - info: make([]string, 0), - - credentials: auth.New(access.AccessKey, access.SecretKey), + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, }, nil } @@ -38,177 +56,52 @@ func (d *QiniuCDNDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *QiniuCDNDeployer) GetInfo() []string { - return d.info +func (d *QiniuCDNDeployer) GetInfos() []string { + return d.infos } func (d *QiniuCDNDeployer) Deploy(ctx context.Context) error { // 上传证书 - certId, err := d.uploadCert() + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("uploadCert failed: %w", err) + return err } + d.infos = append(d.infos, toStr("已上传证书", upres)) + // 获取域名信息 - domainInfo, err := d.getDomainInfo() + // REF: https://developer.qiniu.com/fusion/4246/the-domain-name + domain := d.option.DeployConfig.GetConfigAsString("domain") + getDomainInfoResp, err := d.sdkClient.GetDomainInfo(domain) if err != nil { - return fmt.Errorf("getDomainInfo failed: %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDomainInfo'") } - // 判断域名是否启用 https - if domainInfo.Https != nil && domainInfo.Https.CertID != "" { - // 启用了 https - // 修改域名证书 - err = d.modifyDomainCert(certId, domainInfo.Https.ForceHttps, domainInfo.Https.Http2Enable) + d.infos = append(d.infos, toStr("已获取域名信息", getDomainInfoResp)) + + // 判断域名是否已启用 HTTPS。如果已启用,修改域名证书;否则,启用 HTTPS + // REF: https://developer.qiniu.com/fusion/4246/the-domain-name + if getDomainInfoResp.Https != nil && getDomainInfoResp.Https.CertID != "" { + modifyDomainHttpsConfResp, err := d.sdkClient.ModifyDomainHttpsConf(domain, upres.CertId, getDomainInfoResp.Https.ForceHttps, getDomainInfoResp.Https.Http2Enable) if err != nil { - return fmt.Errorf("modifyDomainCert failed: %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.ModifyDomainHttpsConf'") } + + d.infos = append(d.infos, toStr("已修改域名证书", modifyDomainHttpsConfResp)) } else { - // 没启用 https - // 启用 https - err = d.enableHttps(certId) + enableDomainHttpsResp, err := d.sdkClient.EnableDomainHttps(domain, upres.CertId, true, true) if err != nil { - return fmt.Errorf("enableHttps failed: %w", err) + return xerrors.Wrap(err, "failed to execute sdk request 'cdn.EnableDomainHttps'") } + + d.infos = append(d.infos, toStr("已将域名升级为 HTTPS", enableDomainHttpsResp)) } return nil } -func (d *QiniuCDNDeployer) enableHttps(certId string) error { - domain := d.option.DeployConfig.GetDomain() - path := fmt.Sprintf("/domain/%s/sslize", domain) - - body := &qiniuModifyDomainCertReq{ - CertID: certId, - ForceHttps: true, - Http2Enable: true, - } - - bodyBytes, err := json.Marshal(body) - if err != nil { - return fmt.Errorf("enable https failed: %w", err) - } - - _, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes)) - if err != nil { - return fmt.Errorf("enable https failed: %w", err) - } - - return nil -} - -type qiniuDomainInfo struct { - Https *qiniuModifyDomainCertReq `json:"https"` -} - -func (d *QiniuCDNDeployer) getDomainInfo() (*qiniuDomainInfo, error) { - domain := d.option.DeployConfig.GetDomain() - - path := fmt.Sprintf("/domain/%s", domain) - - res, err := d.req(qiniuGateway+path, http.MethodGet, nil) - if err != nil { - return nil, fmt.Errorf("req failed: %w", err) - } - - resp := &qiniuDomainInfo{} - err = json.Unmarshal(res, resp) - if err != nil { - return nil, fmt.Errorf("json.Unmarshal failed: %w", err) - } - - return resp, nil -} - -type qiniuUploadCertReq struct { - Name string `json:"name"` - CommonName string `json:"common_name"` - Pri string `json:"pri"` - Ca string `json:"ca"` -} - -type qiniuUploadCertResp struct { - CertID string `json:"certID"` -} - -func (d *QiniuCDNDeployer) uploadCert() (string, error) { - path := "/sslcert" - - body := &qiniuUploadCertReq{ - Name: getDeployString(d.option.DeployConfig, "domain"), - CommonName: getDeployString(d.option.DeployConfig, "domain"), - Pri: d.option.Certificate.PrivateKey, - Ca: d.option.Certificate.Certificate, - } - - bodyBytes, err := json.Marshal(body) - if err != nil { - return "", fmt.Errorf("json.Marshal failed: %w", err) - } - - res, err := d.req(qiniuGateway+path, http.MethodPost, bytes.NewReader(bodyBytes)) - if err != nil { - return "", fmt.Errorf("req failed: %w", err) - } - resp := &qiniuUploadCertResp{} - err = json.Unmarshal(res, resp) - if err != nil { - return "", fmt.Errorf("json.Unmarshal failed: %w", err) - } - - return resp.CertID, nil -} - -type qiniuModifyDomainCertReq struct { - CertID string `json:"certId"` - ForceHttps bool `json:"forceHttps"` - Http2Enable bool `json:"http2Enable"` -} - -func (d *QiniuCDNDeployer) modifyDomainCert(certId string, forceHttps, http2Enable bool) error { - domain := d.option.DeployConfig.GetDomain() - path := fmt.Sprintf("/domain/%s/httpsconf", domain) - - body := &qiniuModifyDomainCertReq{ - CertID: certId, - ForceHttps: forceHttps, - Http2Enable: http2Enable, - } - - bodyBytes, err := json.Marshal(body) - if err != nil { - return fmt.Errorf("json.Marshal failed: %w", err) - } - - _, err = d.req(qiniuGateway+path, http.MethodPut, bytes.NewReader(bodyBytes)) - if err != nil { - return fmt.Errorf("req failed: %w", err) - } - - return nil -} - -func (d *QiniuCDNDeployer) req(url, method string, body io.Reader) ([]byte, error) { - req := xhttp.BuildReq(url, method, body, map[string]string{ - "Content-Type": "application/json", - }) - - if err := d.credentials.AddToken(auth.TokenQBox, req); err != nil { - return nil, fmt.Errorf("credentials.AddToken failed: %w", err) - } - - respBody, err := xhttp.ToRequest(req) - if err != nil { - return nil, fmt.Errorf("ToRequest failed: %w", err) - } - - defer respBody.Close() - - res, err := io.ReadAll(respBody) - if err != nil { - return nil, fmt.Errorf("io.ReadAll failed: %w", err) - } - - return res, nil +func (u *QiniuCDNDeployer) createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) { + credential := auth.New(accessKey, secretKey) + client := qiniuEx.NewClient(credential) + return client, nil } diff --git a/internal/deployer/qiniu_cdn_test.go b/internal/deployer/qiniu_cdn_test.go deleted file mode 100644 index 67012bfc..00000000 --- a/internal/deployer/qiniu_cdn_test.go +++ /dev/null @@ -1,85 +0,0 @@ -package deployer - -import ( - "testing" - - "github.com/qiniu/go-sdk/v7/auth" - - "github.com/usual2970/certimate/internal/applicant" -) - -func Test_qiuniu_uploadCert(t *testing.T) { - type fields struct { - option *DeployerOption - } - tests := []struct { - name string - fields fields - want string - wantErr bool - }{ - { - name: "test", - fields: fields{ - option: &DeployerOption{ - DomainId: "1", - Domain: "example.com", - Access: `{"bucket":"test","accessKey":"","secretKey":""}`, - Certificate: applicant.Certificate{ - Certificate: "", - PrivateKey: "", - }, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - q, _ := NewQiniuCDNDeployer(tt.fields.option) - got, err := q.uploadCert() - if (err != nil) != tt.wantErr { - t.Errorf("qiuniu.uploadCert() error = %v, wantErr %v", err, tt.wantErr) - return - } - if got != tt.want { - t.Errorf("qiuniu.uploadCert() = %v, want %v", got, tt.want) - } - }) - } -} - -func Test_qiuniu_modifyDomainCert(t *testing.T) { - type fields struct { - option *DeployerOption - info []string - credentials *auth.Credentials - } - type args struct { - certId string - } - tests := []struct { - name string - fields fields - args args - wantErr bool - }{ - { - name: "test", - fields: fields{ - option: &DeployerOption{ - DomainId: "1", - Domain: "jt1.ikit.fun", - Access: `{"bucket":"test","accessKey":"","secretKey":""}`, - }, - }, - }, - } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - q, _ := NewQiniuCDNDeployer(tt.fields.option) - if err := q.modifyDomainCert(tt.args.certId, true, true); (err != nil) != tt.wantErr { - t.Errorf("qiuniu.modifyDomainCert() error = %v, wantErr %v", err, tt.wantErr) - } - }) - } -} diff --git a/internal/deployer/ssh.go b/internal/deployer/ssh.go index 98c4de48..a50fcab6 100644 --- a/internal/deployer/ssh.go +++ b/internal/deployer/ssh.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" + xerrors "github.com/pkg/errors" "github.com/pkg/sftp" "golang.org/x/crypto/ssh" @@ -31,7 +32,7 @@ func (d *SSHDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *SSHDeployer) GetInfo() []string { +func (d *SSHDeployer) GetInfos() []string { return d.infos } @@ -55,7 +56,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { if preCommand != "" { stdout, stderr, err := d.sshExecCommand(client, preCommand) if err != nil { - return fmt.Errorf("failed to run pre-command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run pre-command: stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("SSH 执行前置命令成功", stdout)) @@ -65,13 +66,13 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { switch d.option.DeployConfig.GetConfigOrDefaultAsString("format", certFormatPEM) { case certFormatPEM: if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("certPath"), d.option.Certificate.Certificate); err != nil { - return fmt.Errorf("failed to upload certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) if err := d.writeSftpFileString(client, d.option.DeployConfig.GetConfigAsString("keyPath"), d.option.Certificate.PrivateKey); err != nil { - return fmt.Errorf("failed to upload private key file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传私钥成功", nil)) @@ -83,11 +84,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("pfxPassword"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := d.writeSftpFile(client, d.option.DeployConfig.GetConfigAsString("certPath"), pfxData); err != nil { - return fmt.Errorf("failed to upload certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("SSH 上传证书成功", nil)) @@ -101,11 +102,11 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { d.option.DeployConfig.GetConfigAsString("jksStorepass"), ) if err != nil { - return fmt.Errorf("failed to convert pem to pfx %w", err) + return err } if err := fs.WriteFile(d.option.DeployConfig.GetConfigAsString("certPath"), jksData); err != nil { - return fmt.Errorf("failed to save certificate file: %w", err) + return err } d.infos = append(d.infos, toStr("保存证书成功", nil)) @@ -116,7 +117,7 @@ func (d *SSHDeployer) Deploy(ctx context.Context) error { if command != "" { stdout, stderr, err := d.sshExecCommand(client, command) if err != nil { - return fmt.Errorf("failed to run command: %w, stdout: %s, stderr: %s", err, stdout, stderr) + return xerrors.Wrapf(err, "failed to run command, stdout: %s, stderr: %s", stdout, stderr) } d.infos = append(d.infos, toStr("SSH 执行命令成功", stdout)) @@ -158,7 +159,7 @@ func (d *SSHDeployer) createSshClient(access *domain.SSHAccess) (*ssh.Client, er func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string, string, error) { session, err := client.NewSession() if err != nil { - return "", "", fmt.Errorf("failed to create ssh session: %w", err) + return "", "", xerrors.Wrap(err, "failed to create ssh session") } defer session.Close() @@ -167,7 +168,11 @@ func (d *SSHDeployer) sshExecCommand(client *ssh.Client, command string) (string var stderrBuf bytes.Buffer session.Stderr = &stderrBuf err = session.Run(command) - return stdoutBuf.String(), stderrBuf.String(), err + if err != nil { + return "", "", xerrors.Wrap(err, "failed to execute ssh script") + } + + return stdoutBuf.String(), stderrBuf.String(), nil } func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, content string) error { @@ -177,23 +182,23 @@ func (d *SSHDeployer) writeSftpFileString(client *ssh.Client, path string, conte func (d *SSHDeployer) writeSftpFile(client *ssh.Client, path string, data []byte) error { sftpCli, err := sftp.NewClient(client) if err != nil { - return fmt.Errorf("failed to create sftp client: %w", err) + return xerrors.Wrap(err, "failed to create sftp client") } defer sftpCli.Close() if err := sftpCli.MkdirAll(filepath.Dir(path)); err != nil { - return fmt.Errorf("failed to create remote directory: %w", err) + return xerrors.Wrap(err, "failed to create remote directory") } file, err := sftpCli.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC) if err != nil { - return fmt.Errorf("failed to open remote file: %w", err) + return xerrors.Wrap(err, "failed to open remote file") } defer file.Close() _, err = file.Write(data) if err != nil { - return fmt.Errorf("failed to write to remote file: %w", err) + return xerrors.Wrap(err, "failed to write to remote file") } return nil diff --git a/internal/deployer/tencent_cdn.go b/internal/deployer/tencent_cdn.go index 566d5b9b..a0a44b6a 100644 --- a/internal/deployer/tencent_cdn.go +++ b/internal/deployer/tencent_cdn.go @@ -6,38 +6,58 @@ import ( "fmt" "strings" - "golang.org/x/exp/slices" - - cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" + xerrors "github.com/pkg/errors" + tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + "golang.org/x/exp/slices" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) type TencentCDNDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentCDNDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentCDNDeployerSdkClients struct { + ssl *tcSsl.Client + cdn *tcCdn.Client } func NewTencentCDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentCDNDeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCDNDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -45,146 +65,129 @@ func (d *TencentCDNDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *TencentCDNDeployer) GetInfo() []string { +func (d *TencentCDNDeployer) GetInfos() []string { return d.infos } func (d *TencentCDNDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 获取待部署的 CDN 实例 + // 如果是泛域名,根据证书匹配 CDN 实例 + tcInstanceIds := make([]string, 0) + domain := d.option.DeployConfig.GetConfigAsString("domain") + if strings.HasPrefix(domain, "*") { + domains, err := d.getDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + tcInstanceIds = domains + } else { + tcInstanceIds = append(tcInstanceIds, domain) } + // 跳过已部署的 CDN 实例 + if len(tcInstanceIds) > 0 { + deployedDomains, err := d.getDeployedDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + temp := make([]string, 0) + for _, aliInstanceId := range tcInstanceIds { + if !slices.Contains(deployedDomains, aliInstanceId) { + temp = append(temp, aliInstanceId) + } + } + tcInstanceIds = temp + } + if len(tcInstanceIds) == 0 { + d.infos = append(d.infos, "已部署过或没有要部署的 CDN 实例") + return nil + } + + // 证书部署到 CDN 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(tcInstanceIds) + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil } -func (d *TencentCDNDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentCDNDeployer) createSdkClients(secretId, secretKey string) (*tencentCDNDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return &tencentCDNDeployerSdkClients{ + ssl: sslClient, + cdn: cdnClient, + }, nil } -func (d *TencentCDNDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("cdn") - request.Status = common.Int64Ptr(1) - - // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名 - domain := getDeployString(d.option.DeployConfig, "domain") - if strings.Contains(domain, "*") { - list, errGetList := d.getDomainList(certId) - if errGetList != nil { - return fmt.Errorf("failed to get certificate domain list: %w", errGetList) - } - if len(list) == 0 { - d.infos = append(d.infos, "没有需要部署的实例") - return nil - } - request.InstanceIdList = common.StringPtrs(list) - } else { // 否则直接使用传入的域名 - deployed, _ := d.isDomainDeployed(certId, domain) - if deployed { - d.infos = append(d.infos, "域名已部署") - return nil - } else { - request.InstanceIdList = common.StringPtrs([]string{domain}) - } - } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) +func (d *TencentCDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { + // 获取证书中的可用域名 + // REF: https://cloud.tencent.com/document/product/228/42491 + describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() + describeCertDomainsReq.CertId = common.StringPtr(tcCertId) + describeCertDomainsReq.Product = common.StringPtr("cdn") + describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} - -func (d *TencentCDNDeployer) getDomainList(certId string) ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com" - client, _ := cdn.NewClient(d.credential, "", cpf) - - request := cdn.NewDescribeCertDomainsRequest() - - request.CertId = common.StringPtr(certId) - - response, err := client.DescribeCertDomains(request) - if err != nil { - return nil, fmt.Errorf("failed to get domain list: %w", err) - } - - deployedDomains, err := d.getDeployedDomainList(certId) - if err != nil { - return nil, fmt.Errorf("failed to get deployed domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") } domains := make([]string, 0) - for _, domain := range response.Response.Domains { - domainStr := *domain - if !slices.Contains(deployedDomains, domainStr) { - domains = append(domains, domainStr) + if describeCertDomainsResp.Response.Domains == nil { + for _, domain := range describeCertDomainsResp.Response.Domains { + domains = append(domains, *domain) } } return domains, nil } -func (d *TencentCDNDeployer) isDomainDeployed(certId, domain string) (bool, error) { - deployedDomains, err := d.getDeployedDomainList(certId) +func (d *TencentCDNDeployer) getDeployedDomainsByCertificateId(tcCertId string) ([]string, error) { + // 根据证书查询关联 CDN 域名 + // REF: https://cloud.tencent.com/document/product/400/62674 + describeDeployedResourcesReq := tcSsl.NewDescribeDeployedResourcesRequest() + describeDeployedResourcesReq.CertificateIds = common.StringPtrs([]string{tcCertId}) + describeDeployedResourcesReq.ResourceType = common.StringPtr("cdn") + describeDeployedResourcesResp, err := d.sdkClients.ssl.DescribeDeployedResources(describeDeployedResourcesReq) if err != nil { - return false, err - } - - return slices.Contains(deployedDomains, domain), nil -} - -func (d *TencentCDNDeployer) getDeployedDomainList(certId string) ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewDescribeDeployedResourcesRequest() - request.CertificateIds = common.StringPtrs([]string{certId}) - request.ResourceType = common.StringPtr("cdn") - - response, err := client.DescribeDeployedResources(request) - if err != nil { - return nil, fmt.Errorf("failed to get deployed domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeDeployedResources'") } domains := make([]string, 0) - for _, domain := range response.Response.DeployedResources[0].Resources { - domains = append(domains, *domain) + if describeDeployedResourcesResp.Response.DeployedResources != nil { + for _, deployedResource := range describeDeployedResourcesResp.Response.DeployedResources { + for _, resource := range deployedResource.Resources { + domains = append(domains, *resource) + } + } } return domains, nil diff --git a/internal/deployer/tencent_clb.go b/internal/deployer/tencent_clb.go index ba97e840..0a0c4b82 100644 --- a/internal/deployer/tencent_clb.go +++ b/internal/deployer/tencent_clb.go @@ -3,37 +3,61 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" + xerrors "github.com/pkg/errors" + tcClb "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/clb/v20180317" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) type TencentCLBDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentCLBDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentCLBDeployerSdkClients struct { + ssl *tcSsl.Client + clb *tcClb.Client } func NewTencentCLBDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentCLBDeployer{}).createSdkClients( access.SecretId, access.SecretKey, + option.DeployConfig.GetConfigAsString("region"), ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCLBDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -41,77 +65,266 @@ func (d *TencentCLBDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *TencentCLBDeployer) GetInfo() []string { +func (d *TencentCLBDeployer) GetInfos() []string { return d.infos } func (d *TencentCLBDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) - } - d.infos = append(d.infos, toStr("上传证书", certId)) + // TODO: 直接部署方式 - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "ssl-deploy": + // 通过 SSL 服务部署到云资源实例 + err := d.deployToInstanceUseSsl(ctx) + if err != nil { + return err + } + case "loadbalancer": + // 部署到指定负载均衡器 + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + // 部署到指定监听器 + if err := d.deployToListener(ctx); err != nil { + return err + } + case "ruledomain": + // 部署到指定七层监听转发规则域名 + if err := d.deployToRuleDomain(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") } return nil } -func (d *TencentCLBDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentCLBDeployer) createSdkClients(secretId, secretKey, region string) (*tencentCLBDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + clbClient, err := tcClb.NewClient(credential, region, profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return &tencentCLBDeployerSdkClients{ + ssl: sslClient, + clb: clbClient, + }, nil } -func (d *TencentCLBDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("clb") - request.Status = common.Int64Ptr(1) - - clbId := getDeployString(d.option.DeployConfig, "clbId") - lsnId := getDeployString(d.option.DeployConfig, "lsnId") - domain := getDeployString(d.option.DeployConfig, "domain") - - if(domain == ""){ - // 未开启SNI,只需要精确到监听器 - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", clbId, lsnId)}) - }else{ - // 开启SNI,需要精确到域名,支持泛域名 - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", clbId, lsnId, domain)}) +func (d *TencentCLBDeployer) deployToInstanceUseSsl(ctx context.Context) error { + tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + tcDomain := d.option.DeployConfig.GetConfigAsString("domain") + if tcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + if tcListenerId == "" { + return errors.New("`listenerId` is required") } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) + + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 证书部署到 CLB 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("clb") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + if tcDomain == "" { + // 未开启 SNI,只需指定到监听器 + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s", tcLoadbalancerId, tcListenerId)}) + } else { + // 开启 SNI,需指定到域名(支持泛域名) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s|%s|%s", tcLoadbalancerId, tcListenerId, tcDomain)}) + } + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil -} \ No newline at end of file +} + +func (d *TencentCLBDeployer) deployToLoadbalancer(ctx context.Context) error { + tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + tcListenerIds := make([]string, 0) + if tcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + // 查询负载均衡器详细信息 + // REF: https://cloud.tencent.com/document/api/214/46916 + describeLoadBalancersDetailReq := tcClb.NewDescribeLoadBalancersDetailRequest() + describeLoadBalancersDetailResp, err := d.sdkClients.clb.DescribeLoadBalancersDetail(describeLoadBalancersDetailReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeLoadBalancersDetail'") + } + + d.infos = append(d.infos, toStr("已查询到负载均衡详细信息", describeLoadBalancersDetailResp)) + + // 查询监听器列表 + // REF: https://cloud.tencent.com/document/api/214/30686 + describeListenersReq := tcClb.NewDescribeListenersRequest() + describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) + describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'") + } else { + if describeListenersResp.Response.Listeners != nil { + for _, listener := range describeListenersResp.Response.Listeners { + if listener.Protocol == nil || (*listener.Protocol != "HTTPS" && *listener.Protocol != "TCP_SSL" && *listener.Protocol != "QUIC") { + continue + } + + tcListenerIds = append(tcListenerIds, *listener.ListenerId) + } + } + } + + d.infos = append(d.infos, toStr("已查询到负载均衡器下的监听器", tcListenerIds)) + + // 上传证书到 SCM + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 批量更新监听器证书 + var errs []error + for _, tcListenerId := range tcListenerIds { + if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *TencentCLBDeployer) deployToListener(ctx context.Context) error { + tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + if tcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + if tcListenerId == "" { + return errors.New("`listenerId` is required") + } + + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 更新监听器证书 + if err := d.modifyListenerCertificate(ctx, tcLoadbalancerId, tcListenerId, upres.CertId); err != nil { + return err + } + + return nil +} + +func (d *TencentCLBDeployer) deployToRuleDomain(ctx context.Context) error { + tcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + tcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + tcDomain := d.option.DeployConfig.GetConfigAsString("domain") + if tcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + if tcListenerId == "" { + return errors.New("`listenerId` is required") + } + if tcDomain == "" { + return errors.New("`domain` is required") + } + + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 修改负载均衡七层监听器转发规则的域名级别属性 + // REF: https://cloud.tencent.com/document/api/214/38092 + modifyDomainAttributesReq := tcClb.NewModifyDomainAttributesRequest() + modifyDomainAttributesReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) + modifyDomainAttributesReq.ListenerId = common.StringPtr(tcListenerId) + modifyDomainAttributesReq.Domain = common.StringPtr(tcDomain) + modifyDomainAttributesReq.Certificate = &tcClb.CertificateInput{ + SSLMode: common.StringPtr("UNIDIRECTIONAL"), + CertId: common.StringPtr(upres.CertId), + } + modifyDomainAttributesResp, err := d.sdkClients.clb.ModifyDomainAttributes(modifyDomainAttributesReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyDomainAttributes'") + } + + d.infos = append(d.infos, toStr("已修改七层监听器转发规则的域名级别属性", modifyDomainAttributesResp.Response)) + + return nil +} + +func (d *TencentCLBDeployer) modifyListenerCertificate(ctx context.Context, tcLoadbalancerId, tcListenerId, tcCertId string) error { + // 查询监听器列表 + // REF: https://cloud.tencent.com/document/api/214/30686 + describeListenersReq := tcClb.NewDescribeListenersRequest() + describeListenersReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) + describeListenersReq.ListenerIds = common.StringPtrs([]string{tcListenerId}) + describeListenersResp, err := d.sdkClients.clb.DescribeListeners(describeListenersReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'clb.DescribeListeners'") + } + if len(describeListenersResp.Response.Listeners) == 0 { + d.infos = append(d.infos, toStr("未找到监听器", nil)) + return errors.New("listener not found") + } + + d.infos = append(d.infos, toStr("已查询到监听器属性", describeListenersResp.Response)) + + // 修改监听器属性 + // REF: https://cloud.tencent.com/document/product/214/30681 + modifyListenerReq := tcClb.NewModifyListenerRequest() + modifyListenerReq.LoadBalancerId = common.StringPtr(tcLoadbalancerId) + modifyListenerReq.ListenerId = common.StringPtr(tcListenerId) + modifyListenerReq.Certificate = &tcClb.CertificateInput{CertId: common.StringPtr(tcCertId)} + if describeListenersResp.Response.Listeners[0].Certificate != nil && describeListenersResp.Response.Listeners[0].Certificate.SSLMode != nil { + modifyListenerReq.Certificate.SSLMode = describeListenersResp.Response.Listeners[0].Certificate.SSLMode + modifyListenerReq.Certificate.CertCaId = describeListenersResp.Response.Listeners[0].Certificate.CertCaId + } else { + modifyListenerReq.Certificate.SSLMode = common.StringPtr("UNIDIRECTIONAL") + } + modifyListenerResp, err := d.sdkClients.clb.ModifyListener(modifyListenerReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'clb.ModifyListener'") + } + + d.infos = append(d.infos, toStr("已修改监听器属性", modifyListenerResp.Response)) + + return nil +} diff --git a/internal/deployer/tencent_cos.go b/internal/deployer/tencent_cos.go index 2ea8c7d6..71039b87 100644 --- a/internal/deployer/tencent_cos.go +++ b/internal/deployer/tencent_cos.go @@ -3,37 +3,54 @@ package deployer import ( "context" "encoding/json" + "errors" "fmt" + xerrors "github.com/pkg/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) type TencentCOSDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClient *tcSsl.Client + sslUploader uploader.Uploader } func NewTencentCOSDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + client, err := (&TencentCOSDeployer{}).createSdkClient( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentCOSDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, }, nil } @@ -41,68 +58,49 @@ func (d *TencentCOSDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *TencentCOSDeployer) GetInfo() []string { +func (d *TencentCOSDeployer) GetInfos() []string { return d.infos } func (d *TencentCOSDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + tcRegion := d.option.DeployConfig.GetConfigAsString("region") + tcBucket := d.option.DeployConfig.GetConfigAsString("bucket") + tcDomain := d.option.DeployConfig.GetConfigAsString("domain") + if tcBucket == "" { + return errors.New("`bucket` is required") } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err } + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 证书部署到 COS 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("cos") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", tcRegion, tcBucket, tcDomain)}) + deployCertificateInstanceResp, err := d.sdkClient.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) + return nil } -// 上传证书,与CDN部署的上传方法一致。 -func (d *TencentCOSDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) +func (d *TencentCOSDeployer) createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) { + credential := common.NewCredential(secretId, secretKey) + client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + return client, nil } - -func (d *TencentCOSDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, getDeployString(d.option.DeployConfig, "region"), cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("cos") - request.Status = common.Int64Ptr(1) - - domain := getDeployString(d.option.DeployConfig, "domain") - request.InstanceIdList = common.StringPtrs([]string{fmt.Sprintf("%s#%s#%s", getDeployString(d.option.DeployConfig, "region"), getDeployString(d.option.DeployConfig, "bucket"), domain)}) - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) - if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} \ No newline at end of file diff --git a/internal/deployer/tencent_ecdn.go b/internal/deployer/tencent_ecdn.go index ea52794e..a61c5680 100644 --- a/internal/deployer/tencent_ecdn.go +++ b/internal/deployer/tencent_ecdn.go @@ -2,41 +2,61 @@ package deployer import ( "context" - "encoding/base64" "encoding/json" "fmt" "strings" - cdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" + xerrors "github.com/pkg/errors" + tcCdn "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/cdn/v20180606" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) type TencentECDNDeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentECDNDeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentECDNDeployerSdkClients struct { + ssl *tcSsl.Client + cdn *tcCdn.Client } func NewTencentECDNDeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentECDNDeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentECDNDeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -44,102 +64,90 @@ func (d *TencentECDNDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *TencentECDNDeployer) GetInfo() []string { +func (d *TencentECDNDeployer) GetInfos() []string { return d.infos } func (d *TencentECDNDeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + return err } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 获取待部署的 ECDN 实例 + // 如果是泛域名,根据证书匹配 ECDN 实例 + aliInstanceIds := make([]string, 0) + domain := d.option.DeployConfig.GetConfigAsString("domain") + if strings.HasPrefix(domain, "*") { + domains, err := d.getDomainsByCertificateId(upres.CertId) + if err != nil { + return err + } + + aliInstanceIds = domains + } else { + aliInstanceIds = append(aliInstanceIds, domain) } + if len(aliInstanceIds) == 0 { + d.infos = append(d.infos, "没有要部署的 ECDN 实例") + return nil + } + + // 证书部署到 ECDN 实例 + // REF: https://cloud.tencent.com/document/product/400/91667 + deployCertificateInstanceReq := tcSsl.NewDeployCertificateInstanceRequest() + deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) + deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") + deployCertificateInstanceReq.Status = common.Int64Ptr(1) + deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(aliInstanceIds) + deployCertificateInstanceResp, err := d.sdkClients.ssl.DeployCertificateInstance(deployCertificateInstanceReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'ssl.DeployCertificateInstance'") + } + + d.infos = append(d.infos, toStr("已部署证书到云资源实例", deployCertificateInstanceResp.Response)) return nil } -func (d *TencentECDNDeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentECDNDeployer) createSdkClients(secretId, secretKey string) (*tencentECDNDeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil + cdnClient, err := tcCdn.NewClient(credential, "", profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return &tencentECDNDeployerSdkClients{ + ssl: sslClient, + cdn: cdnClient, + }, nil } -func (d *TencentECDNDeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := ssl.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := ssl.NewDeployCertificateInstanceRequest() - - request.CertificateId = common.StringPtr(certId) - request.ResourceType = common.StringPtr("ecdn") - request.Status = common.Int64Ptr(1) - - // 如果是泛域名就从cdn列表下获取SSL证书中的可用域名 - domain := getDeployString(d.option.DeployConfig, "domain") - if strings.Contains(domain, "*") { - list, errGetList := d.getDomainList() - if errGetList != nil { - return fmt.Errorf("failed to get certificate domain list: %w", errGetList) - } - if list == nil || len(list) == 0 { - return fmt.Errorf("failed to get certificate domain list: empty list.") - } - request.InstanceIdList = common.StringPtrs(list) - } else { // 否则直接使用传入的域名 - request.InstanceIdList = common.StringPtrs([]string{domain}) - } - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.DeployCertificateInstance(request) +func (d *TencentECDNDeployer) getDomainsByCertificateId(tcCertId string) ([]string, error) { + // 获取证书中的可用域名 + // REF: https://cloud.tencent.com/document/product/228/42491 + describeCertDomainsReq := tcCdn.NewDescribeCertDomainsRequest() + describeCertDomainsReq.CertId = common.StringPtr(tcCertId) + describeCertDomainsReq.Product = common.StringPtr("ecdn") + describeCertDomainsResp, err := d.sdkClients.cdn.DescribeCertDomains(describeCertDomainsReq) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) - } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil -} - -func (d *TencentECDNDeployer) getDomainList() ([]string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "cdn.tencentcloudapi.com" - client, _ := cdn.NewClient(d.credential, "", cpf) - - request := cdn.NewDescribeCertDomainsRequest() - - cert := base64.StdEncoding.EncodeToString([]byte(d.option.Certificate.Certificate)) - request.Cert = &cert - request.Product = common.StringPtr("ecdn") - - response, err := client.DescribeCertDomains(request) - if err != nil { - return nil, fmt.Errorf("failed to get domain list: %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.DescribeCertDomains'") } domains := make([]string, 0) - for _, domain := range response.Response.Domains { - domains = append(domains, *domain) + if describeCertDomainsResp.Response.Domains == nil { + for _, domain := range describeCertDomainsResp.Response.Domains { + domains = append(domains, *domain) + } } return domains, nil diff --git a/internal/deployer/tencent_teo.go b/internal/deployer/tencent_teo.go index f31aee8d..583089dd 100644 --- a/internal/deployer/tencent_teo.go +++ b/internal/deployer/tencent_teo.go @@ -6,36 +6,57 @@ import ( "fmt" "strings" - teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" + xerrors "github.com/pkg/errors" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - ssl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + tcTeo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" "github.com/usual2970/certimate/internal/domain" - "github.com/usual2970/certimate/internal/utils/rand" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploaderTcSsl "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/tencentcloud-ssl" ) type TencentTEODeployer struct { - option *DeployerOption - credential *common.Credential - infos []string + option *DeployerOption + infos []string + + sdkClients *tencentTEODeployerSdkClients + sslUploader uploader.Uploader +} + +type tencentTEODeployerSdkClients struct { + ssl *tcSsl.Client + teo *tcTeo.Client } func NewTencentTEODeployer(option *DeployerOption) (Deployer, error) { access := &domain.TencentAccess{} if err := json.Unmarshal([]byte(option.Access), access); err != nil { - return nil, fmt.Errorf("failed to unmarshal tencent access: %w", err) + return nil, xerrors.Wrap(err, "failed to get access") } - credential := common.NewCredential( + clients, err := (&TencentTEODeployer{}).createSdkClients( access.SecretId, access.SecretKey, ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk clients") + } + + uploader, err := uploaderTcSsl.New(&uploaderTcSsl.TencentCloudSSLUploaderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } return &TencentTEODeployer{ - option: option, - credential: credential, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClients: clients, + sslUploader: uploader, }, nil } @@ -43,69 +64,56 @@ func (d *TencentTEODeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *TencentTEODeployer) GetInfo() []string { +func (d *TencentTEODeployer) GetInfos() []string { return d.infos } func (d *TencentTEODeployer) Deploy(ctx context.Context) error { - // 上传证书 - certId, err := d.uploadCert() - if err != nil { - return fmt.Errorf("failed to upload certificate: %w", err) + tcZoneId := d.option.DeployConfig.GetConfigAsString("zoneId") + if tcZoneId == "" { + return xerrors.New("`zoneId` is required") } - d.infos = append(d.infos, toStr("上传证书", certId)) - if err := d.deploy(certId); err != nil { - return fmt.Errorf("failed to deploy: %w", err) + // 上传证书到 SSL + upres, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err } + d.infos = append(d.infos, toStr("已上传证书", upres)) + + // 配置域名证书 + // REF: https://cloud.tencent.com/document/product/1552/80764 + modifyHostsCertificateReq := tcTeo.NewModifyHostsCertificateRequest() + modifyHostsCertificateReq.ZoneId = common.StringPtr(tcZoneId) + modifyHostsCertificateReq.Mode = common.StringPtr("sslcert") + modifyHostsCertificateReq.Hosts = common.StringPtrs(strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"), "\n")) + modifyHostsCertificateReq.ServerCertInfo = []*tcTeo.ServerCertInfo{{CertId: common.StringPtr(upres.CertId)}} + modifyHostsCertificateResp, err := d.sdkClients.teo.ModifyHostsCertificate(modifyHostsCertificateReq) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'teo.ModifyHostsCertificate'") + } + + d.infos = append(d.infos, toStr("已配置域名证书", modifyHostsCertificateResp.Response)) + return nil } -func (d *TencentTEODeployer) uploadCert() (string, error) { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "ssl.tencentcloudapi.com" +func (d *TencentTEODeployer) createSdkClients(secretId, secretKey string) (*tencentTEODeployerSdkClients, error) { + credential := common.NewCredential(secretId, secretKey) - client, _ := ssl.NewClient(d.credential, "", cpf) - - request := ssl.NewUploadCertificateRequest() - - request.CertificatePublicKey = common.StringPtr(d.option.Certificate.Certificate) - request.CertificatePrivateKey = common.StringPtr(d.option.Certificate.PrivateKey) - request.Alias = common.StringPtr(d.option.Domain + "_" + rand.RandStr(6)) - request.Repeatable = common.BoolPtr(false) - - response, err := client.UploadCertificate(request) + sslClient, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return "", fmt.Errorf("failed to upload certificate: %w", err) + return nil, err } - return *response.Response.CertificateId, nil -} - -func (d *TencentTEODeployer) deploy(certId string) error { - cpf := profile.NewClientProfile() - cpf.HttpProfile.Endpoint = "teo.tencentcloudapi.com" - // 实例化要请求产品的client对象,clientProfile是可选的 - client, _ := teo.NewClient(d.credential, "", cpf) - - // 实例化一个请求对象,每个接口都会对应一个request对象 - request := teo.NewModifyHostsCertificateRequest() - - request.ZoneId = common.StringPtr(getDeployString(d.option.DeployConfig, "zoneId")) - request.Mode = common.StringPtr("sslcert") - request.ServerCertInfo = []*teo.ServerCertInfo{{ - CertId: common.StringPtr(certId), - }} - - domains := strings.Split(strings.ReplaceAll(d.option.Domain, "\r\n", "\n"),"\n") - request.Hosts = common.StringPtrs(domains) - - // 返回的resp是一个DeployCertificateInstanceResponse的实例,与请求对象对应 - resp, err := client.ModifyHostsCertificate(request) + teoClient, err := tcTeo.NewClient(credential, "", profile.NewClientProfile()) if err != nil { - return fmt.Errorf("failed to deploy certificate: %w", err) + return nil, err } - d.infos = append(d.infos, toStr("部署证书", resp.Response)) - return nil + + return &tencentTEODeployerSdkClients{ + ssl: sslClient, + teo: teoClient, + }, nil } diff --git a/internal/deployer/webhook.go b/internal/deployer/webhook.go index 522705d4..35c37235 100644 --- a/internal/deployer/webhook.go +++ b/internal/deployer/webhook.go @@ -7,6 +7,8 @@ import ( "fmt" "net/http" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/domain" xhttp "github.com/usual2970/certimate/internal/utils/http" ) @@ -27,7 +29,7 @@ func (d *WebhookDeployer) GetID() string { return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) } -func (d *WebhookDeployer) GetInfo() []string { +func (d *WebhookDeployer) GetInfos() []string { return d.infos } @@ -41,26 +43,24 @@ type webhookData struct { func (d *WebhookDeployer) Deploy(ctx context.Context) error { access := &domain.WebhookAccess{} if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return fmt.Errorf("failed to parse hook access config: %w", err) + return xerrors.Wrap(err, "failed to get access") } data := &webhookData{ Domain: d.option.Domain, Certificate: d.option.Certificate.Certificate, PrivateKey: d.option.Certificate.PrivateKey, - Variables: getDeployVariables(d.option.DeployConfig), + Variables: d.option.DeployConfig.GetConfigAsVariables(), } - body, _ := json.Marshal(data) - resp, err := xhttp.Req(access.Url, http.MethodPost, bytes.NewReader(body), map[string]string{ "Content-Type": "application/json", }) if err != nil { - return fmt.Errorf("failed to send hook request: %w", err) + return xerrors.Wrap(err, "failed to send webhook request") } - d.infos = append(d.infos, toStr("webhook response", string(resp))) + d.infos = append(d.infos, toStr("Webhook Response", string(resp))) return nil } diff --git a/internal/domain/domains.go b/internal/domain/domains.go index bed38ac2..b679625f 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -1,6 +1,9 @@ package domain -import "strings" +import ( + "encoding/json" + "strings" +) type ApplyConfig struct { Email string `json:"email"` @@ -117,6 +120,33 @@ func (dc *DeployConfig) GetConfigOrDefaultAsBool(key string, defaultValue bool) return defaultValue } +// 以变量字典形式获取配置项。 +// +// 出参: +// - 变量字典。 +func (dc *DeployConfig) GetConfigAsVariables() map[string]string { + rs := make(map[string]string) + + if dc.Config != nil { + value, ok := dc.Config["variables"] + if !ok { + return rs + } + + kvs := make([]KV, 0) + bts, _ := json.Marshal(value) + if err := json.Unmarshal(bts, &kvs); err != nil { + return rs + } + + for _, kv := range kvs { + rs[kv.Key] = kv.Value + } + } + + return rs +} + // GetDomain returns the domain from the deploy config // if the domain is a wildcard domain, and wildcard is true, return the wildcard domain func (dc *DeployConfig) GetDomain(wildcard ...bool) string { diff --git a/internal/domains/deploy.go b/internal/domains/deploy.go index 23b3a1dd..c9df14d3 100644 --- a/internal/domains/deploy.go +++ b/internal/domains/deploy.go @@ -105,11 +105,11 @@ func deploy(ctx context.Context, record *models.Record) error { if err = deployer.Deploy(ctx); err != nil { app.GetApp().Logger().Error("部署失败", "err", err) - history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfo()}) + history.record(deployPhase, "部署失败", &RecordInfo{Err: err, Info: deployer.GetInfos()}) return err } history.record(deployPhase, fmt.Sprintf("[%s]-部署成功", deployer.GetID()), &RecordInfo{ - Info: deployer.GetInfo(), + Info: deployer.GetInfos(), }, false) } diff --git a/internal/pkg/core/uploader/uploader_aliyun_cas.go b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go similarity index 68% rename from internal/pkg/core/uploader/uploader_aliyun_cas.go rename to internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go index b6a1f792..c524f115 100644 --- a/internal/pkg/core/uploader/uploader_aliyun_cas.go +++ b/internal/pkg/core/uploader/providers/aliyun-cas/aliyun_cas.go @@ -1,4 +1,4 @@ -package uploader +package aliyuncas import ( "context" @@ -6,11 +6,12 @@ import ( "strings" "time" - cas20200407 "github.com/alibabacloud-go/cas-20200407/v3/client" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - util "github.com/alibabacloud-go/tea-utils/v2/service" + aliyunCas "github.com/alibabacloud-go/cas-20200407/v3/client" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/x509" ) @@ -21,29 +22,27 @@ type AliyunCASUploaderConfig struct { } type AliyunCASUploader struct { - config *AliyunCASUploaderConfig - sdkClient *cas20200407.Client - sdkRuntime *util.RuntimeOptions + config *AliyunCASUploaderConfig + sdkClient *aliyunCas.Client } -func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) { - client, err := (&AliyunCASUploader{}).createSdkClient( +func New(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) { + client, err := createSdkClient( config.AccessKeyId, config.AccessKeySecret, config.Region, ) if err != nil { - return nil, fmt.Errorf("failed to create sdk client: %w", err) + return nil, xerrors.Wrap(err, "failed to create sdk client") } return &AliyunCASUploader{ - config: config, - sdkClient: client, - sdkRuntime: &util.RuntimeOptions{}, + config: config, + sdkClient: client, }, nil } -func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { +func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 certX509, err := x509.ParseCertificateFromPEM(certPem) if err != nil { @@ -56,25 +55,25 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP listUserCertificateOrderPage := int64(1) listUserCertificateOrderLimit := int64(50) for { - listUserCertificateOrderReq := &cas20200407.ListUserCertificateOrderRequest{ + listUserCertificateOrderReq := &aliyunCas.ListUserCertificateOrderRequest{ CurrentPage: tea.Int64(listUserCertificateOrderPage), ShowSize: tea.Int64(listUserCertificateOrderLimit), OrderType: tea.String("CERT"), } - listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrderWithOptions(listUserCertificateOrderReq, u.sdkRuntime) + listUserCertificateOrderResp, err := u.sdkClient.ListUserCertificateOrder(listUserCertificateOrderReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cas.ListUserCertificateOrder': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.ListUserCertificateOrder'") } if listUserCertificateOrderResp.Body.CertificateOrderList != nil { for _, certDetail := range listUserCertificateOrderResp.Body.CertificateOrderList { if strings.EqualFold(certX509.SerialNumber.Text(16), *certDetail.SerialNo) { - getUserCertificateDetailReq := &cas20200407.GetUserCertificateDetailRequest{ + getUserCertificateDetailReq := &aliyunCas.GetUserCertificateDetailRequest{ CertId: certDetail.CertificateId, } - getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetailWithOptions(getUserCertificateDetailReq, u.sdkRuntime) + getUserCertificateDetailResp, err := u.sdkClient.GetUserCertificateDetail(getUserCertificateDetailReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cas.GetUserCertificateDetail': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.GetUserCertificateDetail'") } var isSameCert bool @@ -91,7 +90,7 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP // 如果已存在相同证书,直接返回已有的证书信息 if isSameCert { - return &UploadResult{ + return &uploader.UploadResult{ CertId: fmt.Sprintf("%d", tea.Int64Value(certDetail.CertificateId)), CertName: *certDetail.Name, }, nil @@ -116,29 +115,29 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP // 上传新证书 // REF: https://help.aliyun.com/zh/ssl-certificate/developer-reference/api-cas-2020-04-07-uploadusercertificate - uploadUserCertificateReq := &cas20200407.UploadUserCertificateRequest{ + uploadUserCertificateReq := &aliyunCas.UploadUserCertificateRequest{ Name: tea.String(certName), Cert: tea.String(certPem), Key: tea.String(privkeyPem), } - uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificateWithOptions(uploadUserCertificateReq, u.sdkRuntime) + uploadUserCertificateResp, err := u.sdkClient.UploadUserCertificate(uploadUserCertificateReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'cas.UploadUserCertificate': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cas.UploadUserCertificate'") } certId = fmt.Sprintf("%d", tea.Int64Value(uploadUserCertificateResp.Body.CertId)) - return &UploadResult{ + return &uploader.UploadResult{ CertId: certId, CertName: certName, }, nil } -func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*cas20200407.Client, error) { +func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunCas.Client, error) { if region == "" { region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 } - aConfig := &openapi.Config{ + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } @@ -152,7 +151,7 @@ func (u *AliyunCASUploader) createSdkClient(accessKeyId, accessKeySecret, region } aConfig.Endpoint = tea.String(endpoint) - client, err := cas20200407.NewClient(aConfig) + client, err := aliyunCas.NewClient(aConfig) if err != nil { return nil, err } diff --git a/internal/pkg/core/uploader/uploader_aliyun_slb.go b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go similarity index 68% rename from internal/pkg/core/uploader/uploader_aliyun_slb.go rename to internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go index 99f3c484..9a396c89 100644 --- a/internal/pkg/core/uploader/uploader_aliyun_slb.go +++ b/internal/pkg/core/uploader/providers/aliyun-slb/aliyun_slb.go @@ -1,4 +1,4 @@ -package uploader +package aliyunslb import ( "context" @@ -8,11 +8,12 @@ import ( "strings" "time" - openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" - slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client" - util "github.com/alibabacloud-go/tea-utils/v2/service" + aliyunOpen "github.com/alibabacloud-go/darabonba-openapi/v2/client" + aliyunSlb "github.com/alibabacloud-go/slb-20140515/v4/client" "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/x509" ) @@ -23,29 +24,27 @@ type AliyunSLBUploaderConfig struct { } type AliyunSLBUploader struct { - config *AliyunSLBUploaderConfig - sdkClient *slb20140515.Client - sdkRuntime *util.RuntimeOptions + config *AliyunSLBUploaderConfig + sdkClient *aliyunSlb.Client } -func NewAliyunSLBUploader(config *AliyunSLBUploaderConfig) (Uploader, error) { - client, err := (&AliyunSLBUploader{}).createSdkClient( +func New(config *AliyunSLBUploaderConfig) (*AliyunSLBUploader, error) { + client, err := createSdkClient( config.AccessKeyId, config.AccessKeySecret, config.Region, ) if err != nil { - return nil, fmt.Errorf("failed to create sdk client: %w", err) + return nil, xerrors.Wrap(err, "failed to create sdk client") } return &AliyunSLBUploader{ - config: config, - sdkClient: client, - sdkRuntime: &util.RuntimeOptions{}, + config: config, + sdkClient: client, }, nil } -func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { +func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 certX509, err := x509.ParseCertificateFromPEM(certPem) if err != nil { @@ -54,12 +53,12 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP // 查询证书列表,避免重复上传 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeservercertificates - describeServerCertificatesReq := &slb20140515.DescribeServerCertificatesRequest{ + describeServerCertificatesReq := &aliyunSlb.DescribeServerCertificatesRequest{ RegionId: tea.String(u.config.Region), } - describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificatesWithOptions(describeServerCertificatesReq, u.sdkRuntime) + describeServerCertificatesResp, err := u.sdkClient.DescribeServerCertificates(describeServerCertificatesReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'slb.DescribeServerCertificates': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.DescribeServerCertificates'") } if describeServerCertificatesResp.Body.ServerCertificates != nil && describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate != nil { @@ -71,7 +70,7 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP strings.EqualFold(certX509.Subject.CommonName, *certDetail.CommonName) // 如果已存在相同证书,直接返回已有的证书信息 if isSameCert { - return &UploadResult{ + return &uploader.UploadResult{ CertId: *certDetail.ServerCertificateId, CertName: *certDetail.ServerCertificateName, }, nil @@ -85,30 +84,30 @@ func (u *AliyunSLBUploader) Upload(ctx context.Context, certPem string, privkeyP // 上传新证书 // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-uploadservercertificate - uploadServerCertificateReq := &slb20140515.UploadServerCertificateRequest{ + uploadServerCertificateReq := &aliyunSlb.UploadServerCertificateRequest{ RegionId: tea.String(u.config.Region), ServerCertificateName: tea.String(certName), ServerCertificate: tea.String(certPem), PrivateKey: tea.String(privkeyPem), } - uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificateWithOptions(uploadServerCertificateReq, u.sdkRuntime) + uploadServerCertificateResp, err := u.sdkClient.UploadServerCertificate(uploadServerCertificateReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'slb.UploadServerCertificate': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'slb.UploadServerCertificate'") } certId = *uploadServerCertificateResp.Body.ServerCertificateId - return &UploadResult{ + return &uploader.UploadResult{ CertId: certId, CertName: certName, }, nil } -func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) { +func createSdkClient(accessKeyId, accessKeySecret, region string) (*aliyunSlb.Client, error) { if region == "" { region = "cn-hangzhou" // SLB 服务默认区域:华东一杭州 } - aConfig := &openapi.Config{ + aConfig := &aliyunOpen.Config{ AccessKeyId: tea.String(accessKeyId), AccessKeySecret: tea.String(accessKeySecret), } @@ -125,7 +124,7 @@ func (u *AliyunSLBUploader) createSdkClient(accessKeyId, accessKeySecret, region } aConfig.Endpoint = tea.String(endpoint) - client, err := slb20140515.NewClient(aConfig) + client, err := aliyunSlb.NewClient(aConfig) if err != nil { return nil, err } diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go similarity index 83% rename from internal/pkg/core/uploader/uploader_huaweicloud_elb.go rename to internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go index 090362af..198494b3 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-elb/huaweicloud_elb.go @@ -1,7 +1,8 @@ -package uploader +package huaweicloudelb import ( "context" + "errors" "fmt" "time" @@ -13,7 +14,9 @@ import ( hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/cast" "github.com/usual2970/certimate/internal/pkg/utils/x509" ) @@ -29,14 +32,14 @@ type HuaweiCloudELBUploader struct { sdkClient *hcElb.ElbClient } -func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, error) { - client, err := (&HuaweiCloudELBUploader{}).createSdkClient( +func New(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) { + client, err := createSdkClient( config.AccessKeyId, config.SecretAccessKey, config.Region, ) if err != nil { - return nil, fmt.Errorf("failed to create sdk client: %w", err) + return nil, xerrors.Wrap(err, "failed to create sdk client: %w") } return &HuaweiCloudELBUploader{ @@ -45,7 +48,7 @@ func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, }, nil } -func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { +func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 newCert, err := x509.ParseCertificateFromPEM(certPem) if err != nil { @@ -65,7 +68,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.ListCertificates'") } if listCertificatesResp.Certificates != nil { @@ -84,7 +87,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri // 如果已存在相同证书,直接返回已有的证书信息 if isSameCert { - return &UploadResult{ + return &uploader.UploadResult{ CertId: certDetail.Id, CertName: certDetail.Name, }, nil @@ -105,9 +108,9 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri // 获取项目 ID // REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html - projectId, err := u.getSdkProjectId(u.config.Region, u.config.AccessKeyId, u.config.SecretAccessKey) + projectId, err := getSdkProjectId(u.config.AccessKeyId, u.config.SecretAccessKey, u.config.Region) if err != nil { - return nil, fmt.Errorf("failed to get SDK project id: %w", err) + return nil, xerrors.Wrap(err, "failed to get SDK project id") } // 生成新证书名(需符合华为云命名规则) @@ -128,18 +131,18 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri } createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'elb.CreateCertificate': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'elb.CreateCertificate'") } certId = createCertificateResp.Certificate.Id certName = createCertificateResp.Certificate.Name - return &UploadResult{ + return &uploader.UploadResult{ CertId: certId, CertName: certName, }, nil } -func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { +func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { if region == "" { region = "cn-north-4" // ELB 服务默认区域:华北四北京 } @@ -169,7 +172,7 @@ func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, r return client, nil } -func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) { +func getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) { if region == "" { region = "cn-north-4" // IAM 服务默认区域:华北四北京 } @@ -207,7 +210,7 @@ func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, r if err != nil { return "", err } else if response.Projects == nil || len(*response.Projects) == 0 { - return "", fmt.Errorf("no project found") + return "", errors.New("no project found") } return (*response.Projects)[0].Id, nil diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go similarity index 83% rename from internal/pkg/core/uploader/uploader_huaweicloud_scm.go rename to internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go index 2b09ca19..b8510900 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go +++ b/internal/pkg/core/uploader/providers/huaweicloud-scm/huaweicloud_scm.go @@ -1,4 +1,4 @@ -package uploader +package huaweicloudscm import ( "context" @@ -9,7 +9,9 @@ import ( hcScm "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3" hcScmModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/model" hcScmRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/scm/v3/region" + xerrors "github.com/pkg/errors" + "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/cast" "github.com/usual2970/certimate/internal/pkg/utils/x509" ) @@ -25,14 +27,14 @@ type HuaweiCloudSCMUploader struct { sdkClient *hcScm.ScmClient } -func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, error) { - client, err := (&HuaweiCloudSCMUploader{}).createSdkClient( +func New(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) { + client, err := createSdkClient( config.AccessKeyId, config.SecretAccessKey, config.Region, ) if err != nil { - return nil, fmt.Errorf("failed to create sdk client: %w", err) + return nil, xerrors.Wrap(err, "failed to create sdk client") } return &HuaweiCloudSCMUploader{ @@ -41,7 +43,7 @@ func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, }, nil } -func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { +func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 certX509, err := x509.ParseCertificateFromPEM(certPem) if err != nil { @@ -63,7 +65,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri } listCertificatesResp, err := u.sdkClient.ListCertificates(listCertificatesReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'scm.ListCertificates': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ListCertificates'") } if listCertificatesResp.Certificates != nil { @@ -76,7 +78,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri if exportCertificateResp != nil && exportCertificateResp.HttpStatusCode == 404 { continue } - return nil, fmt.Errorf("failed to execute sdk request 'scm.ExportCertificate': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ExportCertificate'") } var isSameCert bool @@ -93,7 +95,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri // 如果已存在相同证书,直接返回已有的证书信息 if isSameCert { - return &UploadResult{ + return &uploader.UploadResult{ CertId: certDetail.Id, CertName: certDetail.Name, }, nil @@ -127,17 +129,17 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri } importCertificateResp, err := u.sdkClient.ImportCertificate(importCertificateReq) if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'scm.ImportCertificate': %w", err) + return nil, xerrors.Wrap(err, "failed to execute sdk request 'scm.ImportCertificate'") } certId = *importCertificateResp.CertificateId - return &UploadResult{ + return &uploader.UploadResult{ CertId: certId, CertName: certName, }, nil } -func (u *HuaweiCloudSCMUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) { +func createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) { if region == "" { region = "cn-north-4" // SCM 服务默认区域:华北四北京 } diff --git a/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go new file mode 100644 index 00000000..534881d1 --- /dev/null +++ b/internal/pkg/core/uploader/providers/qiniu-sslcert/qiniu_sslcert.go @@ -0,0 +1,70 @@ +package qiniusslcert + +import ( + "context" + "fmt" + "time" + + xerrors "github.com/pkg/errors" + "github.com/qiniu/go-sdk/v7/auth" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/x509" + qiniuEx "github.com/usual2970/certimate/internal/pkg/vendors/qiniu-sdk" +) + +type QiniuSSLCertUploaderConfig struct { + AccessKey string `json:"accessKey"` + SecretKey string `json:"secretKey"` +} + +type QiniuSSLCertUploader struct { + config *QiniuSSLCertUploaderConfig + sdkClient *qiniuEx.Client +} + +func New(config *QiniuSSLCertUploaderConfig) (*QiniuSSLCertUploader, error) { + client, err := createSdkClient( + config.AccessKey, + config.SecretKey, + ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &QiniuSSLCertUploader{ + config: config, + sdkClient: client, + }, nil +} + +func (u *QiniuSSLCertUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := x509.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 生成新证书名(需符合七牛云命名规则) + var certId, certName string + certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 上传新证书 + // REF: https://developer.qiniu.com/fusion/8593/interface-related-certificate + uploadSslCertResp, err := u.sdkClient.UploadSslCert(certName, certX509.Subject.CommonName, privkeyPem, certPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UploadSslCert'") + } + + certId = uploadSslCertResp.CertID + return &uploader.UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func createSdkClient(accessKey, secretKey string) (*qiniuEx.Client, error) { + credential := auth.New(accessKey, secretKey) + client := qiniuEx.NewClient(credential) + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go new file mode 100644 index 00000000..c6e5dcb5 --- /dev/null +++ b/internal/pkg/core/uploader/providers/tencentcloud-ssl/tencentcloud_ssl.go @@ -0,0 +1,66 @@ +package tencentcloudssl + +import ( + "context" + + xerrors "github.com/pkg/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" +) + +type TencentCloudSSLUploaderConfig struct { + SecretId string `json:"secretId"` + SecretKey string `json:"secretKey"` +} + +type TencentCloudSSLUploader struct { + config *TencentCloudSSLUploaderConfig + sdkClient *tcSsl.Client +} + +func New(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) { + client, err := createSdkClient( + config.SecretId, + config.SecretKey, + ) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &TencentCloudSSLUploader{ + config: config, + sdkClient: client, + }, nil +} + +func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + // 上传新证书 + // REF: https://cloud.tencent.com/document/product/400/41665 + uploadCertificateReq := tcSsl.NewUploadCertificateRequest() + uploadCertificateReq.CertificatePublicKey = common.StringPtr(certPem) + uploadCertificateReq.CertificatePrivateKey = common.StringPtr(privkeyPem) + uploadCertificateReq.Repeatable = common.BoolPtr(false) + uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'ssl.UploadCertificate'") + } + + certId := *uploadCertificateResp.Response.CertificateId + return &uploader.UploadResult{ + CertId: certId, + CertName: "", + }, nil +} + +func createSdkClient(secretId, secretKey string) (*tcSsl.Client, error) { + credential := common.NewCredential(secretId, secretKey) + client, err := tcSsl.NewClient(credential, "", profile.NewClientProfile()) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go b/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go deleted file mode 100644 index 2a34e5e6..00000000 --- a/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go +++ /dev/null @@ -1,92 +0,0 @@ -package uploader - -import ( - "context" - "fmt" - "time" - - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" - "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" - tcSsl "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/ssl/v20191205" - - "github.com/usual2970/certimate/internal/pkg/utils/cast" -) - -type TencentCloudSSLUploaderConfig struct { - Region string `json:"region"` - SecretId string `json:"secretId"` - SecretKey string `json:"secretKey"` -} - -type TencentCloudSSLUploader struct { - config *TencentCloudSSLUploaderConfig - sdkClient *tcSsl.Client -} - -func NewTencentCloudSSLUploader(config *TencentCloudSSLUploaderConfig) (Uploader, error) { - client, err := (&TencentCloudSSLUploader{}).createSdkClient( - config.Region, - config.SecretId, - config.SecretKey, - ) - if err != nil { - return nil, fmt.Errorf("failed to create sdk client: %w", err) - } - - return &TencentCloudSSLUploader{ - config: config, - sdkClient: client, - }, nil -} - -func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) { - // 生成新证书名(需符合腾讯云命名规则) - var certId, certName string - certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) - - // 上传新证书 - // REF: https://cloud.tencent.com/document/product/400/41665 - uploadCertificateReq := &tcSsl.UploadCertificateRequest{ - Alias: cast.StringPtr(certName), - CertificatePublicKey: cast.StringPtr(certPem), - CertificatePrivateKey: cast.StringPtr(privkeyPem), - Repeatable: cast.BoolPtr(false), - } - uploadCertificateResp, err := u.sdkClient.UploadCertificate(uploadCertificateReq) - if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'ssl.UploadCertificate': %w", err) - } - - // 获取证书详情 - // REF: https://cloud.tencent.com/document/api/400/41673 - // - // P.S. 上传重复证书会返回上一次的证书 ID,这里需要重新获取一遍证书名(https://github.com/usual2970/certimate/pull/227) - describeCertificateDetailReq := &tcSsl.DescribeCertificateDetailRequest{ - CertificateId: uploadCertificateResp.Response.CertificateId, - } - describeCertificateDetailResp, err := u.sdkClient.DescribeCertificateDetail(describeCertificateDetailReq) - if err != nil { - return nil, fmt.Errorf("failed to execute sdk request 'ssl.DescribeCertificateDetail': %w", err) - } - - certId = *describeCertificateDetailResp.Response.CertificateId - certName = *describeCertificateDetailResp.Response.Alias - return &UploadResult{ - CertId: certId, - CertName: certName, - }, nil -} - -func (u *TencentCloudSSLUploader) createSdkClient(region, secretId, secretKey string) (*tcSsl.Client, error) { - if region == "" { - region = "ap-guangzhou" // SSL 服务默认区域:广州 - } - - credential := common.NewCredential(secretId, secretKey) - client, err := tcSsl.NewClient(credential, region, profile.NewClientProfile()) - if err != nil { - return nil, err - } - - return client, nil -} diff --git a/internal/pkg/utils/fs/fs.go b/internal/pkg/utils/fs/fs.go index 3ae82060..47b0cafb 100644 --- a/internal/pkg/utils/fs/fs.go +++ b/internal/pkg/utils/fs/fs.go @@ -1,9 +1,10 @@ package fs import ( - "fmt" "os" "path/filepath" + + xerrors "github.com/pkg/errors" ) // 与 [WriteFile] 类似,但写入的是字符串内容。 @@ -33,18 +34,18 @@ func WriteFile(path string, data []byte) error { err := os.MkdirAll(dir, os.ModePerm) if err != nil { - return fmt.Errorf("failed to create directory: %w", err) + return xerrors.Wrap(err, "failed to create directory") } file, err := os.Create(path) if err != nil { - return fmt.Errorf("failed to create file: %w", err) + return xerrors.Wrap(err, "failed to create file") } defer file.Close() _, err = file.Write(data) if err != nil { - return fmt.Errorf("failed to write file: %w", err) + return xerrors.Wrap(err, "failed to write file") } return nil diff --git a/internal/pkg/utils/x509/common.go b/internal/pkg/utils/x509/common.go new file mode 100644 index 00000000..f5557962 --- /dev/null +++ b/internal/pkg/utils/x509/common.go @@ -0,0 +1,22 @@ +package x509 + +import ( + "crypto/x509" +) + +// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 +// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。 +// +// 入参: +// - a: 待比较的第一个 x509.Certificate 对象。 +// - b: 待比较的第二个 x509.Certificate 对象。 +// +// 出参: +// - 是否相同。 +func EqualCertificate(a, b *x509.Certificate) bool { + return string(a.Signature) == string(b.Signature) && + a.SignatureAlgorithm == b.SignatureAlgorithm && + a.SerialNumber.String() == b.SerialNumber.String() && + a.Issuer.SerialNumber == b.Issuer.SerialNumber && + a.Subject.SerialNumber == b.Subject.SerialNumber +} diff --git a/internal/pkg/utils/x509/converter.go b/internal/pkg/utils/x509/converter.go new file mode 100644 index 00000000..c5522f27 --- /dev/null +++ b/internal/pkg/utils/x509/converter.go @@ -0,0 +1,31 @@ +package x509 + +import ( + "crypto/ecdsa" + "crypto/x509" + "encoding/pem" + + xerrors "github.com/pkg/errors" +) + +// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。 +// +// 入参: +// - privkey: ecdsa.PrivateKey 对象。 +// +// 出参: +// - privkeyPem: 私钥 PEM 内容。 +// - err: 错误。 +func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) { + data, err := x509.MarshalECPrivateKey(privkey) + if err != nil { + return "", xerrors.Wrap(err, "failed to marshal EC private key") + } + + block := &pem.Block{ + Type: "EC PRIVATE KEY", + Bytes: data, + } + + return string(pem.EncodeToMemory(block)), nil +} diff --git a/internal/pkg/utils/x509/parser.go b/internal/pkg/utils/x509/parser.go new file mode 100644 index 00000000..d9604526 --- /dev/null +++ b/internal/pkg/utils/x509/parser.go @@ -0,0 +1,83 @@ +package x509 + +import ( + "crypto/ecdsa" + "crypto/rsa" + "crypto/x509" + "encoding/pem" + "errors" + + xerrors "github.com/pkg/errors" +) + +// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 +// +// 入参: +// - certPem: 证书 PEM 内容。 +// +// 出参: +// - cert: x509.Certificate 对象。 +// - err: 错误。 +func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) { + pemData := []byte(certPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + cert, err = x509.ParseCertificate(block.Bytes) + if err != nil { + return nil, xerrors.Wrap(err, "failed to parse certificate") + } + + return cert, nil +} + +// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 +// +// 入参: +// - privkeyPem: 私钥 PEM 内容。 +// +// 出参: +// - privkey: ecdsa.PrivateKey 对象。 +// - err: 错误。 +func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) { + pemData := []byte(privkeyPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + privkey, err = x509.ParseECPrivateKey(block.Bytes) + if err != nil { + return nil, xerrors.Wrap(err, "failed to parse private key") + } + + return privkey, nil +} + +// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。 +// +// 入参: +// - privkeyPem: 私钥 PEM 内容。 +// +// 出参: +// - privkey: rsa.PrivateKey 对象。 +// - err: 错误。 +func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) { + pemData := []byte(privkeyPem) + + block, _ := pem.Decode(pemData) + if block == nil { + return nil, errors.New("failed to decode PEM block") + } + + privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) + if err != nil { + return nil, xerrors.Wrap(err, "failed to parse private key") + } + + return privkey, nil +} diff --git a/internal/pkg/utils/x509/x509.go b/internal/pkg/utils/x509/x509.go deleted file mode 100644 index 40cc39d6..00000000 --- a/internal/pkg/utils/x509/x509.go +++ /dev/null @@ -1,120 +0,0 @@ -package x509 - -import ( - "crypto/ecdsa" - "crypto/rsa" - "crypto/x509" - "encoding/pem" - "fmt" -) - -// 比较两个 x509.Certificate 对象,判断它们是否是同一张证书。 -// 注意,这不是精确比较,而只是基于证书序列号和数字签名的快速判断,但对于权威 CA 签发的证书来说不会存在误判。 -// -// 入参: -// - a: 待比较的第一个 x509.Certificate 对象。 -// - b: 待比较的第二个 x509.Certificate 对象。 -// -// 出参: -// - 是否相同。 -func EqualCertificate(a, b *x509.Certificate) bool { - return string(a.Signature) == string(b.Signature) && - a.SignatureAlgorithm == b.SignatureAlgorithm && - a.SerialNumber.String() == b.SerialNumber.String() && - a.Issuer.SerialNumber == b.Issuer.SerialNumber && - a.Subject.SerialNumber == b.Subject.SerialNumber -} - -// 从 PEM 编码的证书字符串解析并返回一个 x509.Certificate 对象。 -// -// 入参: -// - certPem: 证书 PEM 内容。 -// -// 出参: -// - cert: x509.Certificate 对象。 -// - err: 错误。 -func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) { - pemData := []byte(certPem) - - block, _ := pem.Decode(pemData) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - cert, err = x509.ParseCertificate(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse certificate: %w", err) - } - - return cert, nil -} - -// 从 PEM 编码的私钥字符串解析并返回一个 ecdsa.PrivateKey 对象。 -// -// 入参: -// - privkeyPem: 私钥 PEM 内容。 -// -// 出参: -// - privkey: ecdsa.PrivateKey 对象。 -// - err: 错误。 -func ParseECPrivateKeyFromPEM(privkeyPem string) (privkey *ecdsa.PrivateKey, err error) { - pemData := []byte(privkeyPem) - - block, _ := pem.Decode(pemData) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - privkey, err = x509.ParseECPrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %w", err) - } - - return privkey, nil -} - -// 从 PEM 编码的私钥字符串解析并返回一个 rsa.PrivateKey 对象。 -// -// 入参: -// - privkeyPem: 私钥 PEM 内容。 -// -// 出参: -// - privkey: rsa.PrivateKey 对象。 -// - err: 错误。 -func ParsePKCS1PrivateKeyFromPEM(privkeyPem string) (privkey *rsa.PrivateKey, err error) { - pemData := []byte(privkeyPem) - - block, _ := pem.Decode(pemData) - if block == nil { - return nil, fmt.Errorf("failed to decode PEM block") - } - - privkey, err = x509.ParsePKCS1PrivateKey(block.Bytes) - if err != nil { - return nil, fmt.Errorf("failed to parse private key: %w", err) - } - - return privkey, nil -} - -// 将 ecdsa.PrivateKey 对象转换为 PEM 编码的字符串。 -// -// 入参: -// - privkey: ecdsa.PrivateKey 对象。 -// -// 出参: -// - privkeyPem: 私钥 PEM 内容。 -// - err: 错误。 -func ConvertECPrivateKeyToPEM(privkey *ecdsa.PrivateKey) (privkeyPem string, err error) { - data, err := x509.MarshalECPrivateKey(privkey) - if err != nil { - return "", fmt.Errorf("failed to marshal EC private key: %w", err) - } - - block := &pem.Block{ - Type: "EC PRIVATE KEY", - Bytes: data, - } - - return string(pem.EncodeToMemory(block)), nil -} diff --git a/internal/pkg/vendors/huaweicloud-cdn-sdk/client.go b/internal/pkg/vendors/huaweicloud-cdn-sdk/client.go new file mode 100644 index 00000000..8befeb5f --- /dev/null +++ b/internal/pkg/vendors/huaweicloud-cdn-sdk/client.go @@ -0,0 +1,26 @@ +package huaweicloudcdnsdk + +import ( + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core" + hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" +) + +type Client struct { + hcCdn.CdnClient +} + +func NewClient(hcClient *core.HcHttpClient) *Client { + return &Client{ + CdnClient: *hcCdn.NewCdnClient(hcClient), + } +} + +func (c *Client) UploadDomainMultiCertificatesEx(request *UpdateDomainMultiCertificatesExRequest) (*UpdateDomainMultiCertificatesExResponse, error) { + requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates() + + if resp, err := c.HcClient.Sync(request, requestDef); err != nil { + return nil, err + } else { + return resp.(*UpdateDomainMultiCertificatesExResponse), nil + } +} diff --git a/internal/pkg/vendors/huaweicloud-cdn-sdk/models.go b/internal/pkg/vendors/huaweicloud-cdn-sdk/models.go new file mode 100644 index 00000000..cca42058 --- /dev/null +++ b/internal/pkg/vendors/huaweicloud-cdn-sdk/models.go @@ -0,0 +1,62 @@ +package huaweicloudcdnsdk + +import ( + hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" + + "github.com/usual2970/certimate/internal/pkg/utils/cast" +) + +type UpdateDomainMultiCertificatesExRequestBodyContent struct { + hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"` + + // 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求,可能需要等之后 SDK 更新。 + SCMCertificateId *string `json:"scm_certificate_id,omitempty"` +} + +type UpdateDomainMultiCertificatesExRequestBody struct { + Https *UpdateDomainMultiCertificatesExRequestBodyContent `json:"https,omitempty"` +} + +type UpdateDomainMultiCertificatesExRequest struct { + Body *UpdateDomainMultiCertificatesExRequestBody `json:"body,omitempty"` +} + +type UpdateDomainMultiCertificatesExResponse struct { + hcCdnModel.UpdateDomainMultiCertificatesResponse +} + +func (m *UpdateDomainMultiCertificatesExRequestBodyContent) MergeConfig(src *hcCdnModel.ConfigsGetBody) *UpdateDomainMultiCertificatesExRequestBodyContent { + if src == nil { + return m + } + + // 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去。 + // 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化。 + + if *src.OriginProtocol == "follow" { + m.AccessOriginWay = cast.Int32Ptr(1) + } else if *src.OriginProtocol == "http" { + m.AccessOriginWay = cast.Int32Ptr(2) + } else if *src.OriginProtocol == "https" { + m.AccessOriginWay = cast.Int32Ptr(3) + } + + if src.ForceRedirect != nil { + m.ForceRedirectConfig = &hcCdnModel.ForceRedirect{} + + if src.ForceRedirect.Status == "on" { + m.ForceRedirectConfig.Switch = 1 + m.ForceRedirectConfig.RedirectType = src.ForceRedirect.Type + } else { + m.ForceRedirectConfig.Switch = 0 + } + } + + if src.Https != nil { + if *src.Https.Http2Status == "on" { + m.Http2 = cast.Int32Ptr(1) + } + } + + return m +} diff --git a/internal/pkg/vendors/qiniu-sdk/client.go b/internal/pkg/vendors/qiniu-sdk/client.go new file mode 100644 index 00000000..eceff741 --- /dev/null +++ b/internal/pkg/vendors/qiniu-sdk/client.go @@ -0,0 +1,160 @@ +package qiniusdk + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + + "github.com/qiniu/go-sdk/v7/auth" + + xhttp "github.com/usual2970/certimate/internal/utils/http" +) + +const qiniuHost = "http://api.qiniu.com" + +type Client struct { + mac *auth.Credentials +} + +func NewClient(mac *auth.Credentials) *Client { + if mac == nil { + mac = auth.Default() + } + return &Client{mac: mac} +} + +func (c *Client) GetDomainInfo(domain string) (*GetDomainInfoResponse, error) { + respBytes, err := c.sendReq(http.MethodGet, fmt.Sprintf("domain/%s", domain), nil) + if err != nil { + return nil, err + } + + resp := &GetDomainInfoResponse{} + err = json.Unmarshal(respBytes, resp) + if err != nil { + return nil, err + } + if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { + return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) + } + + return resp, nil +} + +func (c *Client) ModifyDomainHttpsConf(domain, certId string, forceHttps, http2Enable bool) (*ModifyDomainHttpsConfResponse, error) { + req := &ModifyDomainHttpsConfRequest{ + DomainInfoHttpsData: DomainInfoHttpsData{ + CertID: certId, + ForceHttps: forceHttps, + Http2Enable: http2Enable, + }, + } + + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/httpsconf", domain), bytes.NewReader(reqBytes)) + if err != nil { + return nil, err + } + + resp := &ModifyDomainHttpsConfResponse{} + err = json.Unmarshal(respBytes, resp) + if err != nil { + return nil, err + } + if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { + return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) + } + + return resp, nil +} + +func (c *Client) EnableDomainHttps(domain, certId string, forceHttps, http2Enable bool) (*EnableDomainHttpsResponse, error) { + req := &EnableDomainHttpsRequest{ + DomainInfoHttpsData: DomainInfoHttpsData{ + CertID: certId, + ForceHttps: forceHttps, + Http2Enable: http2Enable, + }, + } + + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.sendReq(http.MethodPut, fmt.Sprintf("domain/%s/sslize", domain), bytes.NewReader(reqBytes)) + if err != nil { + return nil, err + } + + resp := &EnableDomainHttpsResponse{} + err = json.Unmarshal(respBytes, resp) + if err != nil { + return nil, err + } + if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { + return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) + } + + return resp, nil +} + +func (c *Client) UploadSslCert(name, commonName, pri, ca string) (*UploadSslCertResponse, error) { + req := &UploadSslCertRequest{ + Name: name, + CommonName: commonName, + Pri: pri, + Ca: ca, + } + + reqBytes, err := json.Marshal(req) + if err != nil { + return nil, err + } + + respBytes, err := c.sendReq(http.MethodPost, "sslcert", bytes.NewReader(reqBytes)) + if err != nil { + return nil, err + } + + resp := &UploadSslCertResponse{} + err = json.Unmarshal(respBytes, resp) + if err != nil { + return nil, err + } + if resp.Code != nil && *resp.Code != 0 && *resp.Code != 200 { + return nil, fmt.Errorf("code: %d, error: %s", *resp.Code, *resp.Error) + } + + return resp, nil +} + +func (c *Client) sendReq(method string, path string, body io.Reader) ([]byte, error) { + req := xhttp.BuildReq(fmt.Sprintf("%s/%s", qiniuHost, path), method, body, map[string]string{ + "Content-Type": "application/json", + }) + + if err := c.mac.AddToken(auth.TokenQBox, req); err != nil { + return nil, err + } + + respBody, err := xhttp.ToRequest(req) + if err != nil { + return nil, err + } + + defer respBody.Close() + + res, err := io.ReadAll(respBody) + if err != nil { + return nil, err + } + + return res, nil +} diff --git a/internal/pkg/vendors/qiniu-sdk/models.go b/internal/pkg/vendors/qiniu-sdk/models.go new file mode 100644 index 00000000..433909e1 --- /dev/null +++ b/internal/pkg/vendors/qiniu-sdk/models.go @@ -0,0 +1,53 @@ +package qiniusdk + +type UploadSslCertRequest struct { + Name string `json:"name"` + CommonName string `json:"common_name"` + Pri string `json:"pri"` + Ca string `json:"ca"` +} + +type UploadSslCertResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + CertID string `json:"certID"` +} + +type DomainInfoHttpsData struct { + CertID string `json:"certId"` + ForceHttps bool `json:"forceHttps"` + Http2Enable bool `json:"http2Enable"` +} + +type GetDomainInfoResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` + Name string `json:"name"` + Type string `json:"type"` + CName string `json:"cname"` + Https *DomainInfoHttpsData `json:"https"` + PareDomain string `json:"pareDomain"` + OperationType string `json:"operationType"` + OperatingState string `json:"operatingState"` + OperatingStateDesc string `json:"operatingStateDesc"` + CreateAt string `json:"createAt"` + ModifyAt string `json:"modifyAt"` +} + +type ModifyDomainHttpsConfRequest struct { + DomainInfoHttpsData +} + +type ModifyDomainHttpsConfResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` +} + +type EnableDomainHttpsRequest struct { + DomainInfoHttpsData +} + +type EnableDomainHttpsResponse struct { + Code *int `json:"code,omitempty"` + Error *string `json:"error,omitempty"` +} diff --git a/internal/utils/rand/rand.go b/internal/utils/rand/rand.go index 3b0b71f6..05fa684e 100644 --- a/internal/utils/rand/rand.go +++ b/internal/utils/rand/rand.go @@ -5,7 +5,8 @@ import ( "time" ) -// RandStr 随机生成指定长度字符串 +// Deprecated: this will be removed in the future. +// 随机生成指定长度字符串 func RandStr(n int) string { seed := time.Now().UnixNano() source := rand.NewSource(seed) diff --git a/ui/src/components/certimate/DeployEdit.tsx b/ui/src/components/certimate/DeployEdit.tsx index 81353a29..05559360 100644 --- a/ui/src/components/certimate/DeployEdit.tsx +++ b/ui/src/components/certimate/DeployEdit.tsx @@ -1,16 +1,17 @@ -import { createContext, useContext } from "react"; +import { createContext, useContext, type Context as ReactContext } from "react"; -import { DeployConfig } from "@/domain/domain"; +import { type DeployConfig } from "@/domain/domain"; -type DeployEditContext = { - deploy: DeployConfig; - error: Record; - setDeploy: (deploy: DeployConfig) => void; - setError: (error: Record) => void; +export type DeployEditContext = { + config: Omit & { config: T }; + setConfig: (config: Omit & { config: T }) => void; + + errors: { [K in keyof T]?: string }; + setErrors: (error: { [K in keyof T]?: string }) => void; }; export const Context = createContext({} as DeployEditContext); -export const useDeployEditContext = () => { - return useContext(Context); -}; +export function useDeployEditContext() { + return useContext>(Context as unknown as ReactContext>); +} diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 6db96012..140ca0b0 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -8,7 +8,7 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectTrigger, SelectValue } from "@/components/ui/select"; import { ScrollArea } from "@/components/ui/scroll-area"; import AccessEditDialog from "./AccessEditDialog"; -import { Context as DeployEditContext } from "./DeployEdit"; +import { Context as DeployEditContext, type DeployEditContext as DeployEditContextType } from "./DeployEdit"; import DeployToAliyunOSS from "./DeployToAliyunOSS"; import DeployToAliyunCDN from "./DeployToAliyunCDN"; import DeployToAliyunCLB from "./DeployToAliyunCLB"; @@ -49,7 +49,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro type: "", }); - const [error, setError] = useState>({}); + const [errors, setErrors] = useState>({}); const [open, setOpen] = useState(false); @@ -66,10 +66,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro useEffect(() => { setDeployType(locDeployConfig.type); - setError({}); + setErrors({}); }, [locDeployConfig.type]); - const setDeploy = useCallback( + const setConfig = useCallback( (deploy: DeployConfig) => { if (deploy.type !== locDeployConfig.type) { setLocDeployConfig({ ...deploy, access: "", config: {} }); @@ -94,10 +94,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro const handleSaveClick = () => { // 验证数据 - const newError = { ...error }; + const newError = { ...errors }; newError.type = locDeployConfig.type === "" ? t("domain.deployment.form.access.placeholder") : ""; newError.access = locDeployConfig.access === "" ? t("domain.deployment.form.access.placeholder") : ""; - setError(newError); + setErrors(newError); if (Object.values(newError).some((e) => !!e)) return; // 保存数据 @@ -108,7 +108,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro access: "", type: "", }); - setError({}); + setErrors({}); // 关闭弹框 setOpen(false); @@ -171,10 +171,10 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro return ( @@ -199,7 +199,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro -
{error.type}
+
{errors.type}
{/* 授权配置 */} @@ -241,7 +241,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro -
{error.access}
+
{errors.access}
{/* 其他参数 */} diff --git a/ui/src/components/certimate/DeployList.tsx b/ui/src/components/certimate/DeployList.tsx index 1e9866e0..606ee470 100644 --- a/ui/src/components/certimate/DeployList.tsx +++ b/ui/src/components/certimate/DeployList.tsx @@ -9,6 +9,7 @@ import { Button } from "@/components/ui/button"; import DeployEditDialog from "./DeployEditDialog"; import { DeployConfig } from "@/domain/domain"; import { accessProvidersMap } from "@/domain/access"; +import { deployTargetsMap } from "@/domain/domain"; import { useConfigContext } from "@/providers/config"; type DeployItemProps = { @@ -18,10 +19,11 @@ type DeployItemProps = { }; const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => { + const { t } = useTranslation(); + const { config: { accesses }, } = useConfigContext(); - const { t } = useTranslation(); const access = accesses.find((access) => access.id === item.access); @@ -34,11 +36,7 @@ const DeployItem = ({ item, onDelete, onSave }: DeployItemProps) => { }; const getTypeName = () => { - if (!access) { - return ""; - } - - return t(accessProvidersMap.get(access.configType)?.name || ""); + return t(deployTargetsMap.get(item.type)?.name || ""); }; return ( diff --git a/ui/src/components/certimate/DeployToAliyunALB.tsx b/ui/src/components/certimate/DeployToAliyunALB.tsx index cf7feba9..33a7c562 100644 --- a/ui/src/components/certimate/DeployToAliyunALB.tsx +++ b/ui/src/components/certimate/DeployToAliyunALB.tsx @@ -8,27 +8,31 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToAliyunALBConfigParams = { + region?: string; + resourceType?: string; + loadbalancerId?: string; + listenerId?: string; +}; + const DeployToAliyunALB = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { region: "cn-hangzhou", - resourceType: "", - loadbalancerId: "", - listenerId: "", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z @@ -50,25 +54,15 @@ const DeployToAliyunALB = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - region: res.error.errors.find((e) => e.path[0] === "region")?.message, - resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, - }); - } else { - setError({ - ...error, - region: undefined, - resourceType: undefined, - loadbalancerId: undefined, - listenerId: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, + }); + }, [config]); return (
@@ -77,28 +71,28 @@ const DeployToAliyunALB = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
-
{error?.resourceType}
+
{errors?.resourceType}
- {data?.config?.resourceType === "loadbalancer" ? ( + {config?.config?.resourceType === "loadbalancer" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.loadbalancerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.loadbalancerId}
+
{errors?.loadbalancerId}
) : ( <> )} - {data?.config?.resourceType === "listener" ? ( + {config?.config?.resourceType === "listener" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.listenerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.listenerId}
+
{errors?.listenerId}
) : ( <> diff --git a/ui/src/components/certimate/DeployToAliyunCDN.tsx b/ui/src/components/certimate/DeployToAliyunCDN.tsx index 074f27a0..0d073720 100644 --- a/ui/src/components/certimate/DeployToAliyunCDN.tsx +++ b/ui/src/components/certimate/DeployToAliyunCDN.tsx @@ -7,45 +7,42 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToAliyunCDNConfigParams = { + domain?: string; +}; + const DeployToAliyunCDN = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - domain: "", - }, + if (!config.id) { + setConfig({ + ...config, + config: {}, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + const formSchema = z.object({ + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); + return (
@@ -53,33 +50,16 @@ const DeployToAliyunCDN = () => { { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToAliyunCLB.tsx b/ui/src/components/certimate/DeployToAliyunCLB.tsx index eb41c0ac..a00c3b5b 100644 --- a/ui/src/components/certimate/DeployToAliyunCLB.tsx +++ b/ui/src/components/certimate/DeployToAliyunCLB.tsx @@ -8,19 +8,24 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToAliyunCLBConfigParams = { + region?: string; + resourceType?: string; + loadbalancerId?: string; + listenerPort?: string; +}; + const DeployToAliyunCLB = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { region: "cn-hangzhou", - resourceType: "", - loadbalancerId: "", listenerPort: "443", }, }); @@ -28,7 +33,7 @@ const DeployToAliyunCLB = () => { }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z @@ -50,25 +55,15 @@ const DeployToAliyunCLB = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - region: res.error.errors.find((e) => e.path[0] === "region")?.message, - resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerPort: res.error.errors.find((e) => e.path[0] === "listenerPort")?.message, - }); - } else { - setError({ - ...error, - region: undefined, - resourceType: undefined, - loadbalancerId: undefined, - listenerPort: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerPort: res.error?.errors?.find((e) => e.path[0] === "listenerPort")?.message, + }); + }, [config]); return (
@@ -77,28 +72,28 @@ const DeployToAliyunCLB = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
-
{error?.resourceType}
+
{errors?.resourceType}
@@ -119,34 +114,34 @@ const DeployToAliyunCLB = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.loadbalancerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.loadbalancerId}
+
{errors?.loadbalancerId}
- {data?.config?.resourceType === "listener" ? ( + {config?.config?.resourceType === "listener" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.listenerPort = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.listenerPort}
+
{errors?.listenerPort}
) : ( <> diff --git a/ui/src/components/certimate/DeployToAliyunNLB.tsx b/ui/src/components/certimate/DeployToAliyunNLB.tsx index 38d6b1f7..8ca68fa8 100644 --- a/ui/src/components/certimate/DeployToAliyunNLB.tsx +++ b/ui/src/components/certimate/DeployToAliyunNLB.tsx @@ -8,27 +8,31 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToAliyunNLBConfigParams = { + region?: string; + resourceType?: string; + loadbalancerId?: string; + listenerId?: string; +}; + const DeployToAliyunNLB = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { region: "cn-hangzhou", - resourceType: "", - loadbalancerId: "", - listenerId: "", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z @@ -50,25 +54,15 @@ const DeployToAliyunNLB = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - region: res.error.errors.find((e) => e.path[0] === "region")?.message, - resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, - loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, - }); - } else { - setError({ - ...error, - region: undefined, - resourceType: undefined, - loadbalancerId: undefined, - listenerId: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, + }); + }, [config]); return (
@@ -77,28 +71,28 @@ const DeployToAliyunNLB = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
-
{error?.resourceType}
+
{errors?.resourceType}
- {data?.config?.resourceType === "loadbalancer" ? ( + {config?.config?.resourceType === "loadbalancer" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.loadbalancerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.loadbalancerId}
+
{errors?.loadbalancerId}
) : ( <> )} - {data?.config?.resourceType === "listener" ? ( + {config?.config?.resourceType === "listener" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.listenerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.listenerId}
+
{errors?.listenerId}
) : ( <> diff --git a/ui/src/components/certimate/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx index ccfcc870..14249758 100644 --- a/ui/src/components/certimate/DeployToAliyunOSS.tsx +++ b/ui/src/components/certimate/DeployToAliyunOSS.tsx @@ -7,65 +7,53 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToAliyunOSSConfigParams = { + endpoint?: string; + bucket?: string; + domain?: string; +}; + const DeployToAliyunOSS = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { - endpoint: "oss-cn-hangzhou.aliyuncs.com", - bucket: "", - domain: "", + endpoint: "oss.aliyuncs.com", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - useEffect(() => { - const resp = bucketSchema.safeParse(data.config?.bucket); - if (!resp.success) { - setError({ - ...error, - bucket: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + const formSchema = z.object({ + endpoint: z.string().min(1, { + message: t("domain.deployment.form.aliyun_oss_endpoint.placeholder"), + }), + bucket: z.string().min(1, { + message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"), + }), + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); - const bucketSchema = z.string().min(1, { - message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"), - }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + endpoint: res.error?.errors?.find((e) => e.path[0] === "endpoint")?.message, + bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); return (
@@ -74,20 +62,16 @@ const DeployToAliyunOSS = () => { { - const temp = e.target.value; - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.endpoint = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.endpoint = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.endpoint}
+
{errors?.endpoint}
@@ -95,33 +79,16 @@ const DeployToAliyunOSS = () => { { - const temp = e.target.value; - - const resp = bucketSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - bucket: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.bucket = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.bucket = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.bucket}
+
{errors?.bucket}
@@ -129,33 +96,16 @@ const DeployToAliyunOSS = () => { { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx index bdf968c4..eaeda1b7 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx @@ -7,63 +7,66 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToHuaweiCloudCDNConfigParams = { + region?: string; + domain?: string; +}; + const DeployToHuaweiCloudCDN = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { region: "cn-north-1", - domain: "", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + const formSchema = z.object({ + region: z.string().min(1, { + message: t("domain.deployment.form.huaweicloud_cdn_region.placeholder"), + }), + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); + return (
- + { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
@@ -71,16 +74,16 @@ const DeployToHuaweiCloudCDN = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx index e7a7fc4a..a26d33c8 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx @@ -8,34 +8,40 @@ import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useDeployEditContext } from "./DeployEdit"; -const DeployToHuaweiCloudCDN = () => { +type DeployToHuaweiCloudELBConfigParams = { + region?: string; + resourceType?: string; + certificateId?: string; + loadbalancerId?: string; + listenerId?: string; +}; + +const DeployToHuaweiCloudELB = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { region: "cn-north-1", - resourceType: "", - certificateId: "", - loadbalancerId: "", - listenerId: "", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z .object({ region: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_region.placeholder")), - resourceType: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder")), + resourceType: z.union([z.literal("certificate"), z.literal("loadbalancer"), z.literal("listener")], { + message: t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder"), + }), certificateId: z.string().optional(), loadbalancerId: z.string().optional(), listenerId: z.string().optional(), @@ -54,27 +60,16 @@ const DeployToHuaweiCloudCDN = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - region: res.error.errors.find((e) => e.path[0] === "region")?.message, - resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, - certificateId: res.error.errors.find((e) => e.path[0] === "certificateId")?.message, - loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, - listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, - }); - } else { - setError({ - ...error, - region: undefined, - resourceType: undefined, - certificateId: undefined, - loadbalancerId: undefined, - listenerId: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, + certificateId: res.error?.errors?.find((e) => e.path[0] === "certificateId")?.message, + loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, + }); + }, [config]); return (
@@ -83,28 +78,28 @@ const DeployToHuaweiCloudCDN = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
-
{error?.resourceType}
+
{errors?.resourceType}
- {data?.config?.resourceType === "certificate" ? ( + {config?.config?.resourceType === "certificate" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.certificateId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.certificateId}
+
{errors?.certificateId}
) : ( <> )} - {data?.config?.resourceType === "loadbalancer" ? ( + {config?.config?.resourceType === "loadbalancer" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.loadbalancerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.loadbalancerId}
+
{errors?.loadbalancerId}
) : ( <> )} - {data?.config?.resourceType === "listener" ? ( + {config?.config?.resourceType === "listener" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.listenerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.listenerId}
+
{errors?.listenerId}
) : ( <> @@ -187,4 +182,4 @@ const DeployToHuaweiCloudCDN = () => { ); }; -export default DeployToHuaweiCloudCDN; +export default DeployToHuaweiCloudELB; diff --git a/ui/src/components/certimate/DeployToKubernetesSecret.tsx b/ui/src/components/certimate/DeployToKubernetesSecret.tsx index c7b8e2a8..119ae12f 100644 --- a/ui/src/components/certimate/DeployToKubernetesSecret.tsx +++ b/ui/src/components/certimate/DeployToKubernetesSecret.tsx @@ -1,23 +1,30 @@ import { useEffect } from "react"; import { useTranslation } from "react-i18next"; +import { z } from "zod"; import { produce } from "immer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToKubernetesSecretConfigParams = { + namespace?: string; + secretName?: string; + secretDataKeyForCrt?: string; + secretDataKeyForKey?: string; +}; + const DeployToKubernetesSecret = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { namespace: "default", - secretName: "", secretDataKeyForCrt: "tls.crt", secretDataKeyForKey: "tls.key", }, @@ -26,9 +33,35 @@ const DeployToKubernetesSecret = () => { }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); + const formSchema = z.object({ + namespace: z.string().min(1, { + message: t("domain.deployment.form.k8s_namespace.placeholder"), + }), + secretName: z.string().min(1, { + message: t("domain.deployment.form.k8s_secret_name.placeholder"), + }), + secretDataKeyForCrt: z.string().min(1, { + message: t("domain.deployment.form.k8s_secret_data_key_for_crt.placeholder"), + }), + secretDataKeyForKey: z.string().min(1, { + message: t("domain.deployment.form.k8s_secret_data_key_for_key.placeholder"), + }), + }); + + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + namespace: res.error?.errors?.find((e) => e.path[0] === "namespace")?.message, + secretName: res.error?.errors?.find((e) => e.path[0] === "secretName")?.message, + secretDataKeyForCrt: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForCrt")?.message, + secretDataKeyForKey: res.error?.errors?.find((e) => e.path[0] === "secretDataKeyForKey")?.message, + }); + }, [config]); + return ( <>
@@ -37,13 +70,13 @@ const DeployToKubernetesSecret = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; - draft.config.namespace = e.target.value; + draft.config.namespace = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} />
@@ -53,13 +86,13 @@ const DeployToKubernetesSecret = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; - draft.config.secretName = e.target.value; + draft.config.secretName = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> @@ -69,13 +102,13 @@ const DeployToKubernetesSecret = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; - draft.config.secretDataKeyForCrt = e.target.value; + draft.config.secretDataKeyForCrt = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> @@ -85,13 +118,13 @@ const DeployToKubernetesSecret = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; - draft.config.secretDataKeyForKey = e.target.value; + draft.config.secretDataKeyForKey = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> diff --git a/ui/src/components/certimate/DeployToLocal.tsx b/ui/src/components/certimate/DeployToLocal.tsx index ae60e75d..11a20c2a 100644 --- a/ui/src/components/certimate/DeployToLocal.tsx +++ b/ui/src/components/certimate/DeployToLocal.tsx @@ -12,33 +12,40 @@ import { Textarea } from "@/components/ui/textarea"; import { useDeployEditContext } from "./DeployEdit"; import { cn } from "@/lib/utils"; +type DeployToLocalConfigParams = { + format?: string; + certPath?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + shell?: string; + preCommand?: string; + command?: string; +}; + const DeployToLocal = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { format: "pem", certPath: "/etc/nginx/ssl/nginx.crt", keyPath: "/etc/nginx/ssl/nginx.key", - pfxPassword: "", - jksAlias: "", - jksKeypass: "", - jksStorepass: "", shell: "sh", - preCommand: "", - command: "sudo service nginx reload", }, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z @@ -86,68 +93,55 @@ const DeployToLocal = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - format: res.error.errors.find((e) => e.path[0] === "format")?.message, - certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, - keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, - pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message, - jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message, - jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message, - jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message, - shell: res.error.errors.find((e) => e.path[0] === "shell")?.message, - preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, - command: res.error.errors.find((e) => e.path[0] === "command")?.message, - }); - } else { - setError({ - ...error, - format: undefined, - certPath: undefined, - keyPath: undefined, - pfxPassword: undefined, - jksAlias: undefined, - jksKeypass: undefined, - jksStorepass: undefined, - shell: undefined, - preCommand: undefined, - command: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + format: res.error?.errors?.find((e) => e.path[0] === "format")?.message, + certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message, + keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message, + pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message, + jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message, + jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message, + jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message, + shell: res.error?.errors?.find((e) => e.path[0] === "shell")?.message, + preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message, + command: res.error?.errors?.find((e) => e.path[0] === "command")?.message, + }); + }, [config]); useEffect(() => { - if (data.config?.format === "pem") { - if (/(.pfx|.jks)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt"); - }); - setDeploy(newData); + if (config.config?.format === "pem") { + if (/(.pfx|.jks)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt"); + }) + ); } - } else if (data.config?.format === "pfx") { - if (/(.crt|.jks)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx"); - }); - setDeploy(newData); + } else if (config.config?.format === "pfx") { + if (/(.crt|.jks)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx"); + }) + ); } - } else if (data.config?.format === "jks") { - if (/(.crt|.pfx)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks"); - }); - setDeploy(newData); + } else if (config.config?.format === "jks") { + if (/(.crt|.pfx)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks"); + }) + ); } } - }, [data.config?.format]); + }, [config.config?.format]); const getOptionCls = (val: string) => { - if (data.config?.shell === val) { + if (config.config?.shell === val) { return "border-primary dark:border-primary"; } @@ -158,21 +152,23 @@ const DeployToLocal = () => { switch (key) { case "reload_nginx": { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.shell = "sh"; - draft.config.command = "sudo service nginx reload"; - }); - setDeploy(newData); + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.shell = "sh"; + draft.config.command = "sudo service nginx reload"; + }) + ); } break; case "binding_iis": { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.shell = "powershell"; - draft.config.command = ` + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.shell = "powershell"; + draft.config.command = ` # 请将以下变量替换为实际值 $pfxPath = "" # PFX 文件路径 $pfxPassword = "" # PFX 密码 @@ -201,17 +197,18 @@ $binding.AddSslCertificate($thumbprint, "My") # 删除目录下的证书文件 Remove-Item -Path "$pfxPath" -Force `.trim(); - }); - setDeploy(newData); + }) + ); } break; case "binding_netsh": { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.shell = "powershell"; - draft.config.command = ` + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.shell = "powershell"; + draft.config.command = ` # 请将以下变量替换为实际值 $pfxPath = "" # PFX 文件路径 $pfxPassword = "" # PFX 密码 @@ -232,8 +229,8 @@ netsh http add sslcert ipport=$addr certhash=$thumbprint # 删除目录下的证书文件 Remove-Item -Path "$pfxPath" -Force `.trim(); - }); - setDeploy(newData); + }) + ); } break; } @@ -245,13 +242,13 @@ Remove-Item -Path "$pfxPath" -Force
-
{error?.format}
+
{errors?.format}
@@ -273,77 +270,77 @@ Remove-Item -Path "$pfxPath" -Force { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.certPath = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.certPath}
+
{errors?.certPath}
- {data.config?.format === "pem" ? ( + {config.config?.format === "pem" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.keyPath = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.keyPath}
+
{errors?.keyPath}
) : ( <> )} - {data.config?.format === "pfx" ? ( + {config.config?.format === "pfx" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.pfxPassword = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.pfxPassword}
+
{errors?.pfxPassword}
) : ( <> )} - {data.config?.format === "jks" ? ( + {config.config?.format === "jks" ? ( <>
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksAlias = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksAlias}
+
{errors?.jksAlias}
@@ -351,16 +348,16 @@ Remove-Item -Path "$pfxPath" -Force { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksKeypass = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksKeypass}
+
{errors?.jksKeypass}
@@ -368,16 +365,16 @@ Remove-Item -Path "$pfxPath" -Force { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksStorepass = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksStorepass}
+
{errors?.jksStorepass}
) : ( @@ -388,13 +385,13 @@ Remove-Item -Path "$pfxPath" -Force { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.shell = val; }); - setDeploy(newData); + setConfig(nv); }} >
@@ -422,24 +419,24 @@ Remove-Item -Path "$pfxPath" -Force
-
{error?.shell}
+
{errors?.shell}
-
{error?.preCommand}
+
{errors?.preCommand}
@@ -464,17 +461,17 @@ Remove-Item -Path "$pfxPath" -Force
-
{error?.command}
+
{errors?.command}
diff --git a/ui/src/components/certimate/DeployToQiniuCDN.tsx b/ui/src/components/certimate/DeployToQiniuCDN.tsx index 508939cd..90163c12 100644 --- a/ui/src/components/certimate/DeployToQiniuCDN.tsx +++ b/ui/src/components/certimate/DeployToQiniuCDN.tsx @@ -7,45 +7,42 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToQiniuCDNConfigParams = { + domain?: string; +}; + const DeployToQiniuCDN = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - domain: "", - }, + if (!config.id) { + setConfig({ + ...config, + config: {}, }); } }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); - useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + const formSchema = z.object({ + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); + return (
@@ -53,33 +50,16 @@ const DeployToQiniuCDN = () => { { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToSSH.tsx b/ui/src/components/certimate/DeployToSSH.tsx index b59eeb72..8db0cde5 100644 --- a/ui/src/components/certimate/DeployToSSH.tsx +++ b/ui/src/components/certimate/DeployToSSH.tsx @@ -9,24 +9,32 @@ import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectVa import { Textarea } from "@/components/ui/textarea"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToSSHConfigParams = { + format?: string; + certPath?: string; + keyPath?: string; + pfxPassword?: string; + jksAlias?: string; + jksKeypass?: string; + jksStorepass?: string; + shell?: string; + preCommand?: string; + command?: string; +}; + const DeployToSSH = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { format: "pem", certPath: "/etc/nginx/ssl/nginx.crt", keyPath: "/etc/nginx/ssl/nginx.key", - pfxPassword: "", - jksAlias: "", - jksKeypass: "", - jksStorepass: "", - preCommand: "", command: "sudo service nginx reload", }, }); @@ -34,7 +42,7 @@ const DeployToSSH = () => { }, []); useEffect(() => { - setError({}); + setErrors({}); }, []); const formSchema = z @@ -79,63 +87,51 @@ const DeployToSSH = () => { }); useEffect(() => { - const res = formSchema.safeParse(data.config); - if (!res.success) { - setError({ - ...error, - format: res.error.errors.find((e) => e.path[0] === "format")?.message, - certPath: res.error.errors.find((e) => e.path[0] === "certPath")?.message, - keyPath: res.error.errors.find((e) => e.path[0] === "keyPath")?.message, - pfxPassword: res.error.errors.find((e) => e.path[0] === "pfxPassword")?.message, - jksAlias: res.error.errors.find((e) => e.path[0] === "jksAlias")?.message, - jksKeypass: res.error.errors.find((e) => e.path[0] === "jksKeypass")?.message, - jksStorepass: res.error.errors.find((e) => e.path[0] === "jksStorepass")?.message, - preCommand: res.error.errors.find((e) => e.path[0] === "preCommand")?.message, - command: res.error.errors.find((e) => e.path[0] === "command")?.message, - }); - } else { - setError({ - ...error, - format: undefined, - certPath: undefined, - keyPath: undefined, - pfxPassword: undefined, - jksAlias: undefined, - jksKeypass: undefined, - jksStorepass: undefined, - preCommand: undefined, - command: undefined, - }); - } - }, [data]); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + format: res.error?.errors?.find((e) => e.path[0] === "format")?.message, + certPath: res.error?.errors?.find((e) => e.path[0] === "certPath")?.message, + keyPath: res.error?.errors?.find((e) => e.path[0] === "keyPath")?.message, + pfxPassword: res.error?.errors?.find((e) => e.path[0] === "pfxPassword")?.message, + jksAlias: res.error?.errors?.find((e) => e.path[0] === "jksAlias")?.message, + jksKeypass: res.error?.errors?.find((e) => e.path[0] === "jksKeypass")?.message, + jksStorepass: res.error?.errors?.find((e) => e.path[0] === "jksStorepass")?.message, + preCommand: res.error?.errors?.find((e) => e.path[0] === "preCommand")?.message, + command: res.error?.errors?.find((e) => e.path[0] === "command")?.message, + }); + }, [config]); useEffect(() => { - if (data.config?.format === "pem") { - if (/(.pfx|.jks)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.pfx|.jks)$/, ".crt"); - }); - setDeploy(newData); + if (config.config?.format === "pem") { + if (/(.pfx|.jks)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.pfx|.jks)$/, ".crt"); + }) + ); } - } else if (data.config?.format === "pfx") { - if (/(.crt|.jks)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.crt|.jks)$/, ".pfx"); - }); - setDeploy(newData); + } else if (config.config?.format === "pfx") { + if (/(.crt|.jks)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.crt|.jks)$/, ".pfx"); + }) + ); } - } else if (data.config?.format === "jks") { - if (/(.crt|.pfx)$/.test(data.config.certPath)) { - const newData = produce(data, (draft) => { - draft.config ??= {}; - draft.config.certPath = data.config!.certPath.replace(/(.crt|.pfx)$/, ".jks"); - }); - setDeploy(newData); + } else if (config.config?.format === "jks") { + if (/(.crt|.pfx)$/.test(config.config.certPath!)) { + setConfig( + produce(config, (draft) => { + draft.config ??= {}; + draft.config.certPath = config.config!.certPath!.replace(/(.crt|.pfx)$/, ".jks"); + }) + ); } } - }, [data.config?.format]); + }, [config.config?.format]); return ( <> @@ -143,13 +139,13 @@ const DeployToSSH = () => {
-
{error?.format}
+
{errors?.format}
@@ -171,77 +167,77 @@ const DeployToSSH = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.certPath = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.certPath}
+
{errors?.certPath}
- {data.config?.format === "pem" ? ( + {config.config?.format === "pem" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.keyPath = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.keyPath}
+
{errors?.keyPath}
) : ( <> )} - {data.config?.format === "pfx" ? ( + {config.config?.format === "pfx" ? (
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.pfxPassword = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.pfxPassword}
+
{errors?.pfxPassword}
) : ( <> )} - {data.config?.format === "jks" ? ( + {config.config?.format === "jks" ? ( <>
{ - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksAlias = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksAlias}
+
{errors?.jksAlias}
@@ -249,16 +245,16 @@ const DeployToSSH = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksKeypass = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksKeypass}
+
{errors?.jksKeypass}
@@ -266,16 +262,16 @@ const DeployToSSH = () => { { - const newData = produce(data, (draft) => { + const nv = produce(config, (draft) => { draft.config ??= {}; draft.config.jksStorepass = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.jksStorepass}
+
{errors?.jksStorepass}
) : ( @@ -286,34 +282,34 @@ const DeployToSSH = () => { -
{error?.preCommand}
+
{errors?.preCommand}
-
{error?.command}
+
{errors?.command}
diff --git a/ui/src/components/certimate/DeployToTencentCDN.tsx b/ui/src/components/certimate/DeployToTencentCDN.tsx index 05eaf22f..fff3d9b5 100644 --- a/ui/src/components/certimate/DeployToTencentCDN.tsx +++ b/ui/src/components/certimate/DeployToTencentCDN.tsx @@ -7,34 +7,42 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToTencentCDNParams = { + domain?: string; +}; + const DeployToTencentCDN = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - setError({}); + if (!config.id) { + setConfig({ + ...config, + config: {}, + }); + } }, []); useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); + setErrors({}); + }, []); - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + const formSchema = z.object({ + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); + return (
@@ -42,33 +50,16 @@ const DeployToTencentCDN = () => { { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToTencentCLB.tsx b/ui/src/components/certimate/DeployToTencentCLB.tsx index 80c6e761..c9ab56ff 100644 --- a/ui/src/components/certimate/DeployToTencentCLB.tsx +++ b/ui/src/components/certimate/DeployToTencentCLB.tsx @@ -5,106 +5,80 @@ import { produce } from "immer"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { useDeployEditContext } from "./DeployEdit"; -const DeployToTencentCLB = () => { - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); +type DeployToTencentCLBParams = { + region?: string; + resourceType?: string; + loadbalancerId?: string; + listenerId?: string; + domain?: string; +}; +const DeployToTencentCLB = () => { const { t } = useTranslation(); - useEffect(() => { - setError({}); - }, []); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - useEffect(() => { - const clbIdresp = clbIdSchema.safeParse(data.config?.clbId); - if (!clbIdresp.success) { - setError({ - ...error, - clbId: JSON.parse(clbIdresp.error.message)[0].message, - }); - } else { - setError({ - ...error, - clbId: "", - }); - } - }, [data]); - - useEffect(() => { - const lsnIdresp = lsnIdSchema.safeParse(data.config?.lsnId); - if (!lsnIdresp.success) { - setError({ - ...error, - lsnId: JSON.parse(lsnIdresp.error.message)[0].message, - }); - } else { - setError({ - ...error, - lsnId: "", - }); - } - }, [data]); - - useEffect(() => { - const regionResp = regionSchema.safeParse(data.config?.region); - if (!regionResp.success) { - setError({ - ...error, - region: JSON.parse(regionResp.error.message)[0].message, - }); - } else { - setError({ - ...error, - region: "", - }); - } - }, []); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { - lsnId: "", - clbId: "", - domain: "", - region: "", + region: "ap-guangzhou", }, }); } }, []); - const regionSchema = z.string().regex(/^ap-[a-z]+$/, { - message: t("domain.deployment.form.tencent_clb_region.placeholder"), - }); + useEffect(() => { + setErrors({}); + }, []); - const domainSchema = z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }); + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.tencent_clb_region.placeholder")), + resourceType: z.union([z.literal("ssl-deploy"), z.literal("loadbalancer"), z.literal("listener"), z.literal("ruledomain")], { + message: t("domain.deployment.form.tencent_clb_resource_type.placeholder"), + }), + loadbalancerId: z.string().min(1, t("domain.deployment.form.tencent_clb_loadbalancer_id.placeholder")), + listenerId: z.string().optional(), + domain: z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), + }) + .refine( + (data) => { + switch (data.resourceType) { + case "ssl-deploy": + case "listener": + case "ruledomain": + return !!data.listenerId?.trim(); + } + return true; + }, + { + message: t("domain.deployment.form.tencent_clb_listener_id.placeholder"), + path: ["listenerId"], + } + ) + .refine((data) => (data.resourceType === "ruledomain" ? !!data.domain?.trim() : true), { + message: t("domain.deployment.form.tencent_clb_ruledomain.placeholder"), + path: ["domain"], + }); - const clbIdSchema = z.string().regex(/^lb-[a-zA-Z0-9]{8}$/, { - message: t("domain.deployment.form.tencent_clb_id.placeholder"), - }); - - const lsnIdSchema = z.string().regex(/^lbl-.{8}$/, { - message: t("domain.deployment.form.tencent_clb_listener.placeholder"), - }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + resourceType: res.error?.errors?.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error?.errors?.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error?.errors?.find((e) => e.path[0] === "listenerId")?.message, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); return (
@@ -113,136 +87,124 @@ const DeployToTencentCLB = () => { { - const temp = e.target.value; - - const resp = regionSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - region: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - region: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.region = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
- - { - const temp = e.target.value; - - const resp = clbIdSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - clbId: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - clbId: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.clbId = temp; + + +
{errors?.resourceType}
- + { - const temp = e.target.value; - - const resp = lsnIdSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - lsnId: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - lsnId: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.lsnId = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.loadbalancerId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.lsnId}
+
{errors?.loadbalancerId}
-
- - { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, + {config?.config?.resourceType === "ssl-deploy" || config?.config?.resourceType === "listener" || config?.config?.resourceType === "ruledomain" ? ( +
+ + { + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.listenerId = e.target.value?.trim(); }); - } else { - setError({ - ...error, - domain: "", - }); - } + setConfig(nv); + }} + /> +
{errors?.listenerId}
+
+ ) : ( + <> + )} - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; - }); - setDeploy(newData); - }} - /> -
{error?.domain}
-
+ {config?.config?.resourceType === "ssl-deploy" ? ( +
+ + { + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); + }); + setConfig(nv); + }} + /> +
{errors?.domain}
+
+ ) : ( + <> + )} + + {config?.config?.resourceType === "ruledomain" ? ( +
+ + { + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); + }); + setConfig(nv); + }} + /> +
{errors?.domain}
+
+ ) : ( + <> + )} ); }; diff --git a/ui/src/components/certimate/DeployToTencentCOS.tsx b/ui/src/components/certimate/DeployToTencentCOS.tsx index 5f0b8e8b..b045fb22 100644 --- a/ui/src/components/certimate/DeployToTencentCOS.tsx +++ b/ui/src/components/certimate/DeployToTencentCOS.tsx @@ -7,84 +7,49 @@ import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; -const DeployToTencentCOS = () => { - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); +type DeployToTencentCOSParams = { + region?: string; + bucket?: string; + domain?: string; +}; +const DeployToTencentCOS = () => { const { t } = useTranslation(); - useEffect(() => { - setError({}); - }, []); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); - - useEffect(() => { - const bucketResp = bucketSchema.safeParse(data.config?.bucket); - if (!bucketResp.success) { - setError({ - ...error, - bucket: JSON.parse(bucketResp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - }, []); - - useEffect(() => { - const regionResp = regionSchema.safeParse(data.config?.region); - if (!regionResp.success) { - setError({ - ...error, - region: JSON.parse(regionResp.error.message)[0].message, - }); - } else { - setError({ - ...error, - region: "", - }); - } - }, []); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, + if (!config.id) { + setConfig({ + ...config, config: { - region: "", - bucket: "", - domain: "", + region: "ap-guangzhou", }, }); } }, []); - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), + useEffect(() => { + setErrors({}); + }, []); + + const formSchema = z.object({ + region: z.string().min(1, t("domain.deployment.form.tencent_cos_region.placeholder")), + bucket: z.string().min(1, t("domain.deployment.form.tencent_cos_bucket.placeholder")), + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), }); - const regionSchema = z.string().regex(/^ap-[a-z]+$/, { - message: t("domain.deployment.form.tencent_cos_region.placeholder"), - }); - - const bucketSchema = z.string().regex(/^.+-\d+$/, { - message: t("domain.deployment.form.tencent_cos_bucket.placeholder"), - }); + useEffect(() => { + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + region: res.error?.errors?.find((e) => e.path[0] === "region")?.message, + bucket: res.error?.errors?.find((e) => e.path[0] === "bucket")?.message, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); return (
@@ -93,33 +58,16 @@ const DeployToTencentCOS = () => { { - const temp = e.target.value; - - const resp = regionSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - region: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - region: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.region = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.region}
+
{errors?.region}
@@ -127,33 +75,16 @@ const DeployToTencentCOS = () => { { - const temp = e.target.value; - - const resp = bucketSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - bucket: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - bucket: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.bucket = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.bucket = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.bucket}
+
{errors?.bucket}
@@ -161,33 +92,16 @@ const DeployToTencentCOS = () => { { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.domain}
+
{errors?.domain}
); diff --git a/ui/src/components/certimate/DeployToTencentTEO.tsx b/ui/src/components/certimate/DeployToTencentTEO.tsx index 80715fd1..56de4d68 100644 --- a/ui/src/components/certimate/DeployToTencentTEO.tsx +++ b/ui/src/components/certimate/DeployToTencentTEO.tsx @@ -8,52 +8,44 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { useDeployEditContext } from "./DeployEdit"; +type DeployToTencentTEOParams = { + zoneId?: string; + domain?: string; +}; + const DeployToTencentTEO = () => { const { t } = useTranslation(); - const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + const { config, setConfig, errors, setErrors } = useDeployEditContext(); useEffect(() => { - setError({}); + if (!config.id) { + setConfig({ + ...config, + config: {}, + }); + } }, []); useEffect(() => { - const resp = domainSchema.safeParse(data.config?.domain); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - }, [data]); + setErrors({}); + }, []); + + const formSchema = z.object({ + zoneId: z.string().min(1, t("domain.deployment.form.tencent_teo_zone_id.placeholder")), + domain: z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { + message: t("common.errmsg.domain_invalid"), + }), + }); useEffect(() => { - const resp = zoneIdSchema.safeParse(data.config?.zoneId); - if (!resp.success) { - setError({ - ...error, - zoneId: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - zoneId: "", - }); - } - }, [data]); - - const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { - message: t("common.errmsg.domain_invalid"), - }); - - const zoneIdSchema = z.string().regex(/^zone-[0-9a-zA-Z]{9}$/, { - message: t("common.errmsg.zoneid_invalid"), - }); + const res = formSchema.safeParse(config.config); + setErrors({ + ...errors, + zoneId: res.error?.errors?.find((e) => e.path[0] === "zoneId")?.message, + domain: res.error?.errors?.find((e) => e.path[0] === "domain")?.message, + }); + }, [config]); return (
@@ -62,33 +54,16 @@ const DeployToTencentTEO = () => { { - const temp = e.target.value; - - const resp = zoneIdSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - zoneId: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - zoneId: "", - }); - } - - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.zoneId = temp; + const nv = produce(config, (draft) => { + draft.config ??= {}; + draft.config.zoneId = e.target.value?.trim(); }); - setDeploy(newData); + setConfig(nv); }} /> -
{error?.zoneId}
+
{errors?.zoneId}
@@ -96,33 +71,16 @@ const DeployToTencentTEO = () => {