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/internal/applicant/applicant.go b/internal/applicant/applicant.go index ebe6208f..8cd8b6d0 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -73,12 +73,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) } else { - accessConfig, err := access.UnmarshalConfigToMap() - if err != nil { - return nil, fmt.Errorf("failed to unmarshal access config: %w", err) - } - - options.ProviderAccessConfig = accessConfig + options.ProviderAccessConfig = access.Config } certRepo := repository.NewCertificateRepository() 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/deployer/deployer.go b/internal/deployer/deployer.go index 36e92866..6efc001c 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -39,14 +39,9 @@ func NewWithDeployNode(node *domain.WorkflowNode, certdata struct { return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) } - accessConfig, err := access.UnmarshalConfigToMap() - if err != nil { - return nil, fmt.Errorf("failed to unmarshal access config: %w", err) - } - deployer, err := createDeployer(&deployerOptions{ Provider: domain.DeployProviderType(nodeConfig.Provider), - ProviderAccessConfig: accessConfig, + ProviderAccessConfig: access.Config, ProviderDeployConfig: nodeConfig.ProviderConfig, }) if err != nil { diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index c6238aff..61d14613 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -24,7 +24,10 @@ 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" + 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" @@ -36,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" @@ -63,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" @@ -303,7 +308,7 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } - case domain.DeployProviderTypeBaiduCloudCDN: + case domain.DeployProviderTypeBaiduCloudAppBLB, domain.DeployProviderTypeBaiduCloudBLB, domain.DeployProviderTypeBaiduCloudCDN, domain.DeployProviderTypeBaiduCloudCert: { access := domain.AccessConfigForBaiduCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -311,6 +316,30 @@ 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, + 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, @@ -319,6 +348,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 } @@ -479,7 +515,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 { @@ -508,6 +544,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, @@ -849,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 { @@ -877,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/access.go b/internal/domain/access.go index 4b0b08b9..dc796c83 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -1,7 +1,6 @@ package domain import ( - "encoding/json" "time" ) @@ -9,19 +8,10 @@ const CollectionNameAccess = "access" type Access struct { Meta - Name string `json:"name" db:"name"` - Provider string `json:"provider" db:"provider"` - Config string `json:"config" db:"config"` - DeletedAt *time.Time `json:"deleted" db:"deleted"` -} - -func (a *Access) UnmarshalConfigToMap() (map[string]any, error) { - config := make(map[string]any) - if err := json.Unmarshal([]byte(a.Config), &config); err != nil { - return nil, err - } - - return config, nil + Name string `json:"name" db:"name"` + Provider string `json:"provider" db:"provider"` + Config map[string]any `json:"config" db:"config"` + DeletedAt *time.Time `json:"deleted" db:"deleted"` } type AccessConfigFor1Panel struct { diff --git a/internal/domain/provider.go b/internal/domain/provider.go index af877778..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") @@ -135,7 +136,10 @@ 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") + DeployProviderTypeBaiduCloudCert = DeployProviderType("baiducloud-cert") DeployProviderTypeBaishanCDN = DeployProviderType("baishan-cdn") DeployProviderTypeBaotaPanelConsole = DeployProviderType("baotapanel-console") DeployProviderTypeBaotaPanelSite = DeployProviderType("baotapanel-site") @@ -147,6 +151,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") @@ -176,6 +181,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/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/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/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/baiducloud-appblb/baiducloud_appblb.go b/internal/pkg/core/deployer/providers/baiducloud-appblb/baiducloud_appblb.go new file mode 100644 index 00000000..7a5f7ef2 --- /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-cert" + "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 new file mode 100644 index 00000000..6be94e6d --- /dev/null +++ b/internal/pkg/core/deployer/providers/baiducloud-blb/baiducloud_blb.go @@ -0,0 +1,332 @@ +package baiducloudblb + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strconv" + "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" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" + "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{ + ClientToken: generateClientToken(), + 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{ + ClientToken: generateClientToken(), + 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{ + ClientToken: generateClientToken(), + 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 +} + +func generateClientToken() string { + return strings.ReplaceAll(uuid.New().String(), "-", "") +} 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/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/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 } 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/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 { 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/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go new file mode 100644 index 00000000..1f311f87 --- /dev/null +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert.go @@ -0,0 +1,139 @@ +package baiducloudcert + +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/cert" +) + +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-cert/baiducloud_cert_test.go b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert_test.go new file mode 100644 index 00000000..55de1576 --- /dev/null +++ b/internal/pkg/core/uploader/providers/baiducloud-cert/baiducloud_cert_test.go @@ -0,0 +1,72 @@ +package baiducloudcert_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/baiducloud-cert" +) + +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/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/internal/pkg/vendors/baiducloud-sdk/cert/api.go b/internal/pkg/vendors/baiducloud-sdk/cert/api.go new file mode 100644 index 00000000..78c6c26a --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cert/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/cert/client.go b/internal/pkg/vendors/baiducloud-sdk/cert/client.go new file mode 100644 index 00000000..02c4feff --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cert/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/cert/models.go b/internal/pkg/vendors/baiducloud-sdk/cert/models.go new file mode 100644 index 00000000..d3fe0449 --- /dev/null +++ b/internal/pkg/vendors/baiducloud-sdk/cert/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 +} diff --git a/internal/repository/access.go b/internal/repository/access.go index 0bfeaea9..d25a6366 100644 --- a/internal/repository/access.go +++ b/internal/repository/access.go @@ -39,6 +39,11 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces return nil, fmt.Errorf("record is nil") } + config := make(map[string]any) + if err := record.UnmarshalJSONField("config", &config); err != nil { + return nil, err + } + access := &domain.Access{ Meta: domain.Meta{ Id: record.Id, @@ -47,7 +52,7 @@ func (r *AccessRepository) castRecordToModel(record *core.Record) (*domain.Acces }, Name: record.GetString("name"), Provider: record.GetString("provider"), - Config: record.GetString("config"), + Config: config, } return access, 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/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/WorkflowRunDetail.tsx b/ui/src/components/workflow/WorkflowRunDetail.tsx index 5d8c7f29..e104410a 100644 --- a/ui/src/components/workflow/WorkflowRunDetail.tsx +++ b/ui/src/components/workflow/WorkflowRunDetail.tsx @@ -37,6 +37,7 @@ import Show from "@/components/Show"; import { type CertificateModel } from "@/domain/certificate"; import type { WorkflowLogModel } from "@/domain/workflowLog"; import { WORKFLOW_RUN_STATUSES, type WorkflowRunModel } from "@/domain/workflowRun"; +import { useBrowserTheme } from "@/hooks"; import { listByWorkflowRunId as listCertificatesByWorkflowRunId } from "@/repository/certificate"; import { listByWorkflowRunId as listLogsByWorkflowRunId } from "@/repository/workflowLog"; import { mergeCls } from "@/utils/css"; @@ -67,6 +68,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin const { t } = useTranslation(); const { token: themeToken } = theme.useToken(); + const { theme: browserTheme } = useBrowserTheme(); type Log = Pick; type LogGroup = { id: string; name: string; records: Log[] }; @@ -212,7 +214,7 @@ const WorkflowRunLogs = ({ runId, runStatus }: { runId: string; runStatus: strin }} trigger={["click"]} > -