From e579cf6ceb3393da4bc134d61094f45a87fbc0d6 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 09:56:48 +0800 Subject: [PATCH 01/14] feat: add baiducloud cert uploader --- .../baiducloud-cas/baiducloud_cas.go | 139 ++++++++++++++++++ .../baiducloud-cas/baiducloud_cas_test.go | 72 +++++++++ .../pkg/vendors/baiducloud-sdk/cas/api.go | 93 ++++++++++++ .../pkg/vendors/baiducloud-sdk/cas/client.go | 17 +++ .../pkg/vendors/baiducloud-sdk/cas/models.go | 48 ++++++ 5 files changed, 369 insertions(+) create mode 100644 internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go create mode 100644 internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go create mode 100644 internal/pkg/vendors/baiducloud-sdk/cas/api.go create mode 100644 internal/pkg/vendors/baiducloud-sdk/cas/client.go create mode 100644 internal/pkg/vendors/baiducloud-sdk/cas/models.go diff --git a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go b/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go new file mode 100644 index 00000000..9c0167a8 --- /dev/null +++ b/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go @@ -0,0 +1,139 @@ +package baiducloudcas + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/certutil" + bdsdk "github.com/usual2970/certimate/internal/pkg/vendors/baiducloud-sdk/cas" +) + +type UploaderConfig struct { + // 百度智能云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 百度智能云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *bdsdk.Client +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &UploaderProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { + if logger == nil { + u.logger = slog.Default() + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 遍历证书列表,避免重复上传 + // REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#35-%E6%9F%A5%E7%9C%8B%E8%AF%81%E4%B9%A6%E5%88%97%E8%A1%A8%E8%AF%A6%E6%83%85 + listCertDetail, err := u.sdkClient.ListCertDetail() + u.logger.Debug("sdk request 'cert.ListCertDetail'", slog.Any("response", listCertDetail)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.ListCertDetail'") + } else { + for _, certDetail := range listCertDetail.Certs { + // 先对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, certDetail.CertCommonName) { + continue + } + + // 再对比证书有效期 + oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certDetail.CertStartTime) + oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certDetail.CertStopTime) + if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { + continue + } + + // 再对比证书多域名 + if certDetail.CertDNSNames != strings.Join(certX509.DNSNames, ",") { + continue + } + + // 最后对比证书内容 + getCertDetailResp, err := u.sdkClient.GetCertRawData(certDetail.CertId) + u.logger.Debug("sdk request 'cert.GetCertRawData'", slog.Any("certId", certDetail.CertId), slog.Any("response", getCertDetailResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.GetCertRawData'") + } else { + oldCertX509, err := certutil.ParseCertificateFromPEM(getCertDetailResp.CertServerData) + if err != nil { + continue + } + if !certutil.EqualCertificate(certX509, oldCertX509) { + continue + } + } + + // 如果以上信息都一致,则视为已存在相同证书,直接返回 + u.logger.Info("ssl certificate already exists") + return &uploader.UploadResult{ + CertId: certDetail.CertId, + CertName: certDetail.CertName, + }, nil + } + } + + // 创建证书 + // REF: https://cloud.baidu.com/doc/Reference/s/Gjwvz27xu#31-%E5%88%9B%E5%BB%BA%E8%AF%81%E4%B9%A6 + createCertReq := &bdsdk.CreateCertArgs{} + createCertReq.CertName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + createCertReq.CertServerData = certPem + createCertReq.CertPrivateData = privkeyPem + createCertResp, err := u.sdkClient.CreateCert(createCertReq) + u.logger.Debug("sdk request 'cert.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cert.CreateCert'") + } + + return &uploader.UploadResult{ + CertId: createCertResp.CertId, + CertName: createCertResp.CertName, + }, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*bdsdk.Client, error) { + client, err := bdsdk.NewClient(accessKeyId, secretAccessKey, "") + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go b/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go new file mode 100644 index 00000000..d727f71f --- /dev/null +++ b/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go @@ -0,0 +1,72 @@ +package baiducloudcas_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_BAIDUCLOUDCAS_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./baiducloud_cas_test.go -args \ + --CERTIMATE_UPLOADER_BAIDUCLOUDCAS_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_BAIDUCLOUDCAS_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_BAIDUCLOUDCAS_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_UPLOADER_BAIDUCLOUDCAS_SECRETACCESSKEY="your-access-key-secret" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/api.go b/internal/pkg/vendors/baiducloud-sdk/cas/api.go new file mode 100644 index 00000000..78c6c26a --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cas/api.go @@ -0,0 +1,93 @@ +package cert + +import ( + "errors" + "fmt" + + "github.com/baidubce/bce-sdk-go/bce" + "github.com/baidubce/bce-sdk-go/http" + "github.com/baidubce/bce-sdk-go/services/cert" +) + +func (c *Client) CreateCert(args *CreateCertArgs) (*CreateCertResult, error) { + if args == nil { + return nil, errors.New("unset args") + } + + result, err := c.Client.CreateCert(&args.CreateCertArgs) + if err != nil { + return nil, err + } + + return &CreateCertResult{CreateCertResult: *result}, nil +} + +func (c *Client) ListCerts() (*ListCertResult, error) { + result, err := c.Client.ListCerts() + if err != nil { + return nil, err + } + + return &ListCertResult{ListCertResult: *result}, nil +} + +func (c *Client) ListCertDetail() (*ListCertDetailResult, error) { + result, err := c.Client.ListCertDetail() + if err != nil { + return nil, err + } + + return &ListCertDetailResult{ListCertDetailResult: *result}, nil +} + +func (c *Client) GetCertMeta(id string) (*CertificateMeta, error) { + result, err := c.Client.GetCertMeta(id) + if err != nil { + return nil, err + } + + return &CertificateMeta{CertificateMeta: *result}, nil +} + +func (c *Client) GetCertDetail(id string) (*CertificateDetailMeta, error) { + result, err := c.Client.GetCertDetail(id) + if err != nil { + return nil, err + } + + return &CertificateDetailMeta{CertificateDetailMeta: *result}, nil +} + +func (c *Client) GetCertRawData(id string) (*CertificateRawData, error) { + result := &CertificateRawData{} + err := bce.NewRequestBuilder(c). + WithMethod(http.GET). + WithURL(cert.URI_PREFIX + cert.REQUEST_CERT_URL + "/" + id + "/rawData"). + WithResult(result). + Do() + + return result, err +} + +func (c *Client) UpdateCertName(id string, args *UpdateCertNameArgs) error { + if args == nil { + return errors.New("unset args") + } + + err := c.Client.UpdateCertName(id, &args.UpdateCertNameArgs) + return err +} + +func (c *Client) UpdateCertData(id string, args *UpdateCertDataArgs) error { + if args == nil { + return fmt.Errorf("unset args") + } + + err := c.Client.UpdateCertData(id, &args.UpdateCertDataArgs) + return err +} + +func (c *Client) DeleteCert(id string) error { + err := c.Client.DeleteCert(id) + return err +} diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/client.go b/internal/pkg/vendors/baiducloud-sdk/cas/client.go new file mode 100644 index 00000000..02c4feff --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cas/client.go @@ -0,0 +1,17 @@ +package cert + +import ( + "github.com/baidubce/bce-sdk-go/services/cert" +) + +type Client struct { + *cert.Client +} + +func NewClient(ak, sk, endPoint string) (*Client, error) { + client, err := cert.NewClient(ak, sk, endPoint) + if err != nil { + return nil, err + } + return &Client{client}, nil +} diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/models.go b/internal/pkg/vendors/baiducloud-sdk/cas/models.go new file mode 100644 index 00000000..d3fe0449 --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cas/models.go @@ -0,0 +1,48 @@ +package cert + +import "github.com/baidubce/bce-sdk-go/services/cert" + +type CreateCertArgs struct { + cert.CreateCertArgs +} + +type CreateCertResult struct { + cert.CreateCertResult +} + +type UpdateCertNameArgs struct { + cert.UpdateCertNameArgs +} + +type CertificateMeta struct { + cert.CertificateMeta +} + +type CertificateDetailMeta struct { + cert.CertificateDetailMeta +} + +type CertificateRawData struct { + CertId string `json:"certId"` + CertName string `json:"certName"` + CertServerData string `json:"certServerData"` + CertPrivateData string `json:"certPrivateKey"` + CertLinkData string `json:"certLinkData,omitempty"` + CertType int `json:"certType,omitempty"` +} + +type ListCertResult struct { + cert.ListCertResult +} + +type ListCertDetailResult struct { + cert.ListCertDetailResult +} + +type UpdateCertDataArgs struct { + cert.UpdateCertDataArgs +} + +type CertInServiceMeta struct { + cert.CertInServiceMeta +} From aaec840d8c488f2abef185ed0ad1bcfa5120074c Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 11:09:32 +0800 Subject: [PATCH 02/14] feat: add baiducloud blb deployer --- internal/deployer/providers.go | 15 +- internal/domain/provider.go | 1 + .../providers/aliyun-clb/aliyun_clb_test.go | 2 +- .../baiducloud-blb/baiducloud_blb.go | 324 ++++++++++++++++++ .../baiducloud-blb/baiducloud_blb_test.go | 86 +++++ .../providers/baiducloud-blb/consts.go | 10 + .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../DeployNodeConfigFormAliyunCLBConfig.tsx | 16 +- ...eployNodeConfigFormBaiduCloudBLBConfig.tsx | 142 ++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 16 + ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 16 + 14 files changed, 619 insertions(+), 16 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go create mode 100644 internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go create mode 100644 internal/pkg/core/deployer/providers/baiducloud-blb/consts.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index c6238aff..5957fded 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -24,6 +24,7 @@ import ( pAWSACM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-acm" pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront" pAzureKeyVault "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/azure-keyvault" + pBaiduCloudBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb" pBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn" pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console" @@ -303,7 +304,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeBaiduCloudCDN: + case domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN: { access := domain.AccessConfigForBaiduCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -311,6 +312,18 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } switch options.Provider { + case domain.DeployProviderTypeBaiduCloudBLB: + deployer, err := pBaiduCloudBLB.NewDeployer(&pBaiduCloudBLB.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + Region: maputil.GetString(options.ProviderDeployConfig, "region"), + ResourceType: pBaiduCloudBLB.ResourceType(maputil.GetString(options.ProviderDeployConfig, "resourceType")), + LoadbalancerId: maputil.GetString(options.ProviderDeployConfig, "loadbalancerId"), + ListenerPort: maputil.GetInt32(options.ProviderDeployConfig, "listenerPort"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + }) + return deployer, err + case domain.DeployProviderTypeBaiduCloudCDN: deployer, err := pBaiduCloudCDN.NewDeployer(&pBaiduCloudCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index af877778..6cc229a1 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -135,6 +135,7 @@ const ( DeployProviderTypeAWSACM = DeployProviderType("aws-acm") DeployProviderTypeAWSCloudFront = DeployProviderType("aws-cloudfront") DeployProviderTypeAzureKeyVault = DeployProviderType("azure-keyvault") + DeployProviderTypeBaiduCloudBLB = DeployProviderType("baiducloud-blb") DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn") DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn") DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console") diff --git a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go index a845bf86..f688edf9 100644 --- a/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go +++ b/internal/pkg/core/deployer/providers/aliyun-clb/aliyun_clb_test.go @@ -46,7 +46,7 @@ Shell command to run this test: --CERTIMATE_DEPLOYER_ALIYUNCLB_REGION="cn-hangzhou" \ --CERTIMATE_DEPLOYER_ALIYUNCLB_LOADBALANCERID="your-clb-instance-id" \ --CERTIMATE_DEPLOYER_ALIYUNCLB_LISTENERPORT=443 \ - --CERTIMATE_DEPLOYER_ALIYUNCLB_DOMAIN="your-alb-sni-domain" + --CERTIMATE_DEPLOYER_ALIYUNCLB_DOMAIN="your-clb-sni-domain" */ func TestDeploy(t *testing.T) { flag.Parse() diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go new file mode 100644 index 00000000..1c0fe371 --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -0,0 +1,324 @@ +package baiducloudblb + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + + bceblb "github.com/baidubce/bce-sdk-go/services/blb" + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" + "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" +) + +type DeployerConfig struct { + // 百度智能云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 百度智能云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` + // 百度智能云区域。 + Region string `json:"region"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 负载均衡实例 ID。 + // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 + LoadbalancerId string `json:"loadbalancerId,omitempty"` + // 负载均衡监听端口。 + // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 + ListenerPort int32 `json:"listenerPort,omitempty"` + // SNI 域名(支持泛域名)。 + // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 + Domain string `json:"domain,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *bceblb.Client + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 CAS + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_LOADBALANCER: + if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { + return nil, err + } + + case RESOURCE_TYPE_LISTENER: + if err := d.deployToListener(ctx, upres.CertId); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { + if d.config.LoadbalancerId == "" { + return errors.New("config `loadbalancerId` is required") + } + + // 查询 BLB 实例详情 + // REF: https://cloud.baidu.com/doc/BLB/s/njwvxnv79#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85 + describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId) + d.logger.Debug("sdk request 'blb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeLoadBalancerDetail'") + } + + // 获取全部 HTTPS/SSL 监听端口 + listeners := make([]struct { + Type string + Port int32 + }, 0) + for _, listener := range describeLoadBalancerDetailResp.Listener { + if listener.Type == "HTTPS" || listener.Type == "SSL" { + listenerPort, err := strconv.Atoi(listener.Port) + if err != nil { + continue + } + + listeners = append(listeners, struct { + Type string + Port int32 + }{ + Type: listener.Type, + Port: int32(listenerPort), + }) + } + } + + // 遍历更新监听证书 + if len(listeners) == 0 { + d.logger.Info("no blb listeners to deploy") + } else { + d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) + var errs []error + + for _, listener := range listeners { + if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + } + + return nil +} + +func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error { + if d.config.LoadbalancerId == "" { + return errors.New("config `loadbalancerId` is required") + } + if d.config.ListenerPort == 0 { + return errors.New("config `listenerPort` is required") + } + + // 查询监听 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describealllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC + describeAllListenersRequest := &bceblb.DescribeListenerArgs{ + ListenerPort: uint16(d.config.ListenerPort), + } + describeAllListenersResp, err := d.sdkClient.DescribeAllListeners(d.config.LoadbalancerId, describeAllListenersRequest) + d.logger.Debug("sdk request 'blb.DescribeAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAllListenersRequest), slog.Any("response", describeAllListenersResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeAllListeners'") + } + + // 获取全部 HTTPS/SSL 监听端口 + listeners := make([]struct { + Type string + Port int32 + }, 0) + for _, listener := range describeAllListenersResp.AllListenerList { + if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" { + listeners = append(listeners, struct { + Type string + Port int32 + }{ + Type: listener.ListenerType, + Port: int32(listener.ListenerPort), + }) + } + } + + // 遍历更新监听证书 + if len(listeners) == 0 { + d.logger.Info("no blb listeners to deploy") + } else { + d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) + var errs []error + + for _, listener := range listeners { + if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + } + + return nil +} + +func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error { + switch strings.ToUpper(cloudListenerType) { + case "HTTPS": + return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) + case "SSL": + return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) + default: + return fmt.Errorf("unsupported listener type: %s", cloudListenerType) + } +} + +func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { + // 查询 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#describehttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8 + describeHTTPSListenersReq := &bceblb.DescribeListenerArgs{ + ListenerPort: uint16(cloudHttpsListenerPort), + MaxKeys: 1, + } + describeHTTPSListenersResp, err := d.sdkClient.DescribeHTTPSListeners(cloudLoadbalancerId, describeHTTPSListenersReq) + d.logger.Debug("sdk request 'blb.DescribeHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeHTTPSListenersReq), slog.Any("response", describeHTTPSListenersResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.DescribeHTTPSListeners'") + } else if len(describeHTTPSListenersResp.ListenerList) == 0 { + return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort) + } + + if d.config.Domain == "" { + // 未指定 SNI,只需部署到监听器 + + // 更新 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 + updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ + ListenerPort: uint16(cloudHttpsListenerPort), + CertIds: []string{cloudCertId}, + } + err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq) + d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateHTTPSListener'") + } + } else { + // 指定 SNI,需部署到扩展域名 + + // 更新 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 + updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ + ListenerPort: uint16(cloudHttpsListenerPort), + AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel { + if domain.Host == d.config.Domain { + return bceblb.AdditionalCertDomainsModel{ + Host: domain.Host, + CertId: cloudCertId, + } + } + + return bceblb.AdditionalCertDomainsModel{ + Host: domain.Host, + CertId: domain.CertId, + } + }), + } + err := d.sdkClient.UpdateHTTPSListener(cloudLoadbalancerId, updateHTTPSListenerReq) + d.logger.Debug("sdk request 'blb.UpdateHTTPSListener'", slog.Any("request", updateHTTPSListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateHTTPSListener'") + } + } + + return nil +} + +func (d *DeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { + // 更新 SSL 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8 + updateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{ + ListenerPort: uint16(cloudHttpsListenerPort), + CertIds: []string{cloudCertId}, + } + err := d.sdkClient.UpdateSSLListener(cloudLoadbalancerId, updateSSLListenerReq) + d.logger.Debug("sdk request 'blb.UpdateSSLListener'", slog.Any("request", updateSSLListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'blb.UpdateSSLListener'") + } + + return nil +} + +func createSdkClient(accessKeyId, secretAccessKey, region string) (*bceblb.Client, error) { + endpoint := "" + if region != "" { + endpoint = fmt.Sprintf("blb.%s.baidubce.com", region) + } + + client, err := bceblb.NewClient(accessKeyId, secretAccessKey, endpoint) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go new file mode 100644 index 00000000..756e6f8f --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb_test.go @@ -0,0 +1,86 @@ +package baiducloudblb_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fRegion string + fLoadbalancerId string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") + flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./baiducloud_blb_test.go -args \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_REGION="bj" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_LOADBALANCERID="your-blb-loadbalancer-id" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDBLB_DOMAIN="your-blb-sni-domain" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), + fmt.Sprintf("REGION: %v", fRegion), + fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, + Region: fRegion, + LoadbalancerId: fLoadbalancerId, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/consts.go b/internal/pkg/core/deployer/providers/baiducloud-blb/consts.go new file mode 100644 index 00000000..52cb8804 --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/consts.go @@ -0,0 +1,10 @@ +package baiducloudblb + +type ResourceType string + +const ( + // 资源类型:部署到指定负载均衡器。 + RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer") + // 资源类型:部署到指定监听器。 + RESOURCE_TYPE_LISTENER = ResourceType("listener") +) diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index fdf0aaa5..2ba13579 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -33,6 +33,7 @@ import DeployNodeConfigFormAliyunWAFConfig from "./DeployNodeConfigFormAliyunWAF import DeployNodeConfigFormAWSACMConfig from "./DeployNodeConfigFormAWSACMConfig"; import DeployNodeConfigFormAWSCloudFrontConfig from "./DeployNodeConfigFormAWSCloudFrontConfig"; import DeployNodeConfigFormAzureKeyVaultConfig from "./DeployNodeConfigFormAzureKeyVaultConfig"; +import DeployNodeConfigFormBaiduCloudBLBConfig from "./DeployNodeConfigFormBaiduCloudBLBConfig"; import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaiduCloudCDNConfig"; import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig"; import DeployNodeConfigFormBaotaPanelConsoleConfig from "./DeployNodeConfigFormBaotaPanelConsoleConfig"; @@ -185,6 +186,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.AZURE_KEYVAULT: return ; + case DEPLOY_PROVIDERS.BAIDUCLOUD_BLB: + return ; case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN: return ; case DEPLOY_PROVIDERS.BAISHAN_CDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx index dfbfc707..2dd66b42 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCLBConfig.tsx @@ -54,20 +54,8 @@ const DeployNodeConfigFormAliyunCLBConfig = ({ .max(64, t("common.errmsg.string_max", { max: 64 })) .trim(), listenerPort: z - .union([ - z - .number() - .refine( - (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), - t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder") - ), - z - .string() - .refine( - (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), - t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder") - ), - ]) + .union([z.number(), z.string()]) + .refine((v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), t("workflow_node.deploy.form.aliyun_clb_listener_port.placeholder")) .nullish(), domain: z .string() diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx new file mode 100644 index 00000000..f6ab1750 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudBLBConfig.tsx @@ -0,0 +1,142 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; +import { validDomainName, validPortNumber } from "@/utils/validators"; + +type DeployNodeConfigFormBaiduCloudBLBConfigFieldValues = Nullish<{ + resourceType: string; + region: string; + loadbalancerId?: string; + listenerPort?: string | number; + domain?: string; +}>; + +export type DeployNodeConfigFormBaiduCloudBLBConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormBaiduCloudBLBConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; +const RESOURCE_TYPE_LISTENER = "listener" as const; + +const initFormModel = (): DeployNodeConfigFormBaiduCloudBLBConfigFieldValues => { + return { + listenerPort: 443, + }; +}; + +const DeployNodeConfigFormBaiduCloudBLBConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormBaiduCloudBLBConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], { + message: t("workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder"), + }), + region: z + .string({ message: t("workflow_node.deploy.form.baiducloud_blb_region.placeholder") }) + .nonempty(t("workflow_node.deploy.form.baiducloud_blb_region.placeholder")) + .trim(), + loadbalancerId: z + .string({ message: t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder") }) + .min(1, t("workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + listenerPort: z + .union([z.number(), z.string()]) + .refine( + (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), + t("workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder") + ) + .nullish(), + domain: z + .string() + .nullish() + .refine((v) => { + if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; + return !v || validDomainName(v!, { allowWildcard: true }); + }, t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + } + > + + + + + } + > + + + + + + } + > + + + +
+ ); +}; + +export default DeployNodeConfigFormBaiduCloudBLBConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index a0119bca..37c4bc00 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -249,6 +249,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({ AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`, AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`, AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`, + BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`, BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`, BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`, BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`, @@ -360,6 +361,7 @@ export const deployProvidersMap: Maphttps://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates", + "workflow_node.deploy.form.baiducloud_blb_resource_type.label": "Resource type", + "workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "BLB load balancer", + "workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label": "BLB listener", + "workflow_node.deploy.form.baiducloud_blb_region.label": "Baidu Cloud BLB region", + "workflow_node.deploy.form.baiducloud_blb_region.placeholder": "Please enter Baidu Cloud BLB region (e.g. bj)", + "workflow_node.deploy.form.baiducloud_blb_region.tooltip": "For more information, see https://cloud.baidu.com/doc/BLB/s/cjwvxnzix", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label": "Baidu Cloud BLB load balancer ID", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder": "Please enter Baidu Cloud BLB load balancer ID", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/blb/list", + "workflow_node.deploy.form.baiducloud_blb_listener_port.label": "Baidu Cloud BLB listener port", + "workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder": "Please enter Baidu Cloud BLB listener port", + "workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/blb/list", + "workflow_node.deploy.form.baiducloud_blb_snidomain.label": "Baidu Cloud BLB SNI domain (Optional)", + "workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder": "Please enter Baidu Cloud BLB SNI domain name", + "workflow_node.deploy.form.baiducloud_blb_snidomain.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/blb/list", "workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain", "workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name", "workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see https://console.bce.baidu.com/cdn", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index d3086452..acee7a7b 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -28,6 +28,7 @@ "provider.azure.dns": "Azure - DNS", "provider.azure.keyvault": "Azure - KeyVault", "provider.baiducloud": "百度智能云", + "provider.baiducloud.blb": "百度智能云 - 普通型负载均衡 BLB", "provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN", "provider.baiducloud.dns": "百度智能云 - 智能云解析 DNS", "provider.baishan": "白山云", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 1f7ce4a5..05868ff3 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -222,6 +222,22 @@ "workflow_node.deploy.form.azure_keyvault_name.label": "Azure KeyVault 名称", "workflow_node.deploy.form.azure_keyvault_name.placeholder": "请输入 Azure KeyVault 名称", "workflow_node.deploy.form.azure_keyvault_name.tooltip": "这是什么?请参阅 https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates", + "workflow_node.deploy.form.baiducloud_blb_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/SSL 监听的证书", + "workflow_node.deploy.form.baiducloud_blb_resource_type.option.listener.label": "替换指定负载均衡监听的证书", + "workflow_node.deploy.form.baiducloud_blb_region.label": "百度智能云 BLB 服务地域", + "workflow_node.deploy.form.baiducloud_blb_region.placeholder": "请输入百度智能云 BLB 服务地域(例如:bj)", + "workflow_node.deploy.form.baiducloud_blb_region.tooltip": "这是什么?请参阅 https://cloud.baidu.com/doc/BLB/s/cjwvxnzix", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.label": "百度智能云 BLB 负载均衡器 ID", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.placeholder": "请输入百度智能云 BLB 负载均衡器 ID", + "workflow_node.deploy.form.baiducloud_blb_loadbalancer_id.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/blb/list", + "workflow_node.deploy.form.baiducloud_blb_listener_port.label": "百度智能云 BLB 监听端口", + "workflow_node.deploy.form.baiducloud_blb_listener_port.placeholder": "请输入百度智能云 BLB 监听端口", + "workflow_node.deploy.form.baiducloud_blb_listener_port.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/blb/list", + "workflow_node.deploy.form.baiducloud_blb_snidomain.label": "百度智能云 BLB 扩展域名(可选)", + "workflow_node.deploy.form.baiducloud_blb_snidomain.placeholder": "请输入百度智能云 BLB 扩展域名(支持泛域名)", + "workflow_node.deploy.form.baiducloud_blb_snidomain.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/blb/list

不填写时,将替换监听器的默认证书。", "workflow_node.deploy.form.baiducloud_cdn_domain.label": "百度智能云 CDN 加速域名", "workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "请输入百度智能云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/cdn", From 5de033814b1b5a50e58f732e3de62cf7addd539c Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 13:22:35 +0800 Subject: [PATCH 03/14] feat: add baiducloud appblb deployer --- internal/deployer/providers.go | 15 +- internal/domain/provider.go | 1 + .../baiducloud-appblb/baiducloud_appblb.go | 332 ++++++++++++++++++ .../baiducloud_appblb_test.go | 86 +++++ .../providers/baiducloud-appblb/consts.go | 10 + .../baiducloud-blb/baiducloud_blb.go | 8 + .../workflow/node/DeployNodeConfigForm.tsx | 3 + ...oyNodeConfigFormBaiduCloudAppBLBConfig.tsx | 142 ++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 16 + ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 16 + 13 files changed, 632 insertions(+), 1 deletion(-) create mode 100644 internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go create mode 100644 internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go create mode 100644 internal/pkg/core/deployer/providers/baiducloud-appblb/consts.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 5957fded..39b8a49f 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -24,6 +24,7 @@ import ( pAWSACM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-acm" pAWSCloudFront "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/aws-cloudfront" pAzureKeyVault "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/azure-keyvault" + pBaiduCloudAppBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb" pBaiduCloudBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb" pBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn" @@ -304,7 +305,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN: + case domain.DeployProviderTypeBaiduCloudAppBLB, domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN: { access := domain.AccessConfigForBaiduCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -312,6 +313,18 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } switch options.Provider { + case domain.DeployProviderTypeBaiduCloudAppBLB: + deployer, err := pBaiduCloudAppBLB.NewDeployer(&pBaiduCloudAppBLB.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + Region: maputil.GetString(options.ProviderDeployConfig, "region"), + ResourceType: pBaiduCloudAppBLB.ResourceType(maputil.GetString(options.ProviderDeployConfig, "resourceType")), + LoadbalancerId: maputil.GetString(options.ProviderDeployConfig, "loadbalancerId"), + ListenerPort: maputil.GetInt32(options.ProviderDeployConfig, "listenerPort"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + }) + return deployer, err + case domain.DeployProviderTypeBaiduCloudBLB: deployer, err := pBaiduCloudBLB.NewDeployer(&pBaiduCloudBLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 6cc229a1..79ee6709 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -135,6 +135,7 @@ const ( DeployProviderTypeAWSACM = DeployProviderType("aws-acm") DeployProviderTypeAWSCloudFront = DeployProviderType("aws-cloudfront") DeployProviderTypeAzureKeyVault = DeployProviderType("azure-keyvault") + DeployProviderTypeBaiduCloudAppBLB = DeployProviderType("baiducloud-appblb") DeployProviderTypeBaiduCloudBLB = DeployProviderType("baiducloud-blb") DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn") DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn") diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go new file mode 100644 index 00000000..c470bfcd --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go @@ -0,0 +1,332 @@ +package baiducloudappblb + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "strings" + + bceappblb "github.com/baidubce/bce-sdk-go/services/appblb" + "github.com/google/uuid" + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" + "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" +) + +type DeployerConfig struct { + // 百度智能云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 百度智能云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` + // 百度智能云区域。 + Region string `json:"region"` + // 部署资源类型。 + ResourceType ResourceType `json:"resourceType"` + // 负载均衡实例 ID。 + // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时必填。 + LoadbalancerId string `json:"loadbalancerId,omitempty"` + // 负载均衡监听端口。 + // 部署资源类型为 [RESOURCE_TYPE_LISTENER] 时必填。 + ListenerPort int32 `json:"listenerPort,omitempty"` + // SNI 域名(支持泛域名)。 + // 部署资源类型为 [RESOURCE_TYPE_LOADBALANCER]、[RESOURCE_TYPE_LISTENER] 时选填。 + Domain string `json:"domain,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *bceappblb.Client + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.SecretAccessKey, config.Region) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 CAS + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_LOADBALANCER: + if err := d.deployToLoadbalancer(ctx, upres.CertId); err != nil { + return nil, err + } + + case RESOURCE_TYPE_LISTENER: + if err := d.deployToListener(ctx, upres.CertId); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type: %s", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *DeployerProvider) deployToLoadbalancer(ctx context.Context, cloudCertId string) error { + if d.config.LoadbalancerId == "" { + return errors.New("config `loadbalancerId` is required") + } + + // 查询 BLB 实例详情 + // REF: https://cloud.baidu.com/doc/BLB/s/6jwvxnyhi#describeloadbalancerdetail%E6%9F%A5%E8%AF%A2blb%E5%AE%9E%E4%BE%8B%E8%AF%A6%E6%83%85 + describeLoadBalancerDetailResp, err := d.sdkClient.DescribeLoadBalancerDetail(d.config.LoadbalancerId) + d.logger.Debug("sdk request 'appblb.DescribeLoadBalancerAttribute'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("response", describeLoadBalancerDetailResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeLoadBalancerDetail'") + } + + // 获取全部 HTTPS/SSL 监听端口 + listeners := make([]struct { + Type string + Port int32 + }, 0) + for _, listener := range describeLoadBalancerDetailResp.Listener { + if listener.Type == "HTTPS" || listener.Type == "SSL" { + listenerPort, err := strconv.Atoi(listener.Port) + if err != nil { + continue + } + + listeners = append(listeners, struct { + Type string + Port int32 + }{ + Type: listener.Type, + Port: int32(listenerPort), + }) + } + } + + // 遍历更新监听证书 + if len(listeners) == 0 { + d.logger.Info("no blb listeners to deploy") + } else { + d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) + var errs []error + + for _, listener := range listeners { + if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + } + + return nil +} + +func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId string) error { + if d.config.LoadbalancerId == "" { + return errors.New("config `loadbalancerId` is required") + } + if d.config.ListenerPort == 0 { + return errors.New("config `listenerPort` is required") + } + + // 查询监听 + // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeappalllisteners%E6%9F%A5%E8%AF%A2%E6%89%80%E6%9C%89%E7%9B%91%E5%90%AC + describeAppAllListenersRequest := &bceappblb.DescribeAppListenerArgs{ + ListenerPort: uint16(d.config.ListenerPort), + } + describeAppAllListenersResp, err := d.sdkClient.DescribeAppAllListeners(d.config.LoadbalancerId, describeAppAllListenersRequest) + d.logger.Debug("sdk request 'appblb.DescribeAppAllListeners'", slog.String("blbId", d.config.LoadbalancerId), slog.Any("request", describeAppAllListenersRequest), slog.Any("response", describeAppAllListenersResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeAppAllListeners'") + } + + // 获取全部 HTTPS/SSL 监听端口 + listeners := make([]struct { + Type string + Port int32 + }, 0) + for _, listener := range describeAppAllListenersResp.ListenerList { + if listener.ListenerType == "HTTPS" || listener.ListenerType == "SSL" { + listeners = append(listeners, struct { + Type string + Port int32 + }{ + Type: listener.ListenerType, + Port: int32(listener.ListenerPort), + }) + } + } + + // 遍历更新监听证书 + if len(listeners) == 0 { + d.logger.Info("no blb listeners to deploy") + } else { + d.logger.Info("found https/ssl listeners to deploy", slog.Any("listeners", listeners)) + var errs []error + + for _, listener := range listeners { + if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, listener.Type, listener.Port, cloudCertId); err != nil { + errs = append(errs, err) + } + } + + if len(errs) > 0 { + return errors.Join(errs...) + } + } + + return nil +} + +func (d *DeployerProvider) updateListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudListenerType string, cloudListenerPort int32, cloudCertId string) error { + switch strings.ToUpper(cloudListenerType) { + case "HTTPS": + return d.updateHttpsListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) + case "SSL": + return d.updateSslListenerCertificate(ctx, cloudLoadbalancerId, cloudListenerPort, cloudCertId) + default: + return fmt.Errorf("unsupported listener type: %s", cloudListenerType) + } +} + +func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { + // 查询 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#describeapphttpslisteners%E6%9F%A5%E8%AF%A2https%E7%9B%91%E5%90%AC%E5%99%A8 + describeAppHTTPSListenersReq := &bceappblb.DescribeAppListenerArgs{ + ListenerPort: uint16(cloudHttpsListenerPort), + MaxKeys: 1, + } + describeAppHTTPSListenersResp, err := d.sdkClient.DescribeAppHTTPSListeners(cloudLoadbalancerId, describeAppHTTPSListenersReq) + d.logger.Debug("sdk request 'appblb.DescribeAppHTTPSListeners'", slog.String("blbId", cloudLoadbalancerId), slog.Any("request", describeAppHTTPSListenersReq), slog.Any("response", describeAppHTTPSListenersResp)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.DescribeAppHTTPSListeners'") + } else if len(describeAppHTTPSListenersResp.ListenerList) == 0 { + return fmt.Errorf("listener %s:%d not found", cloudLoadbalancerId, cloudHttpsListenerPort) + } + + if d.config.Domain == "" { + // 未指定 SNI,只需部署到监听器 + + // 更新 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateapphttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 + updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{ + ClientToken: generateClientToken(), + ListenerPort: uint16(cloudHttpsListenerPort), + CertIds: []string{cloudCertId}, + } + err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq) + d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppHTTPSListener'") + } + } else { + // 指定 SNI,需部署到扩展域名 + + // 更新 HTTPS 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 + updateAppHTTPSListenerReq := &bceappblb.UpdateAppHTTPSListenerArgs{ + ClientToken: generateClientToken(), + ListenerPort: uint16(cloudHttpsListenerPort), + AdditionalCertDomains: sliceutil.Map(describeAppHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceappblb.AdditionalCertDomainsModel) bceappblb.AdditionalCertDomainsModel { + if domain.Host == d.config.Domain { + return bceappblb.AdditionalCertDomainsModel{ + Host: domain.Host, + CertId: cloudCertId, + } + } + + return bceappblb.AdditionalCertDomainsModel{ + Host: domain.Host, + CertId: domain.CertId, + } + }), + } + err := d.sdkClient.UpdateAppHTTPSListener(cloudLoadbalancerId, updateAppHTTPSListenerReq) + d.logger.Debug("sdk request 'appblb.UpdateAppHTTPSListener'", slog.Any("request", updateAppHTTPSListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppHTTPSListener'") + } + } + + return nil +} + +func (d *DeployerProvider) updateSslListenerCertificate(ctx context.Context, cloudLoadbalancerId string, cloudHttpsListenerPort int32, cloudCertId string) error { + // 更新 SSL 监听器 + // REF: https://cloud.baidu.com/doc/BLB/s/ujwvxnyux#updateappssllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8 + updateAppSSLListenerReq := &bceappblb.UpdateAppSSLListenerArgs{ + ClientToken: generateClientToken(), + ListenerPort: uint16(cloudHttpsListenerPort), + CertIds: []string{cloudCertId}, + } + err := d.sdkClient.UpdateAppSSLListener(cloudLoadbalancerId, updateAppSSLListenerReq) + d.logger.Debug("sdk request 'appblb.UpdateAppSSLListener'", slog.Any("request", updateAppSSLListenerReq)) + if err != nil { + return xerrors.Wrap(err, "failed to execute sdk request 'appblb.UpdateAppSSLListener'") + } + + return nil +} + +func createSdkClient(accessKeyId, secretAccessKey, region string) (*bceappblb.Client, error) { + endpoint := "" + if region != "" { + endpoint = fmt.Sprintf("blb.%s.baidubce.com", region) + } + + client, err := bceappblb.NewClient(accessKeyId, secretAccessKey, endpoint) + if err != nil { + return nil, err + } + + return client, nil +} + +func generateClientToken() string { + return strings.ReplaceAll(uuid.New().String(), "-", "") +} diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go new file mode 100644 index 00000000..6753475c --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb_test.go @@ -0,0 +1,86 @@ +package baiducloudappblb_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fRegion string + fLoadbalancerId string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fRegion, argsPrefix+"REGION", "", "") + flag.StringVar(&fLoadbalancerId, argsPrefix+"LOADBALANCERID", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./baiducloud_appblb_test.go -args \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_REGION="bj" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_LOADBALANCERID="your-blb-loadbalancer-id" \ + --CERTIMATE_DEPLOYER_BAIDUCLOUDAPPBLB_DOMAIN="your-blb-sni-domain" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("SECRETACCESSKEY: %v", fSecretAccessKey), + fmt.Sprintf("REGION: %v", fRegion), + fmt.Sprintf("LOADBALANCERID: %v", fLoadbalancerId), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + ResourceType: provider.RESOURCE_TYPE_LOADBALANCER, + Region: fRegion, + LoadbalancerId: fLoadbalancerId, + Domain: fDomain, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/consts.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/consts.go new file mode 100644 index 00000000..3955f9a0 --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/consts.go @@ -0,0 +1,10 @@ +package baiducloudappblb + +type ResourceType string + +const ( + // 资源类型:部署到指定负载均衡器。 + RESOURCE_TYPE_LOADBALANCER = ResourceType("loadbalancer") + // 资源类型:部署到指定监听器。 + RESOURCE_TYPE_LISTENER = ResourceType("listener") +) diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go index 1c0fe371..c6b19da6 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -9,6 +9,7 @@ import ( "strings" bceblb "github.com/baidubce/bce-sdk-go/services/blb" + "github.com/google/uuid" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" @@ -254,6 +255,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ + ClientToken: generateClientToken(), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: []string{cloudCertId}, } @@ -268,6 +270,7 @@ func (d *DeployerProvider) updateHttpsListenerCertificate(ctx context.Context, c // 更新 HTTPS 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatehttpslistener%E6%9B%B4%E6%96%B0https%E7%9B%91%E5%90%AC%E5%99%A8 updateHTTPSListenerReq := &bceblb.UpdateHTTPSListenerArgs{ + ClientToken: generateClientToken(), ListenerPort: uint16(cloudHttpsListenerPort), AdditionalCertDomains: sliceutil.Map(describeHTTPSListenersResp.ListenerList[0].AdditionalCertDomains, func(domain bceblb.AdditionalCertDomainsModel) bceblb.AdditionalCertDomainsModel { if domain.Host == d.config.Domain { @@ -297,6 +300,7 @@ func (d *DeployerProvider) updateSslListenerCertificate(ctx context.Context, clo // 更新 SSL 监听器 // REF: https://cloud.baidu.com/doc/BLB/s/yjwvxnvl6#updatessllistener%E6%9B%B4%E6%96%B0ssl%E7%9B%91%E5%90%AC%E5%99%A8 updateSSLListenerReq := &bceblb.UpdateSSLListenerArgs{ + ClientToken: generateClientToken(), ListenerPort: uint16(cloudHttpsListenerPort), CertIds: []string{cloudCertId}, } @@ -322,3 +326,7 @@ func createSdkClient(accessKeyId, secretAccessKey, region string) (*bceblb.Clien return client, nil } + +func generateClientToken() string { + return strings.ReplaceAll(uuid.New().String(), "-", "") +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 2ba13579..f1f6c6a0 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -33,6 +33,7 @@ import DeployNodeConfigFormAliyunWAFConfig from "./DeployNodeConfigFormAliyunWAF import DeployNodeConfigFormAWSACMConfig from "./DeployNodeConfigFormAWSACMConfig"; import DeployNodeConfigFormAWSCloudFrontConfig from "./DeployNodeConfigFormAWSCloudFrontConfig"; import DeployNodeConfigFormAzureKeyVaultConfig from "./DeployNodeConfigFormAzureKeyVaultConfig"; +import DeployNodeConfigFormBaiduCloudAppBLBConfig from "./DeployNodeConfigFormBaiduCloudAppBLBConfig"; import DeployNodeConfigFormBaiduCloudBLBConfig from "./DeployNodeConfigFormBaiduCloudBLBConfig"; import DeployNodeConfigFormBaiduCloudCDNConfig from "./DeployNodeConfigFormBaiduCloudCDNConfig"; import DeployNodeConfigFormBaishanCDNConfig from "./DeployNodeConfigFormBaishanCDNConfig"; @@ -186,6 +187,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.AZURE_KEYVAULT: return ; + case DEPLOY_PROVIDERS.BAIDUCLOUD_APPBLB: + return ; case DEPLOY_PROVIDERS.BAIDUCLOUD_BLB: return ; case DEPLOY_PROVIDERS.BAIDUCLOUD_CDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx new file mode 100644 index 00000000..157b9a27 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormBaiduCloudAppBLBConfig.tsx @@ -0,0 +1,142 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; +import { validDomainName, validPortNumber } from "@/utils/validators"; + +type DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues = Nullish<{ + resourceType: string; + region: string; + loadbalancerId?: string; + listenerPort?: string | number; + domain?: string; +}>; + +export type DeployNodeConfigFormBaiduCloudAppBLBConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues) => void; +}; + +const RESOURCE_TYPE_LOADBALANCER = "loadbalancer" as const; +const RESOURCE_TYPE_LISTENER = "listener" as const; + +const initFormModel = (): DeployNodeConfigFormBaiduCloudAppBLBConfigFieldValues => { + return { + listenerPort: 443, + }; +}; + +const DeployNodeConfigFormBaiduCloudAppBLBConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormBaiduCloudAppBLBConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.union([z.literal(RESOURCE_TYPE_LOADBALANCER), z.literal(RESOURCE_TYPE_LISTENER)], { + message: t("workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder"), + }), + region: z + .string({ message: t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder") }) + .nonempty(t("workflow_node.deploy.form.baiducloud_appblb_region.placeholder")) + .trim(), + loadbalancerId: z + .string({ message: t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder") }) + .min(1, t("workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + listenerPort: z + .union([z.number(), z.string()]) + .refine( + (v) => fieldResourceType === RESOURCE_TYPE_LISTENER && validPortNumber(v), + t("workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder") + ) + .nullish(), + domain: z + .string() + .nullish() + .refine((v) => { + if (![RESOURCE_TYPE_LOADBALANCER, RESOURCE_TYPE_LISTENER].includes(fieldResourceType)) return true; + return !v || validDomainName(v!, { allowWildcard: true }); + }, t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const fieldResourceType = Form.useWatch("resourceType", formInst); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + } + > + + + + + } + > + + + + + + } + > + + + +
+ ); +}; + +export default DeployNodeConfigFormBaiduCloudAppBLBConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 37c4bc00..088a2d62 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -249,6 +249,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({ AWS_ACM: `${ACCESS_PROVIDERS.AWS}-acm`, AWS_CLOUDFRONT: `${ACCESS_PROVIDERS.AWS}-cloudfront`, AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`, + BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`, BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`, BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`, BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`, @@ -362,6 +363,7 @@ export const deployProvidersMap: Maphttps://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.label": "Resource type", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder": "Please select resource type", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label": "BLB load balancer", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label": "BLB listener", + "workflow_node.deploy.form.baiducloud_appblb_region.label": "Baidu Cloud BLB region", + "workflow_node.deploy.form.baiducloud_appblb_region.placeholder": "Please enter Baidu Cloud BLB region (e.g. bj)", + "workflow_node.deploy.form.baiducloud_appblb_region.tooltip": "For more information, see https://cloud.baidu.com/doc/BLB/s/cjwvxnzix", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label": "Baidu Cloud BLB load balancer ID", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder": "Please enter Baidu Cloud BLB load balancer ID", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/appblb/list", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.label": "Baidu Cloud BLB listener port", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder": "Please enter Baidu Cloud BLB listener port", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/appblb/list", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.label": "Baidu Cloud BLB SNI domain (Optional)", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder": "Please enter Baidu Cloud BLB SNI domain name", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.tooltip": "For more information, see https://console.bce.baidu.com/blb/#/appblb/list", "workflow_node.deploy.form.baiducloud_blb_resource_type.label": "Resource type", "workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "BLB load balancer", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index acee7a7b..ae30c2a6 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -28,6 +28,7 @@ "provider.azure.dns": "Azure - DNS", "provider.azure.keyvault": "Azure - KeyVault", "provider.baiducloud": "百度智能云", + "provider.baiducloud.appblb": "百度智能云 - 应用型负载均衡 BLB", "provider.baiducloud.blb": "百度智能云 - 普通型负载均衡 BLB", "provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN", "provider.baiducloud.dns": "百度智能云 - 智能云解析 DNS", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 05868ff3..715efd41 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -222,6 +222,22 @@ "workflow_node.deploy.form.azure_keyvault_name.label": "Azure KeyVault 名称", "workflow_node.deploy.form.azure_keyvault_name.placeholder": "请输入 Azure KeyVault 名称", "workflow_node.deploy.form.azure_keyvault_name.tooltip": "这是什么?请参阅 https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.label": "证书替换方式", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.placeholder": "请选择证书替换方式", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/SSL 监听的证书", + "workflow_node.deploy.form.baiducloud_appblb_resource_type.option.listener.label": "替换指定负载均衡监听的证书", + "workflow_node.deploy.form.baiducloud_appblb_region.label": "百度智能云 BLB 服务地域", + "workflow_node.deploy.form.baiducloud_appblb_region.placeholder": "请输入百度智能云 BLB 服务地域(例如:bj)", + "workflow_node.deploy.form.baiducloud_appblb_region.tooltip": "这是什么?请参阅 https://cloud.baidu.com/doc/BLB/s/cjwvxnzix", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.label": "百度智能云 BLB 负载均衡器 ID", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.placeholder": "请输入百度智能云 BLB 负载均衡器 ID", + "workflow_node.deploy.form.baiducloud_appblb_loadbalancer_id.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/appblb/list", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.label": "百度智能云 BLB 监听端口", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.placeholder": "请输入百度智能云 BLB 监听端口", + "workflow_node.deploy.form.baiducloud_appblb_listener_port.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/appblb/list", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.label": "百度智能云 BLB 扩展域名(可选)", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.placeholder": "请输入百度智能云 BLB 扩展域名(支持泛域名)", + "workflow_node.deploy.form.baiducloud_appblb_snidomain.tooltip": "这是什么?请参阅 https://console.bce.baidu.com/blb/#/appblb/list

不填写时,将替换监听器的默认证书。", "workflow_node.deploy.form.baiducloud_blb_resource_type.label": "证书替换方式", "workflow_node.deploy.form.baiducloud_blb_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.baiducloud_blb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS/SSL 监听的证书", From 4d710a1aaf86fd119363c13a0518e99522b27688 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 13:30:24 +0800 Subject: [PATCH 04/14] feat: add baiducloud cert deployer --- internal/deployer/providers.go | 10 ++- internal/domain/provider.go | 1 + .../baiducloud-appblb/baiducloud_appblb.go | 2 +- .../baiducloud-blb/baiducloud_blb.go | 2 +- .../baiducloud-cert/baiducloud_cert.go | 68 +++++++++++++++++++ .../baiducloud_cert.go} | 4 +- .../baiducloud_cert_test.go} | 4 +- .../baiducloud-sdk/{cas => cert}/api.go | 0 .../baiducloud-sdk/{cas => cert}/client.go | 0 .../baiducloud-sdk/{cas => cert}/models.go | 0 ui/src/domain/provider.ts | 8 ++- ui/src/i18n/locales/en/nls.provider.json | 9 +-- ui/src/i18n/locales/zh/nls.provider.json | 5 +- 13 files changed, 97 insertions(+), 16 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go rename internal/pkg/core/uploader/providers/{baiducloud-cas/baiducloud_cas.go => baiducloud-cert/baiducloud_cert.go} (99%) rename internal/pkg/core/uploader/providers/{baiducloud-cas/baiducloud_cas_test.go => baiducloud-cert/baiducloud_cert_test.go} (96%) rename internal/pkg/vendors/baiducloud-sdk/{cas => cert}/api.go (100%) rename internal/pkg/vendors/baiducloud-sdk/{cas => cert}/client.go (100%) rename internal/pkg/vendors/baiducloud-sdk/{cas => cert}/models.go (100%) diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 39b8a49f..1984e5e5 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -27,6 +27,7 @@ import ( pBaiduCloudAppBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-appblb" pBaiduCloudBLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-blb" pBaiduCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cdn" + pBaiduCloudCert "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baiducloud-cert" pBaishanCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baishan-cdn" pBaotaPanelConsole "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-console" pBaotaPanelSite "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/baotapanel-site" @@ -305,7 +306,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeBaiduCloudAppBLB, domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN: + case domain.DeployProviderTypeBaiduCloudAppBLB, domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN, domain.DeployProviderTypeBaiduCloudCert: { access := domain.AccessConfigForBaiduCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -345,6 +346,13 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { }) return deployer, err + case domain.DeployProviderTypeBaiduCloudCert: + deployer, err := pBaiduCloudCert.NewDeployer(&pBaiduCloudCert.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + }) + return deployer, err + default: break } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 79ee6709..cce395fd 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -138,6 +138,7 @@ const ( DeployProviderTypeBaiduCloudAppBLB = DeployProviderType("baiducloud-appblb") DeployProviderTypeBaiduCloudBLB = DeployProviderType("baiducloud-blb") DeployProviderTypeBaiduCloudCDN = DeployProviderType("baiducloud-cdn") + DeployProviderTypeBaiduCloudCert = DeployProviderType("baiducloud-cert") DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn") DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console") DeployProviderTypeBaotaPanelSite = DeployProviderType("baotapanel-site") diff --git a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go index c470bfcd..7a5f7ef2 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go @@ -14,7 +14,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" ) diff --git a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go index c6b19da6..6be94e6d 100644 --- a/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -14,7 +14,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" - uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" ) diff --git a/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go new file mode 100644 index 00000000..d51018f0 --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-cert/baiducloud_cert.go @@ -0,0 +1,68 @@ +package baiducloudcert + +import ( + "context" + "log/slog" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" +) + +type DeployerConfig struct { + // 百度智能云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 百度智能云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 CAS + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + return &deployer.DeployResult{}, nil +} diff --git a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go similarity index 99% rename from internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go rename to internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go index 9c0167a8..1f311f87 100644 --- a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas.go +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go @@ -1,4 +1,4 @@ -package baiducloudcas +package baiducloudcert import ( "context" @@ -11,7 +11,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/certutil" - bdsdk "github.com/usual2970/certimate/internal/pkg/vendors/baiducloud-sdk/cas" + bdsdk "github.com/usual2970/certimate/internal/pkg/vendors/baiducloud-sdk/cert" ) type UploaderConfig struct { diff --git a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert_test.go similarity index 96% rename from internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go rename to internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert_test.go index d727f71f..55de1576 100644 --- a/internal/pkg/core/uploader/providers/baiducloud-cas/baiducloud_cas_test.go +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert_test.go @@ -1,4 +1,4 @@ -package baiducloudcas_test +package baiducloudcert_test import ( "context" @@ -9,7 +9,7 @@ import ( "strings" "testing" - provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cas" + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" ) var ( diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/api.go b/internal/pkg/vendors/baiducloud-sdk/cert/api.go similarity index 100% rename from internal/pkg/vendors/baiducloud-sdk/cas/api.go rename to internal/pkg/vendors/baiducloud-sdk/cert/api.go diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/client.go b/internal/pkg/vendors/baiducloud-sdk/cert/client.go similarity index 100% rename from internal/pkg/vendors/baiducloud-sdk/cas/client.go rename to internal/pkg/vendors/baiducloud-sdk/cert/client.go diff --git a/internal/pkg/vendors/baiducloud-sdk/cas/models.go b/internal/pkg/vendors/baiducloud-sdk/cert/models.go similarity index 100% rename from internal/pkg/vendors/baiducloud-sdk/cas/models.go rename to internal/pkg/vendors/baiducloud-sdk/cert/models.go diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 088a2d62..583c5118 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -252,6 +252,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({ BAIDUCLOUD_APPBLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-appblb`, BAIDUCLOUD_BLB: `${ACCESS_PROVIDERS.BAIDUCLOUD}-blb`, BAIDUCLOUD_CDN: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cdn`, + BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`, BAISHAN_CDN: `${ACCESS_PROVIDERS.BAISHAN}-cdn`, BAOTAPANEL_CONSOLE: `${ACCESS_PROVIDERS.BAOTAPANEL}-console`, BAOTAPANEL_SITE: `${ACCESS_PROVIDERS.BAOTAPANEL}-site`, @@ -358,12 +359,13 @@ export const deployProvidersMap: Map Date: Tue, 25 Mar 2025 13:34:24 +0800 Subject: [PATCH 05/14] feat: add huaweicloud scm deployer --- internal/deployer/providers.go | 10 ++- internal/domain/provider.go | 1 + .../huaweicloud-scm/huaweicloud_scm.go | 69 +++++++++++++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + ui/src/i18n/locales/zh/nls.provider.json | 1 + 6 files changed, 83 insertions(+), 1 deletion(-) create mode 100644 internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 1984e5e5..c3578332 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -39,6 +39,7 @@ import ( pGcoreCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/gcore-cdn" pHuaweiCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-cdn" pHuaweiCloudELB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-elb" + pHuaweiCloudSCM "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-scm" pHuaweiCloudWAF "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/huaweicloud-waf" pJDCloudALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-alb" pJDCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/jdcloud-cdn" @@ -513,7 +514,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB, domain.DeployProviderTypeHuaweiCloudWAF: + case domain.DeployProviderTypeHuaweiCloudCDN, domain.DeployProviderTypeHuaweiCloudELB, domain.DeployProviderTypeHuaweiCloudSCM, domain.DeployProviderTypeHuaweiCloudWAF: { access := domain.AccessConfigForHuaweiCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -542,6 +543,13 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { }) return deployer, err + case domain.DeployProviderTypeHuaweiCloudSCM: + deployer, err := pHuaweiCloudSCM.NewDeployer(&pHuaweiCloudSCM.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + }) + return deployer, err + case domain.DeployProviderTypeHuaweiCloudWAF: deployer, err := pHuaweiCloudWAF.NewDeployer(&pHuaweiCloudWAF.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index cce395fd..45326aac 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -150,6 +150,7 @@ const ( DeployProviderTypeGcoreCDN = DeployProviderType("gcore-cdn") DeployProviderTypeHuaweiCloudCDN = DeployProviderType("huaweicloud-cdn") DeployProviderTypeHuaweiCloudELB = DeployProviderType("huaweicloud-elb") + DeployProviderTypeHuaweiCloudSCM = DeployProviderType("huaweicloud-scm") DeployProviderTypeHuaweiCloudWAF = DeployProviderType("huaweicloud-waf") DeployProviderTypeJDCloudALB = DeployProviderType("jdcloud-alb") DeployProviderTypeJDCloudCDN = DeployProviderType("jdcloud-cdn") diff --git a/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go new file mode 100644 index 00000000..69b75f2f --- /dev/null +++ b/internal/pkg/core/deployer/providers/huaweicloud-scm/huaweicloud_scm.go @@ -0,0 +1,69 @@ +package huaweicloudscm + +import ( + "context" + "log/slog" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/huaweicloud-scm" +) + +type DeployerConfig struct { + // 华为云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 华为云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + d.sslUploader.WithLogger(logger) + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 SCM + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + return &deployer.DeployResult{}, nil +} diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 583c5118..301d8883 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -264,6 +264,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({ GCORE_CDN: `${ACCESS_PROVIDERS.GCORE}-cdn`, HUAWEICLOUD_CDN: `${ACCESS_PROVIDERS.HUAWEICLOUD}-cdn`, HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`, + HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`, HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`, JDCLOUD_ALB: `${ACCESS_PROVIDERS.JDCLOUD}-alb`, JDCLOUD_CDN: `${ACCESS_PROVIDERS.JDCLOUD}-cdn`, @@ -366,6 +367,7 @@ export const deployProvidersMap: Map Date: Tue, 25 Mar 2025 13:44:41 +0800 Subject: [PATCH 06/14] feat: add volcengine certcenter deployer --- internal/deployer/providers.go | 11 ++- internal/domain/provider.go | 1 + .../volcengine_certcenter.go | 72 +++++++++++++++++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + ...deConfigFormVolcEngineCertCenterConfig.tsx | 59 +++++++++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 8 ++- ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 8 ++- 10 files changed, 159 insertions(+), 7 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index c3578332..61d14613 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -67,6 +67,7 @@ import ( pUpyunCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/upyun-cdn" pVolcEngineALB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-alb" pVolcEngineCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-cdn" + pVolcEngineCertCenter "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-certcenter" pVolcEngineCLB "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-clb" pVolcEngineDCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-dcdn" pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex" @@ -891,7 +892,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeVolcEngineALB, domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineImageX, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS: + case domain.DeployProviderTypeVolcEngineALB, domain.DeployProviderTypeVolcEngineCDN, domain.DeployProviderTypeVolcEngineCertCenter, domain.DeployProviderTypeVolcEngineCLB, domain.DeployProviderTypeVolcEngineDCDN, domain.DeployProviderTypeVolcEngineImageX, domain.DeployProviderTypeVolcEngineLive, domain.DeployProviderTypeVolcEngineTOS: { access := domain.AccessConfigForVolcEngine{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -919,6 +920,14 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { }) return deployer, err + case domain.DeployProviderTypeVolcEngineCertCenter: + deployer, err := pVolcEngineCertCenter.NewDeployer(&pVolcEngineCertCenter.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.SecretAccessKey, + Region: maputil.GetString(options.ProviderDeployConfig, "region"), + }) + return deployer, err + case domain.DeployProviderTypeVolcEngineCLB: deployer, err := pVolcEngineCLB.NewDeployer(&pVolcEngineCLB.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 45326aac..029405d9 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -180,6 +180,7 @@ const ( DeployProviderTypeUpyunFile = DeployProviderType("upyun-file") DeployProviderTypeVolcEngineALB = DeployProviderType("volcengine-alb") DeployProviderTypeVolcEngineCDN = DeployProviderType("volcengine-cdn") + DeployProviderTypeVolcEngineCertCenter = DeployProviderType("volcengine-certcenter") DeployProviderTypeVolcEngineCLB = DeployProviderType("volcengine-clb") DeployProviderTypeVolcEngineDCDN = DeployProviderType("volcengine-dcdn") DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex") diff --git a/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go new file mode 100644 index 00000000..a8062641 --- /dev/null +++ b/internal/pkg/core/deployer/providers/volcengine-certcenter/volcengine_certcenter.go @@ -0,0 +1,72 @@ +package volcenginecertcenter + +import ( + "context" + "log/slog" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/volcengine-certcenter" +) + +type DeployerConfig struct { + // 火山引擎 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 火山引擎 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` + // 火山引擎地域。 + Region string `json:"region"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + Region: config.Region, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + d.sslUploader.WithLogger(logger) + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到证书中心 + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + return &deployer.DeployResult{}, nil +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index f1f6c6a0..8941432f 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -74,6 +74,7 @@ import DeployNodeConfigFormUpyunCDNConfig from "./DeployNodeConfigFormUpyunCDNCo import DeployNodeConfigFormUpyunFileConfig from "./DeployNodeConfigFormUpyunFileConfig.tsx"; import DeployNodeConfigFormVolcEngineALBConfig from "./DeployNodeConfigFormVolcEngineALBConfig.tsx"; import DeployNodeConfigFormVolcEngineCDNConfig from "./DeployNodeConfigFormVolcEngineCDNConfig.tsx"; +import DeployNodeConfigFormVolcEngineCertCenterConfig from "./DeployNodeConfigFormVolcEngineCertCenterConfig.tsx"; import DeployNodeConfigFormVolcEngineCLBConfig from "./DeployNodeConfigFormVolcEngineCLBConfig.tsx"; import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolcEngineDCDNConfig.tsx"; import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx"; @@ -269,6 +270,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.VOLCENGINE_CDN: return ; + case DEPLOY_PROVIDERS.VOLCENGINE_CERTCENTER: + return ; case DEPLOY_PROVIDERS.VOLCENGINE_CLB: return ; case DEPLOY_PROVIDERS.VOLCENGINE_DCDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx new file mode 100644 index 00000000..21f4557f --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCertCenterConfig.tsx @@ -0,0 +1,59 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +type DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues = Nullish<{ + region: string; +}>; + +export type DeployNodeConfigFormVolcEngineCertCenterConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormVolcEngineCertCenterConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormVolcEngineCertCenterConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormVolcEngineCertCenterConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + region: z + .string({ message: t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder") }) + .nonempty(t("workflow_node.deploy.form.volcengine_certcenter_region.placeholder")) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + +
+ ); +}; + +export default DeployNodeConfigFormVolcEngineCertCenterConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 301d8883..950c72cb 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -294,6 +294,7 @@ export const DEPLOY_PROVIDERS = Object.freeze({ UPYUN_FILE: `${ACCESS_PROVIDERS.UPYUN}-file`, VOLCENGINE_ALB: `${ACCESS_PROVIDERS.VOLCENGINE}-alb`, VOLCENGINE_CDN: `${ACCESS_PROVIDERS.VOLCENGINE}-cdn`, + VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`, VOLCENGINE_CLB: `${ACCESS_PROVIDERS.VOLCENGINE}-clb`, VOLCENGINE_DCDN: `${ACCESS_PROVIDERS.VOLCENGINE}-dcdn`, VOLCENGINE_IMAGEX: `${ACCESS_PROVIDERS.VOLCENGINE}-imagex`, @@ -375,6 +376,7 @@ export const deployProvidersMap: Maphttps://console.volcengine.com/alb", - "workflow_node.deploy.form.volcengine_clb_snidomain.label": "VolcEngine CLB SNI domain (Optional)", - "workflow_node.deploy.form.volcengine_clb_snidomain.placeholder": "Please enter VolcEngine CLB SNI domain name", - "workflow_node.deploy.form.volcengine_clb_snidomain.tooltip": "For more information, see https://console.volcengine.com/alb", + "workflow_node.deploy.form.volcengine_alb_snidomain.label": "VolcEngine CLB SNI domain (Optional)", + "workflow_node.deploy.form.volcengine_alb_snidomain.placeholder": "Please enter VolcEngine CLB SNI domain name", + "workflow_node.deploy.form.volcengine_alb_snidomain.tooltip": "For more information, see https://console.volcengine.com/alb", "workflow_node.deploy.form.volcengine_cdn_domain.label": "VolcEngine CDN domain", "workflow_node.deploy.form.volcengine_cdn_domain.placeholder": "Please enter VolcEngine CDN domain name", "workflow_node.deploy.form.volcengine_cdn_domain.tooltip": "For more information, see https://console.volcengine.com/cdn/homepage", + "workflow_node.deploy.form.volcengine_certcenter_region.label": "VolcEngine Certificate Center region", + "workflow_node.deploy.form.volcengine_certcenter_region.placeholder": "Please enter VolcEngine Certificate Center region (e.g. cn-beijing)", "workflow_node.deploy.form.volcengine_clb_resource_type.label": "Resource type", "workflow_node.deploy.form.volcengine_clb_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label": "CLB load balancer", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index fcd924f1..62806a9d 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -112,6 +112,7 @@ "provider.volcengine": "火山引擎", "provider.volcengine.alb": "火山引擎 - 应用型负载均衡 ALB", "provider.volcengine.cdn": "火山引擎 - 内容分发网络 CDN", + "provider.volcengine.certcenter": "火山引擎 - 上传到证书中心", "provider.volcengine.clb": "火山引擎 - 负载均衡 CLB", "provider.volcengine.dcdn": "火山引擎 - 全站加速 DCDN", "provider.volcengine.dns": "火山引擎 - 云解析 DNS", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 715efd41..b0fd81ad 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -573,12 +573,14 @@ "workflow_node.deploy.form.volcengine_alb_listener_id.label": "火山引擎 ALB 监听器 ID", "workflow_node.deploy.form.volcengine_alb_listener_id.placeholder": "请输入火山引擎 ALB 监听器 ID", "workflow_node.deploy.form.volcengine_alb_listener_id.tooltip": "这是什么?请参阅 https://console.volcengine.com/alb", - "workflow_node.deploy.form.volcengine_clb_snidomain.label": "火山引擎 ALB 扩展域名(可选)", - "workflow_node.deploy.form.volcengine_clb_snidomain.placeholder": "请输入火山引擎 ALB 扩展域名(支持泛域名)", - "workflow_node.deploy.form.volcengine_clb_snidomain.tooltip": "这是什么?请参阅 https://console.volcengine.com/alb

不填写时,将替换监听器的默认证书。", + "workflow_node.deploy.form.volcengine_alb_snidomain.label": "火山引擎 ALB 扩展域名(可选)", + "workflow_node.deploy.form.volcengine_alb_snidomain.placeholder": "请输入火山引擎 ALB 扩展域名(支持泛域名)", + "workflow_node.deploy.form.volcengine_alb_snidomain.tooltip": "这是什么?请参阅 https://console.volcengine.com/alb

不填写时,将替换监听器的默认证书。", "workflow_node.deploy.form.volcengine_cdn_domain.label": "火山引擎 CDN 加速域名", "workflow_node.deploy.form.volcengine_cdn_domain.placeholder": "请输入火山引擎 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.volcengine_cdn_domain.tooltip": "这是什么?请参阅 https://console.volcengine.com/cdn/homepage", + "workflow_node.deploy.form.volcengine_certcenter_region.label": "火山引擎证书中心服务地域", + "workflow_node.deploy.form.volcengine_certcenter_region.placeholder": "请输入火山引擎证书中心服务地域(例如:cn-beijing)", "workflow_node.deploy.form.volcengine_clb_resource_type.label": "证书替换方式", "workflow_node.deploy.form.volcengine_clb_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.volcengine_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器下的全部 HTTPS 监听的证书", From 7d5c714211df5542a718f49b6d7fc7ae4d3268b7 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 13:52:02 +0800 Subject: [PATCH 07/14] feat: improve i18n --- README.md | 4 ++-- README_EN.md | 2 +- .../i18n/locales/en/nls.workflow.nodes.json | 18 +++++++------- .../i18n/locales/zh/nls.workflow.nodes.json | 24 +++++++++---------- 4 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index dbad9bdb..7daffaaf 100644 --- a/README.md +++ b/README.md @@ -38,8 +38,8 @@ Certimate 旨在为用户提供一个安全、简便的 SSL 证书管理解决 - 灵活的工作流编排方式,证书从申请到部署完全自动化; - 支持单域名、多域名、泛域名证书,可选 RSA、ECC 签名算法; - 支持 PEM、PFX、JKS 等多种格式输出证书; -- 支持 20+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)完整提供商清单); -- 支持 60+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看](https://docs.certimate.me/docs/reference/providers#supported-host-providers)完整提供商清单); +- 支持 20+ 域名托管商(如阿里云、腾讯云、Cloudflare 等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-dns-providers)); +- 支持 70+ 部署目标(如 Kubernetes、CDN、WAF、负载均衡等,[点此查看完整清单](https://docs.certimate.me/docs/reference/providers#supported-host-providers)); - 支持邮件、钉钉、飞书、企业微信、Webhook 等多种通知渠道; - 支持 Let's Encrypt、ZeroSSL、Google Trust Services 等多种 ACME 证书颁发机构; - 更多特性等待探索。 diff --git a/README_EN.md b/README_EN.md index 71472e08..3b0506c8 100644 --- a/README_EN.md +++ b/README_EN.md @@ -39,7 +39,7 @@ Certimate aims to provide users with a secure and user-friendly SSL certificate - Supports single-domain, multi-domain, wildcard certificates, with options for RSA or ECC. - Supports various certificate formats such as PEM, PFX, JKS. - Supports more than 20+ domain registrars (e.g., Alibaba Cloud, Tencent Cloud, Cloudflare, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-dns-providers)); -- Supports more than 60+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers)); +- Supports more than 70+ deployment targets (e.g., Kubernetes, CDN, WAF, load balancers, etc. [Check out this link](https://docs.certimate.me/en/docs/reference/providers#supported-host-providers)); - Supports multiple notification channels including email, DingTalk, Feishu, WeCom, Webhook, and more; - Supports multiple ACME CAs including Let's Encrypt, ZeroSSL, Google Trust Services, and more; - More features waiting to be discovered. diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index a57490e1..2135dc72 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -630,15 +630,6 @@ "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "skip", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "not skip", - "workflow_node.notify.label": "Notification", - "workflow_node.notify.form.subject.label": "Subject", - "workflow_node.notify.form.subject.placeholder": "Please enter subject", - "workflow_node.notify.form.message.label": "Message", - "workflow_node.notify.form.message.placeholder": "Please enter message", - "workflow_node.notify.form.channel.label": "Channel", - "workflow_node.notify.form.channel.placeholder": "Please select channel", - "workflow_node.notify.form.channel.button": "Configure", - "workflow_node.upload.label": "Upload", "workflow_node.upload.form.domains.label": "Domains", "workflow_node.upload.form.domains.placholder": "Please select certificate file", @@ -649,6 +640,15 @@ "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", "workflow_node.upload.form.private_key.button": "Choose file ...", + "workflow_node.notify.label": "Notification", + "workflow_node.notify.form.subject.label": "Subject", + "workflow_node.notify.form.subject.placeholder": "Please enter subject", + "workflow_node.notify.form.message.label": "Message", + "workflow_node.notify.form.message.placeholder": "Please enter message", + "workflow_node.notify.form.channel.label": "Channel", + "workflow_node.notify.form.channel.placeholder": "Please select channel", + "workflow_node.notify.form.channel.button": "Configure", + "workflow_node.end.label": "End", "workflow_node.branch.label": "Parallel branch", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index b0fd81ad..4fc41e8d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -22,7 +22,7 @@ "workflow_node.start.form.trigger_cron.extra": "预计最近 5 次执行时间:", "workflow_node.start.form.trigger_cron.guide": "小贴士:如果你有多个工作流,建议将它们设置为在一天中的多个时间段运行,而非总是在相同的特定时间。也不要总是设置为每日零时,以免遭遇证书颁发机构的流量高峰。

参考链接:
1. Let’s Encrypt 速率限制
2. 为什么我的 Let’s Encrypt (ACME) 客户端启动时间应当随机?", - "workflow_node.apply.label": "申请", + "workflow_node.apply.label": "申请证书", "workflow_node.apply.form.domains.label": "域名", "workflow_node.apply.form.domains.placeholder": "请输入域名(多个值请用半角分号隔开)", "workflow_node.apply.form.domains.tooltip": "泛域名表示形式为:*.example.com", @@ -77,7 +77,7 @@ "workflow_node.apply.form.skip_before_expiry_days.unit": "天", "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过 CA 的证书有效期限制,否则证书可能永远不会续期。", - "workflow_node.deploy.label": "部署", + "workflow_node.deploy.label": "部署证书", "workflow_node.deploy.search.provider.placeholder": "搜索部署目标……", "workflow_node.deploy.form.provider.label": "部署目标", "workflow_node.deploy.form.provider.placeholder": "请选择部署目标", @@ -630,16 +630,7 @@ "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过", - "workflow_node.notify.label": "通知", - "workflow_node.notify.form.subject.label": "通知主题", - "workflow_node.notify.form.subject.placeholder": "请输入通知主题", - "workflow_node.notify.form.message.label": "通知内容", - "workflow_node.notify.form.message.placeholder": "请输入通知内容", - "workflow_node.notify.form.channel.label": "通知渠道", - "workflow_node.notify.form.channel.placeholder": "请选择通知渠道", - "workflow_node.notify.form.channel.button": "去配置", - - "workflow_node.upload.label": "上传", + "workflow_node.upload.label": "上传证书", "workflow_node.upload.form.domains.label": "域名", "workflow_node.upload.form.domains.placeholder": "上传证书文件后显示", "workflow_node.upload.form.certificate.label": "证书文件(PEM 格式)", @@ -649,6 +640,15 @@ "workflow_node.upload.form.private_key.placeholder": "-----BEGIN (RSA|EC) PRIVATE KEY-----...-----END(RSA|EC) PRIVATE KEY-----", "workflow_node.upload.form.private_key.button": "选择文件", + "workflow_node.notify.label": "推送通知", + "workflow_node.notify.form.subject.label": "通知主题", + "workflow_node.notify.form.subject.placeholder": "请输入通知主题", + "workflow_node.notify.form.message.label": "通知内容", + "workflow_node.notify.form.message.placeholder": "请输入通知内容", + "workflow_node.notify.form.channel.label": "通知渠道", + "workflow_node.notify.form.channel.placeholder": "请选择通知渠道", + "workflow_node.notify.form.channel.button": "去配置", + "workflow_node.end.label": "结束", "workflow_node.branch.label": "并行分支", From e9610eaede73c0fe0642d5d4b02d06cd7f581fbd Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 16:17:35 +0800 Subject: [PATCH 08/14] fix: #556 --- .../providers/aliyun-waf/aliyun_waf.go | 186 ++++++++++++++++-- .../huaweicloud-cdn/huaweicloud_cdn.go | 30 +-- 2 files changed, 183 insertions(+), 33 deletions(-) diff --git a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go index 7ab5dda4..cc179b34 100644 --- a/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go +++ b/internal/pkg/core/deployer/providers/aliyun-waf/aliyun_waf.go @@ -15,6 +15,7 @@ import ( "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/core/uploader" uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/aliyun-cas" + "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" ) type DeployerConfig struct { @@ -156,26 +157,10 @@ func (d *DeployerProvider) deployToWAF3(ctx context.Context, certPem string, pri InstanceId: tea.String(d.config.InstanceId), RegionId: tea.String(d.config.Region), Domain: tea.String(d.config.Domain), - Listen: &aliwaf.ModifyDomainRequestListen{ - CertId: tea.String(upres.CertId), - TLSVersion: tea.String("tlsv1"), - EnableTLSv3: tea.Bool(false), - }, - Redirect: &aliwaf.ModifyDomainRequestRedirect{ - Loadbalance: tea.String("iphash"), - }, - } - if describeDomainDetailResp.Body != nil && describeDomainDetailResp.Body.Listen != nil { - modifyDomainReq.Listen.TLSVersion = describeDomainDetailResp.Body.Listen.TLSVersion - modifyDomainReq.Listen.EnableTLSv3 = describeDomainDetailResp.Body.Listen.EnableTLSv3 - modifyDomainReq.Listen.FocusHttps = describeDomainDetailResp.Body.Listen.FocusHttps - } - if describeDomainDetailResp.Body != nil && describeDomainDetailResp.Body.Redirect != nil { - modifyDomainReq.Redirect.Loadbalance = describeDomainDetailResp.Body.Redirect.Loadbalance - modifyDomainReq.Redirect.FocusHttpBackend = describeDomainDetailResp.Body.Redirect.FocusHttpBackend - modifyDomainReq.Redirect.SniEnabled = describeDomainDetailResp.Body.Redirect.SniEnabled - modifyDomainReq.Redirect.SniHost = describeDomainDetailResp.Body.Redirect.SniHost + Listen: &aliwaf.ModifyDomainRequestListen{CertId: tea.String(upres.CertId)}, + Redirect: &aliwaf.ModifyDomainRequestRedirect{Loadbalance: tea.String("iphash")}, } + modifyDomainReq = assign(modifyDomainReq, describeDomainDetailResp.Body) modifyDomainResp, err := d.sdkClient.ModifyDomain(modifyDomainReq) d.logger.Debug("sdk request 'waf.ModifyDomain'", slog.Any("request", modifyDomainReq), slog.Any("response", modifyDomainResp)) if err != nil { @@ -222,3 +207,166 @@ func createSslUploader(accessKeyId, accessKeySecret, region string) (uploader.Up }) return uploader, err } + +func assign(source *aliwaf.ModifyDomainRequest, target *aliwaf.DescribeDomainDetailResponseBody) *aliwaf.ModifyDomainRequest { + // `ModifyDomain` 中不传的字段表示使用默认值、而非保留原值, + // 因此这里需要把原配置中的参数重新赋值回去。 + + if target == nil { + return source + } + + if target.Listen != nil { + if source.Listen == nil { + source.Listen = &aliwaf.ModifyDomainRequestListen{} + } + + if target.Listen.CipherSuite != nil { + source.Listen.CipherSuite = tea.Int32(int32(*target.Listen.CipherSuite)) + } + + if target.Listen.CustomCiphers != nil { + source.Listen.CustomCiphers = target.Listen.CustomCiphers + } + + if target.Listen.EnableTLSv3 != nil { + source.Listen.EnableTLSv3 = target.Listen.EnableTLSv3 + } + + if target.Listen.ExclusiveIp != nil { + source.Listen.ExclusiveIp = target.Listen.ExclusiveIp + } + + if target.Listen.FocusHttps != nil { + source.Listen.FocusHttps = target.Listen.FocusHttps + } + + if target.Listen.Http2Enabled != nil { + source.Listen.Http2Enabled = target.Listen.Http2Enabled + } + + if target.Listen.HttpPorts != nil { + source.Listen.HttpPorts = sliceutil.Map(target.Listen.HttpPorts, func(v *int64) *int32 { + if v == nil { + return nil + } + return tea.Int32(int32(*v)) + }) + } + + if target.Listen.HttpsPorts != nil { + source.Listen.HttpsPorts = sliceutil.Map(target.Listen.HttpsPorts, func(v *int64) *int32 { + if v == nil { + return nil + } + return tea.Int32(int32(*v)) + }) + } + + if target.Listen.IPv6Enabled != nil { + source.Listen.IPv6Enabled = target.Listen.IPv6Enabled + } + + if target.Listen.ProtectionResource != nil { + source.Listen.ProtectionResource = target.Listen.ProtectionResource + } + + if target.Listen.TLSVersion != nil { + source.Listen.TLSVersion = target.Listen.TLSVersion + } + + if target.Listen.XffHeaderMode != nil { + source.Listen.XffHeaderMode = tea.Int32(int32(*target.Listen.XffHeaderMode)) + } + + if target.Listen.XffHeaders != nil { + source.Listen.XffHeaders = target.Listen.XffHeaders + } + } + + if target.Redirect != nil { + if source.Redirect == nil { + source.Redirect = &aliwaf.ModifyDomainRequestRedirect{} + } + + if target.Redirect.Backends != nil { + source.Redirect.Backends = sliceutil.Map(target.Redirect.Backends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackends) *string { + if v == nil { + return nil + } + return v.Backend + }) + } + + if target.Redirect.BackupBackends != nil { + source.Redirect.BackupBackends = sliceutil.Map(target.Redirect.BackupBackends, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectBackupBackends) *string { + if v == nil { + return nil + } + return v.Backend + }) + } + + if target.Redirect.ConnectTimeout != nil { + source.Redirect.ConnectTimeout = target.Redirect.ConnectTimeout + } + + if target.Redirect.FocusHttpBackend != nil { + source.Redirect.FocusHttpBackend = target.Redirect.FocusHttpBackend + } + + if target.Redirect.Keepalive != nil { + source.Redirect.Keepalive = target.Redirect.Keepalive + } + + if target.Redirect.KeepaliveRequests != nil { + source.Redirect.KeepaliveRequests = target.Redirect.KeepaliveRequests + } + + if target.Redirect.KeepaliveTimeout != nil { + source.Redirect.KeepaliveTimeout = target.Redirect.KeepaliveTimeout + } + + if target.Redirect.Loadbalance != nil { + source.Redirect.Loadbalance = target.Redirect.Loadbalance + } + + if target.Redirect.ReadTimeout != nil { + source.Redirect.ReadTimeout = target.Redirect.ReadTimeout + } + + if target.Redirect.RequestHeaders != nil { + source.Redirect.RequestHeaders = sliceutil.Map(target.Redirect.RequestHeaders, func(v *aliwaf.DescribeDomainDetailResponseBodyRedirectRequestHeaders) *aliwaf.ModifyDomainRequestRedirectRequestHeaders { + if v == nil { + return nil + } + return &aliwaf.ModifyDomainRequestRedirectRequestHeaders{ + Key: v.Key, + Value: v.Value, + } + }) + } + + if target.Redirect.Retry != nil { + source.Redirect.Retry = target.Redirect.Retry + } + + if target.Redirect.SniEnabled != nil { + source.Redirect.SniEnabled = target.Redirect.SniEnabled + } + + if target.Redirect.SniHost != nil { + source.Redirect.SniHost = target.Redirect.SniHost + } + + if target.Redirect.WriteTimeout != nil { + source.Redirect.WriteTimeout = target.Redirect.WriteTimeout + } + + if target.Redirect.XffProto != nil { + source.Redirect.XffProto = target.Redirect.XffProto + } + } + + return source +} diff --git a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go index 65d03490..048ccbd2 100644 --- a/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go +++ b/internal/pkg/core/deployer/providers/huaweicloud-cdn/huaweicloud_cdn.go @@ -150,38 +150,40 @@ func createSdkClient(accessKeyId, secretAccessKey, region string) (*hccdn.CdnCli return client, nil } -func assign(reqContent *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent, target *hccdnmodel.ConfigsGetBody) *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent { +func assign(source *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent, target *hccdnmodel.ConfigsGetBody) *hccdnmodel.UpdateDomainMultiCertificatesRequestBodyContent { + // `UpdateDomainMultiCertificates` 中不传的字段表示使用默认值、而非保留原值, + // 因此这里需要把原配置中的参数重新赋值回去。 + if target == nil { - return reqContent + return source } - // 华为云 API 中不传的字段表示使用默认值、而非保留原值,因此这里需要把原配置中的参数重新赋值回去。 - // 而且蛋疼的是查询接口返回的数据结构和更新接口传入的参数结构不一致,需要做很多转化。 - if *target.OriginProtocol == "follow" { - reqContent.AccessOriginWay = hwsdk.Int32Ptr(1) + source.AccessOriginWay = hwsdk.Int32Ptr(1) } else if *target.OriginProtocol == "http" { - reqContent.AccessOriginWay = hwsdk.Int32Ptr(2) + source.AccessOriginWay = hwsdk.Int32Ptr(2) } else if *target.OriginProtocol == "https" { - reqContent.AccessOriginWay = hwsdk.Int32Ptr(3) + source.AccessOriginWay = hwsdk.Int32Ptr(3) } if target.ForceRedirect != nil { - reqContent.ForceRedirectConfig = &hccdnmodel.ForceRedirect{} + if source.ForceRedirectConfig == nil { + source.ForceRedirectConfig = &hccdnmodel.ForceRedirect{} + } if target.ForceRedirect.Status == "on" { - reqContent.ForceRedirectConfig.Switch = 1 - reqContent.ForceRedirectConfig.RedirectType = target.ForceRedirect.Type + source.ForceRedirectConfig.Switch = 1 + source.ForceRedirectConfig.RedirectType = target.ForceRedirect.Type } else { - reqContent.ForceRedirectConfig.Switch = 0 + source.ForceRedirectConfig.Switch = 0 } } if target.Https != nil { if *target.Https.Http2Status == "on" { - reqContent.Http2 = hwsdk.Int32Ptr(1) + source.Http2 = hwsdk.Int32Ptr(1) } } - return reqContent + return source } From 51c1b193e52a352bc51ad7dcb35cad00c41fa1c8 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 19:41:55 +0800 Subject: [PATCH 09/14] fix: #559 --- .../providers/tencentcloud-css/tencentcloud_css.go | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go index e404ac29..e29e25ea 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-css/tencentcloud_css.go @@ -79,15 +79,14 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe // 绑定证书对应的播放域名 // REF: https://cloud.tencent.com/document/product/267/78655 - modifyLiveDomainCertBindingsReq := &tclive.ModifyLiveDomainCertBindingsRequest{ - DomainInfos: []*tclive.LiveCertDomainInfo{ - { - DomainName: common.StringPtr(d.config.Domain), - Status: common.Int64Ptr(1), - }, + modifyLiveDomainCertBindingsReq := tclive.NewModifyLiveDomainCertBindingsRequest() + modifyLiveDomainCertBindingsReq.DomainInfos = []*tclive.LiveCertDomainInfo{ + { + DomainName: common.StringPtr(d.config.Domain), + Status: common.Int64Ptr(1), }, - CloudCertId: common.StringPtr(upres.CertId), } + modifyLiveDomainCertBindingsReq.CloudCertId = common.StringPtr(upres.CertId) modifyLiveDomainCertBindingsResp, err := d.sdkClient.ModifyLiveDomainCertBindings(modifyLiveDomainCertBindingsReq) d.logger.Debug("sdk request 'live.ModifyLiveDomainCertBindings'", slog.Any("request", modifyLiveDomainCertBindingsReq), slog.Any("response", modifyLiveDomainCertBindingsResp)) if err != nil { From fd8ac3ae375b4a76572e70e8732375b9bf188196 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 19:52:09 +0800 Subject: [PATCH 10/14] feat(ui): allow select dns-01 provider on application --- internal/pkg/core/uploader/uploader.go | 2 +- ui/src/components/access/AccessSelect.tsx | 4 +-- .../provider/AccessProviderSelect.tsx | 28 +++++++++++++------ .../provider/ApplyDNSProviderSelect.tsx | 28 +++++++++++++------ .../provider/DeployProviderSelect.tsx | 28 +++++++++++++------ .../workflow/node/DeployNodeConfigForm.tsx | 2 +- .../i18n/locales/en/nls.workflow.nodes.json | 4 ++- .../i18n/locales/zh/nls.workflow.nodes.json | 4 ++- 8 files changed, 67 insertions(+), 33 deletions(-) diff --git a/internal/pkg/core/uploader/uploader.go b/internal/pkg/core/uploader/uploader.go index 06cecec0..5edcdce4 100644 --- a/internal/pkg/core/uploader/uploader.go +++ b/internal/pkg/core/uploader/uploader.go @@ -27,6 +27,6 @@ type Uploader interface { // 表示证书上传结果的数据结构,包含上传后的证书 ID、名称和其他数据。 type UploadResult struct { CertId string `json:"certId"` - CertName string `json:"certName"` + CertName string `json:"certName,omitzero"` ExtendedData map[string]any `json:"extendedData,omitempty"` } diff --git a/ui/src/components/access/AccessSelect.tsx b/ui/src/components/access/AccessSelect.tsx index 3ad60348..7077e78d 100644 --- a/ui/src/components/access/AccessSelect.tsx +++ b/ui/src/components/access/AccessSelect.tsx @@ -21,9 +21,9 @@ const AccessSelect = ({ filter, ...props }: AccessTypeSelectProps) => { const [options, setOptions] = useState>([]); useEffect(() => { - const items = filter != null ? accesses.filter(filter) : accesses; + const filteredItems = filter != null ? accesses.filter(filter) : accesses; setOptions( - items.map((item) => ({ + filteredItems.map((item) => ({ key: item.id, value: item.id, label: item.name, diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index 97cd35fd..0e0a992c 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -1,22 +1,32 @@ -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, Select, type SelectProps, Space, Tag, Typography } from "antd"; -import { ACCESS_USAGES, accessProvidersMap } from "@/domain/provider"; +import { ACCESS_USAGES, type AccessProvider, accessProvidersMap } from "@/domain/provider"; export type AccessProviderSelectProps = Omit< SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" ->; +> & { + filter?: (record: AccessProvider) => boolean; +}; -const AccessProviderSelect = (props: AccessProviderSelectProps) => { +const AccessProviderSelect = ({ filter, ...props }: AccessProviderSelectProps) => { const { t } = useTranslation(); - const options = Array.from(accessProvidersMap.values()).map((item) => ({ - key: item.type, - value: item.type, - label: t(item.name), - })); + const [options, setOptions] = useState>([]); + useEffect(() => { + const allItems = Array.from(accessProvidersMap.values()); + const filteredItems = filter != null ? allItems.filter(filter) : allItems; + setOptions( + filteredItems.map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + data: item, + })) + ); + }, [filter]); const renderOption = (key: string) => { const provider = accessProvidersMap.get(key); diff --git a/ui/src/components/provider/ApplyDNSProviderSelect.tsx b/ui/src/components/provider/ApplyDNSProviderSelect.tsx index c88cfebf..f7e3c259 100644 --- a/ui/src/components/provider/ApplyDNSProviderSelect.tsx +++ b/ui/src/components/provider/ApplyDNSProviderSelect.tsx @@ -1,22 +1,32 @@ -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; -import { applyDNSProvidersMap } from "@/domain/provider"; +import { type ApplyDNSProvider, applyDNSProvidersMap } from "@/domain/provider"; export type ApplyDNSProviderSelectProps = Omit< SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" ->; +> & { + filter?: (record: ApplyDNSProvider) => boolean; +}; -const ApplyDNSProviderSelect = (props: ApplyDNSProviderSelectProps) => { +const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProps) => { const { t } = useTranslation(); - const options = Array.from(applyDNSProvidersMap.values()).map((item) => ({ - key: item.type, - value: item.type, - label: t(item.name), - })); + const [options, setOptions] = useState>([]); + useEffect(() => { + const allItems = Array.from(applyDNSProvidersMap.values()); + const filteredItems = filter != null ? allItems.filter(filter) : allItems; + setOptions( + filteredItems.map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + data: item, + })) + ); + }, [filter]); const renderOption = (key: string) => { const provider = applyDNSProvidersMap.get(key); diff --git a/ui/src/components/provider/DeployProviderSelect.tsx b/ui/src/components/provider/DeployProviderSelect.tsx index cc2b13ea..5be10cb7 100644 --- a/ui/src/components/provider/DeployProviderSelect.tsx +++ b/ui/src/components/provider/DeployProviderSelect.tsx @@ -1,22 +1,32 @@ -import { memo } from "react"; +import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; -import { deployProvidersMap } from "@/domain/provider"; +import { type DeployProvider, deployProvidersMap } from "@/domain/provider"; export type DeployProviderSelectProps = Omit< SelectProps, "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" ->; +> & { + filter?: (record: DeployProvider) => boolean; +}; -const DeployProviderSelect = (props: DeployProviderSelectProps) => { +const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) => { const { t } = useTranslation(); - const options = Array.from(deployProvidersMap.values()).map((item) => ({ - key: item.type, - value: item.type, - label: t(item.name), - })); + const [options, setOptions] = useState>([]); + useEffect(() => { + const allItems = Array.from(deployProvidersMap.values()); + const filteredItems = filter != null ? allItems.filter(filter) : allItems; + setOptions( + filteredItems.map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + data: item, + })) + ); + }, [filter]); const renderOption = (key: string) => { const provider = deployProvidersMap.get(key); diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 8941432f..948f59f2 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -400,7 +400,6 @@ const DeployNodeConfigForm = forwardRef { if (fieldProvider) { return deployProvidersMap.get(fieldProvider)?.provider === record.provider; @@ -409,6 +408,7 @@ const DeployNodeConfigForm = forwardRef diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 2135dc72..58ac67ee 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -31,6 +31,8 @@ "workflow_node.apply.form.contact_email.label": "Contact email", "workflow_node.apply.form.contact_email.placeholder": "Please enter contact email", "workflow_node.apply.form.contact_email.tooltip": "Contact information required for SSL certificate application. Please pay attention to the rate limits.", + "workflow_node.apply.form.challenge_type.label": "Challenge type", + "workflow_node.apply.form.challenge_type.placeholder": "Please select challenge type", "workflow_node.apply.form.provider.label": "DNS provider", "workflow_node.apply.form.provider.placeholder": "Please select DNS provider of the domains", "workflow_node.apply.form.provider_access.label": "DNS provider authorization", @@ -75,7 +77,7 @@ "workflow_node.apply.form.skip_before_expiry_days.prefix": "If the certificate expiration time exceeds", "workflow_node.apply.form.skip_before_expiry_days.suffix": ", skip to re-apply.", "workflow_node.apply.form.skip_before_expiry_days.unit": "days", - "workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the CA, otherwise the certificate may never be renewed.", + "workflow_node.apply.form.skip_before_expiry_days.tooltip": "Be careful not to exceed the validity period limit of the issued certificate, otherwise the certificate may never be renewed.", "workflow_node.deploy.label": "Deployment", "workflow_node.deploy.search.provider.placeholder": "Search deploy target ...", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 4fc41e8d..0207a4bf 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -31,6 +31,8 @@ "workflow_node.apply.form.contact_email.label": "联系邮箱", "workflow_node.apply.form.contact_email.placeholder": "请输入联系邮箱", "workflow_node.apply.form.contact_email.tooltip": "申请签发 SSL 证书时所需的联系方式。请注意 Let's Encrypt 账户注册的速率限制。点此了解更多。", + "workflow_node.apply.form.challenge_type.label": "质询方式", + "workflow_node.apply.form.challenge_type.placeholder": "请选择质询方式", "workflow_node.apply.form.provider.label": "DNS 提供商", "workflow_node.apply.form.provider.placeholder": "请选择 DNS 提供商", "workflow_node.apply.form.provider_access.label": "DNS 提供商授权", @@ -75,7 +77,7 @@ "workflow_node.apply.form.skip_before_expiry_days.prefix": "当上次签发的证书距到期时间超过", "workflow_node.apply.form.skip_before_expiry_days.suffix": "时,跳过重新申请。", "workflow_node.apply.form.skip_before_expiry_days.unit": "天", - "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过 CA 的证书有效期限制,否则证书可能永远不会续期。", + "workflow_node.apply.form.skip_before_expiry_days.tooltip": "注意不要超过颁发的证书最大有效期,否则证书可能永远不会续期。", "workflow_node.deploy.label": "部署证书", "workflow_node.deploy.search.provider.placeholder": "搜索部署目标……", From fa63f2a838b9b3eddc4af89011a4d456c85f9f0a Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Tue, 25 Mar 2025 20:27:16 +0800 Subject: [PATCH 11/14] feat: add tencentcloud-eo dns-01 applicant --- internal/applicant/providers.go | 35 ++- internal/domain/provider.go | 1 + .../lego-providers/cmcccloud/internal/lego.go | 1 + .../tencentcloud-eo/internal/lego.go | 207 ++++++++++++++++++ .../tencentcloud-eo/tencentcloud_eo.go | 41 ++++ .../workflow/node-processor/apply_node.go | 2 +- .../workflow/node-processor/deploy_node.go | 2 +- .../workflow/node-processor/upload_node.go | 2 +- .../workflow/node/ApplyNodeConfigForm.tsx | 40 +++- ...pplyNodeConfigFormTencentCloudEOConfig.tsx | 64 ++++++ ui/src/domain/provider.ts | 4 +- .../i18n/locales/en/nls.workflow.nodes.json | 3 + .../i18n/locales/zh/nls.workflow.nodes.json | 3 + 13 files changed, 385 insertions(+), 20 deletions(-) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go create mode 100644 ui/src/components/workflow/node/ApplyNodeConfigFormTencentCloudEOConfig.tsx diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index af4aa234..90e8a972 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -30,6 +30,7 @@ import ( pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns" pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun" pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud" + pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo" pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel" pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine" pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn" @@ -294,7 +295,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, - RegionId: maputil.GetString(options.ProviderApplyConfig, "region_id"), + RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -410,20 +411,36 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { return applicant, err } - case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: + case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS, domain.ApplyDNSProviderTypeTencentCloudEO: { access := domain.AccessConfigForTencentCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } - applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - DnsPropagationTimeout: options.DnsPropagationTimeout, - DnsTTL: options.DnsTTL, - }) - return applicant, err + switch options.Provider { + case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: + applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + case domain.ApplyDNSProviderTypeTencentCloudEO: + applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"), + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + default: + break + } } case domain.ApplyDNSProviderTypeVercel: diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 029405d9..0e4bcce3 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -101,6 +101,7 @@ const ( ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun") ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS] ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns") + ApplyDNSProviderTypeTencentCloudEO = ApplyDNSProviderType("tencentcloud-eo") ApplyDNSProviderTypeVercel = ApplyDNSProviderType("vercel") ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS] ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns") diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go index 92ef6dfe..0329d18d 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go @@ -106,6 +106,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return err } + if record == nil { // add new record resp, err := d.client.CreateRecordOpenapi(&model.CreateRecordOpenapiRequest{ diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go new file mode 100644 index 00000000..57f74193 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go @@ -0,0 +1,207 @@ +package lego_tencentcloudeo + +import ( + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" +) + +const ( + envNamespace = "TENCENTCLOUDEO_" + + EnvSecretID = envNamespace + "SECRET_ID" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvZoneId = envNamespace + "ZONE_ID" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + SecretID string + SecretKey string + ZoneId string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int32 + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *teo.Client + config *Config +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvSecretID, EnvSecretKey, EnvZoneId) + if err != nil { + return nil, fmt.Errorf("tencentcloud-eo: %w", err) + } + + config := NewDefaultConfig() + config.SecretID = values[EnvSecretID] + config.SecretKey = values[EnvSecretKey] + config.ZoneId = values[EnvSecretKey] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("tencentcloud-eo: the configuration of the DNS provider is nil") + } + + credential := common.NewCredential(config.SecretID, config.SecretKey) + cpf := profile.NewClientProfile() + cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds())) + client, err := teo.NewClient(credential, "", cpf) + if err != nil { + return nil, err + } + + return &DNSProvider{ + client: client, + config: config, + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + if err := d.addOrUpdateDNSRecord(strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil { + return fmt.Errorf("tencentcloud-eo: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + if err := d.removeDNSRecord(strings.TrimRight(info.EffectiveFQDN, ".")); err != nil { + return fmt.Errorf("tencentcloud-eo: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) { + pageOffset := 0 + pageLimit := 1000 + for { + request := teo.NewDescribeDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.Offset = common.Int64Ptr(int64(pageOffset)) + request.Limit = common.Int64Ptr(int64(pageLimit)) + request.Filters = []*teo.AdvancedFilter{ + { + Name: common.StringPtr("type"), + Values: []*string{common.StringPtr("TXT")}, + }, + } + + response, err := d.client.DescribeDnsRecords(request) + if err != nil { + return nil, err + } + + if response.Response == nil { + break + } else { + for _, record := range response.Response.DnsRecords { + if *record.Name == effectiveFQDN { + return record, nil + } + } + + if len(response.Response.DnsRecords) < int(pageLimit) { + break + } + + pageOffset += len(response.Response.DnsRecords) + } + } + + return nil, nil +} + +func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { + record, err := d.getDNSRecord(effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + request := teo.NewCreateDnsRecordRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.Name = common.StringPtr(effectiveFQDN) + request.Type = common.StringPtr("TXT") + request.Content = common.StringPtr(value) + request.TTL = common.Int64Ptr(int64(d.config.TTL)) + _, err := d.client.CreateDnsRecord(request) + return err + } else { + record.Content = common.StringPtr(value) + request := teo.NewModifyDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.DnsRecords = []*teo.DnsRecord{record} + if _, err := d.client.ModifyDnsRecords(request); err != nil { + return err + } + + if *record.Status == "disable" { + request := teo.NewModifyDnsRecordsStatusRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.RecordsToEnable = []*string{record.RecordId} + if _, err = d.client.ModifyDnsRecordsStatus(request); err != nil { + return err + } + } + + return nil + } +} + +func (d *DNSProvider) removeDNSRecord(effectiveFQDN string) error { + record, err := d.getDNSRecord(effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + return nil + } else { + request := teo.NewDeleteDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.RecordIds = []*string{record.RecordId} + _, err = d.client.DeleteDnsRecords(request) + return err + } +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go new file mode 100644 index 00000000..33552ecf --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go @@ -0,0 +1,41 @@ +package tencentcloudeo + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + + internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal" +) + +type ChallengeProviderConfig struct { + SecretId string `json:"secretId"` + SecretKey string `json:"secretKey"` + ZoneId string `json:"zoneId"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.SecretID = config.SecretId + providerConfig.SecretKey = config.SecretKey + providerConfig.ZoneId = config.ZoneId + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := internal.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index a7d6a101..a1ca977d 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -45,7 +45,7 @@ func (n *applyNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this application, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to apply, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-apply, because %s", skipReason)) } // 初始化申请器 diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 42bc9ca6..edb3c53d 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -58,7 +58,7 @@ func (n *deployNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this deployment, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to deploy, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-deploy, because %s", skipReason)) } } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 6c46e90f..891f2978 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -43,7 +43,7 @@ func (n *uploadNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this upload, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to upload, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-upload, because %s", skipReason)) } // 生成证书实体 diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index c714e197..6d219b49 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -36,6 +36,7 @@ import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/val import ApplyNodeConfigFormAWSRoute53Config from "./ApplyNodeConfigFormAWSRoute53Config"; import ApplyNodeConfigFormHuaweiCloudDNSConfig from "./ApplyNodeConfigFormHuaweiCloudDNSConfig"; import ApplyNodeConfigFormJDCloudDNSConfig from "./ApplyNodeConfigFormJDCloudDNSConfig"; +import ApplyNodeConfigFormTencentCloudEOConfig from "./ApplyNodeConfigFormTencentCloudEOConfig"; type ApplyNodeConfigFormFieldValues = Partial; @@ -125,6 +126,19 @@ const ApplyNodeConfigForm = forwardRef("domains", formInst); const fieldNameservers = Form.useWatch("nameservers", formInst); + const [showProvider, setShowProvider] = useState(false); + useEffect(() => { + // 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段; + // 如果对应多个(如 AWS 的 Route53、Lightsail,腾讯云的 DNS、EdgeOne 等),则显示。 + if (fieldProviderAccessId) { + const access = accesses.find((e) => e.id === fieldProviderAccessId); + const providers = Array.from(applyDNSProvidersMap.values()).filter((e) => e.provider === access?.provider); + setShowProvider(providers.length > 1); + } else { + setShowProvider(false); + } + }, [accesses, fieldProviderAccessId]); + const [nestedFormInst] = Form.useForm(); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" }); const nestedFormEl = useMemo(() => { @@ -149,12 +163,15 @@ const ApplyNodeConfigForm = forwardRef; + case APPLY_DNS_PROVIDERS.TENCENTCLOUD_EO: + return ; } }, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]); const handleProviderSelect = (value: string) => { if (fieldProvider === value) return; + // 切换 DNS 提供商时联动授权信息 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId); onValuesChange?.(formInst.getFieldsValue(true)); @@ -169,10 +186,13 @@ const ApplyNodeConfigForm = forwardRef { if (fieldProviderAccessId === value) return; - // DNS 提供商和授权提供商目前一一对应,因此切换授权时,自动切换到相应提供商 + // 切换授权信息时联动 DNS 提供商 const access = accesses.find((access) => access.id === value); - formInst.setFieldValue("provider", Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider)?.type); - onValuesChange?.(formInst.getFieldsValue(true)); + const provider = Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider); + if (fieldProvider !== provider?.type) { + formInst.setFieldValue("provider", provider?.type); + onValuesChange?.(formInst.getFieldsValue(true)); + } }; const handleFormProviderChange = (name: string) => { @@ -252,10 +272,16 @@ const ApplyNodeConfigForm = forwardRef -