From 20d2c5699c249193f863ed5a36f4a441f00b5d2b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 26 Oct 2024 00:31:38 +0800 Subject: [PATCH] feat: add aliyun clb deployer --- README.md | 2 +- README_EN.md | 32 +- internal/deployer/aliyun_clb.go | 281 ++++++++++++++++++ internal/deployer/deployer.go | 3 + internal/deployer/huaweicloud_cdn.go | 2 +- internal/deployer/huaweicloud_elb.go | 39 ++- internal/domain/domains.go | 34 ++- .../components/certimate/DeployEditDialog.tsx | 4 + .../certimate/DeployToAliyunCLB.tsx | 156 ++++++++++ .../certimate/DeployToHuaweiCloudELB.tsx | 2 +- ui/src/domain/domain.ts | 1 + ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.domain.json | 10 + ui/src/i18n/locales/zh/nls.common.json | 1 + ui/src/i18n/locales/zh/nls.domain.json | 26 +- 15 files changed, 554 insertions(+), 40 deletions(-) create mode 100644 internal/deployer/aliyun_clb.go create mode 100644 ui/src/components/certimate/DeployToAliyunCLB.tsx diff --git a/README.md b/README.md index 63d7898b..4fe79035 100644 --- a/README.md +++ b/README.md @@ -73,7 +73,7 @@ make local.run | 服务商 | 支持申请证书 | 支持部署证书 | 备注 | | :--------: | :----------: | :----------: | ------------------------------------------------------------ | -| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN | +| 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN、CLB(SLB) | | 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、CLB | | 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | | 七牛云 | | √ | 可部署到七牛云 CDN | diff --git a/README_EN.md b/README_EN.md index 5bb30b38..74cb3098 100644 --- a/README_EN.md +++ b/README_EN.md @@ -70,22 +70,22 @@ password:1234567890 ## List of Supported Providers -| Provider | Registration | Deployment | Remarks | -| :-----------: | :----------: | :--------: | ------------------------------------------------------------------------------------------------ | -| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN | -| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, CLB | -| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB | -| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN | -| AWS | √ | | Supports domains managed on AWS Route53 | -| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | -| GoDaddy | √ | | Supports domains registered on GoDaddy | -| Namesilo | √ | | Supports domains registered on Namesilo | -| PowerDNS | √ | | Supports domains managed on PowerDNS | -| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | -| Local Deploy | | √ | Supports deployment to local servers | -| SSH | | √ | Supports deployment to SSH servers | -| Webhook | | √ | Supports callback to Webhook | -| Kubernetes | | √ | Supports deployment to Kubernetes Secret | +| Provider | Registration | Deployment | Remarks | +| :-----------: | :----------: | :--------: | ----------------------------------------------------------------------------------------------------- | +| Alibaba Cloud | √ | √ | Supports domains registered on Alibaba Cloud; supports deployment to Alibaba Cloud OSS, CDN, CLB(SLB) | +| Tencent Cloud | √ | √ | Supports domains registered on Tencent Cloud; supports deployment to Tencent Cloud COS, CDN, CLB | +| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN, ELB | +| Qiniu Cloud | | √ | Supports deployment to Qiniu Cloud CDN | +| AWS | √ | | Supports domains managed on AWS Route53 | +| CloudFlare | √ | | Supports domains registered on CloudFlare; CloudFlare services come with SSL certificates | +| GoDaddy | √ | | Supports domains registered on GoDaddy | +| Namesilo | √ | | Supports domains registered on Namesilo | +| PowerDNS | √ | | Supports domains managed on PowerDNS | +| HTTP Request | √ | | Supports domains which allow managing DNS by HTTP request | +| Local Deploy | | √ | Supports deployment to local servers | +| SSH | | √ | Supports deployment to SSH servers | +| Webhook | | √ | Supports callback to Webhook | +| Kubernetes | | √ | Supports deployment to Kubernetes Secret | ## Screenshots diff --git a/internal/deployer/aliyun_clb.go b/internal/deployer/aliyun_clb.go new file mode 100644 index 00000000..9a0f8789 --- /dev/null +++ b/internal/deployer/aliyun_clb.go @@ -0,0 +1,281 @@ +package deployer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + + openapi "github.com/alibabacloud-go/darabonba-openapi/v2/client" + slb20140515 "github.com/alibabacloud-go/slb-20140515/v4/client" + "github.com/alibabacloud-go/tea/tea" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/uploader" +) + +type AliyunCLBDeployer struct { + option *DeployerOption + infos []string + + sdkClient *slb20140515.Client + sslUploader uploader.Uploader +} + +func NewAliyunCLBDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.AliyunAccess{} + json.Unmarshal([]byte(option.Access), access) + + client, err := (&AliyunCLBDeployer{}).createSdkClient( + access.AccessKeyId, + access.AccessKeySecret, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + uploader, err := uploader.NewAliyunSLBUploader(&uploader.AliyunSLBUploaderConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Region: option.DeployConfig.GetConfigAsString("region"), + }) + if err != nil { + return nil, err + } + + return &AliyunCLBDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *AliyunCLBDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *AliyunCLBDeployer) GetInfo() []string { + return d.infos +} + +func (d *AliyunCLBDeployer) Deploy(ctx context.Context) error { + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "loadbalancer": + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + if err := d.deployToListener(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") + } + + return nil +} + +func (d *AliyunCLBDeployer) createSdkClient(accessKeyId, accessKeySecret, region string) (*slb20140515.Client, error) { + if region == "" { + region = "cn-hangzhou" // CLB(SLB) 服务默认区域:华东一杭州 + } + + aConfig := &openapi.Config{ + AccessKeyId: tea.String(accessKeyId), + AccessKeySecret: tea.String(accessKeySecret), + } + + var endpoint string + switch region { + case "cn-hangzhou": + case "cn-hangzhou-finance": + case "cn-shanghai-finance-1": + case "cn-shenzhen-finance-1": + endpoint = "slb.aliyuncs.com" + default: + endpoint = fmt.Sprintf("slb.%s.aliyuncs.com", region) + } + aConfig.Endpoint = tea.String(endpoint) + + client, err := slb20140515.NewClient(aConfig) + if err != nil { + return nil, err + } + + return client, nil +} + +func (d *AliyunCLBDeployer) deployToLoadbalancer(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + // 查询负载均衡器实例的详细信息 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerattribute + describeLoadBalancerAttributeReq := &slb20140515.DescribeLoadBalancerAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + } + describeLoadBalancerAttributeResp, err := d.sdkClient.DescribeLoadBalancerAttribute(describeLoadBalancerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB 负载均衡器实例", describeLoadBalancerAttributeResp)) + + // 查询监听列表 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerlisteners + aliListenerPorts := make([]int32, 0) + listListenersPage := 1 + listListenersLimit := int32(100) + var listListenersToken *string = nil + for { + describeLoadBalancerListenersReq := &slb20140515.DescribeLoadBalancerListenersRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + MaxResults: tea.Int32(listListenersLimit), + NextToken: listListenersToken, + LoadBalancerId: []*string{tea.String(aliLoadbalancerId)}, + ListenerProtocol: tea.String("https"), + } + describeLoadBalancerListenersResp, err := d.sdkClient.DescribeLoadBalancerListeners(describeLoadBalancerListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerListeners': %w", err) + } + + if describeLoadBalancerListenersResp.Body.Listeners != nil { + for _, listener := range describeLoadBalancerListenersResp.Body.Listeners { + aliListenerPorts = append(aliListenerPorts, *listener.ListenerPort) + } + } + + if describeLoadBalancerListenersResp.Body.NextToken == nil { + break + } else { + listListenersToken = describeLoadBalancerListenersResp.Body.NextToken + listListenersPage += 1 + } + } + + d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", aliListenerPorts)) + + // 上传证书到 SLB + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 批量更新监听证书 + var errs []error + for _, aliListenerPort := range aliListenerPorts { + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *AliyunCLBDeployer) deployToListener(ctx context.Context) error { + aliLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if aliLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + + aliListenerPort := d.option.DeployConfig.GetConfigAsInt32("listenerPort") + if aliListenerPort == 0 { + return errors.New("`listenerPort` is required") + } + + // 上传证书到 SLB + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 更新监听 + if err := d.updateListenerCertificate(ctx, aliLoadbalancerId, aliListenerPort, uploadResult.CertId); err != nil { + return err + } + + return nil +} + +func (d *AliyunCLBDeployer) updateListenerCertificate(ctx context.Context, aliLoadbalancerId string, aliListenerPort int32, aliCertId string) error { + // 查询监听配置 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describeloadbalancerhttpslistenerattribute + describeLoadBalancerHTTPSListenerAttributeReq := &slb20140515.DescribeLoadBalancerHTTPSListenerAttributeRequest{ + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + } + describeLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.DescribeLoadBalancerHTTPSListenerAttribute(describeLoadBalancerHTTPSListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeLoadBalancerHTTPSListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB HTTPS 监听配置", describeLoadBalancerHTTPSListenerAttributeResp)) + + // 查询扩展域名 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-describedomainextensions + describeDomainExtensionsReq := &slb20140515.DescribeDomainExtensionsRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + } + describeDomainExtensionsResp, err := d.sdkClient.DescribeDomainExtensions(describeDomainExtensionsReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.DescribeDomainExtensions': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到 CLB 扩展域名", describeDomainExtensionsResp)) + + // 遍历修改扩展域名 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setdomainextensionattribute + // + // 这里仅修改跟被替换证书一致的扩展域名 + if describeDomainExtensionsResp.Body.DomainExtensions == nil && describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension == nil { + for _, domainExtension := range describeDomainExtensionsResp.Body.DomainExtensions.DomainExtension { + if *domainExtension.ServerCertificateId == *describeLoadBalancerHTTPSListenerAttributeResp.Body.ServerCertificateId { + break + } + + setDomainExtensionAttributeReq := &slb20140515.SetDomainExtensionAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + DomainExtensionId: tea.String(*domainExtension.DomainExtensionId), + ServerCertificateId: tea.String(aliCertId), + } + _, err := d.sdkClient.SetDomainExtensionAttribute(setDomainExtensionAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.SetDomainExtensionAttribute': %w", err) + } + } + } + + // 修改监听配置 + // REF: https://help.aliyun.com/zh/slb/classic-load-balancer/developer-reference/api-slb-2014-05-15-setloadbalancerhttpslistenerattribute + // + // 注意修改监听配置要放在修改扩展域名之后 + setLoadBalancerHTTPSListenerAttributeReq := &slb20140515.SetLoadBalancerHTTPSListenerAttributeRequest{ + RegionId: tea.String(d.option.DeployConfig.GetConfigAsString("region")), + LoadBalancerId: tea.String(aliLoadbalancerId), + ListenerPort: tea.Int32(aliListenerPort), + ServerCertificateId: tea.String(aliCertId), + } + setLoadBalancerHTTPSListenerAttributeResp, err := d.sdkClient.SetLoadBalancerHTTPSListenerAttribute(setLoadBalancerHTTPSListenerAttributeReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'slb.SetLoadBalancerHTTPSListenerAttribute': %w", err) + } + + d.infos = append(d.infos, toStr("已更新 CLB HTTPS 监听配置", setLoadBalancerHTTPSListenerAttributeResp)) + + return nil +} diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 512fd748..8e130b3b 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -18,6 +18,7 @@ const ( targetAliyunOSS = "aliyun-oss" targetAliyunCDN = "aliyun-cdn" targetAliyunESA = "aliyun-dcdn" + targetAliyunCLB = "aliyun-clb" targetTencentCDN = "tencent-cdn" targetTencentCLB = "tencent-clb" targetTencentCOS = "tencent-cos" @@ -106,6 +107,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return NewAliyunCDNDeployer(option) case targetAliyunESA: return NewAliyunESADeployer(option) + case targetAliyunCLB: + return NewAliyunCLBDeployer(option) case targetTencentCDN: return NewTencentCDNDeployer(option) case targetTencentCLB: diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index f7835dcb..ab6e936b 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -41,9 +41,9 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ - Region: "", AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, + Region: "", }) if err != nil { return nil, err diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go index e9a6f243..fc5920bb 100644 --- a/internal/deployer/huaweicloud_elb.go +++ b/internal/deployer/huaweicloud_elb.go @@ -46,9 +46,9 @@ func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { } uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{ - Region: option.DeployConfig.GetConfigAsString("region"), AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, + Region: option.DeployConfig.GetConfigAsString("region"), }) if err != nil { return nil, err @@ -176,10 +176,15 @@ func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, r } func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error { + hcCertId := d.option.DeployConfig.GetConfigAsString("certificateId") + if hcCertId == "" { + return errors.New("`certificateId` is required") + } + // 更新证书 // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html updateCertificateReq := &hcElbModel.UpdateCertificateRequest{ - CertificateId: d.option.DeployConfig.GetConfigAsString("certificateId"), + CertificateId: hcCertId, Body: &hcElbModel.UpdateCertificateRequestBody{ Certificate: &hcElbModel.UpdateCertificateOption{ Certificate: cast.StringPtr(d.option.Certificate.Certificate), @@ -198,21 +203,26 @@ func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error } func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error { + hcLoadbalancerId := d.option.DeployConfig.GetConfigAsString("loadbalancerId") + if hcLoadbalancerId == "" { + return errors.New("`loadbalancerId` is required") + } + // 查询负载均衡器详情 // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{ - LoadbalancerId: d.option.DeployConfig.GetConfigAsString("loadbalancerId"), + LoadbalancerId: hcLoadbalancerId, } showLoadBalancerResp, err := d.sdkClient.ShowLoadBalancer(showLoadBalancerReq) if err != nil { return fmt.Errorf("failed to execute sdk request 'elb.ShowLoadBalancer': %w", err) } - d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器", showLoadBalancerResp)) + d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器", showLoadBalancerResp)) // 查询监听器列表 // REF: https://support.huaweicloud.com/api-elb/ListListeners.html - listenerIds := make([]string, 0) + hcListenerIds := make([]string, 0) listListenersLimit := int32(2000) var listListenersMarker *string = nil for { @@ -229,7 +239,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error if listListenersResp.Listeners != nil { for _, listener := range *listListenersResp.Listeners { - listenerIds = append(listenerIds, listener.Id) + hcListenerIds = append(hcListenerIds, listener.Id) } } @@ -240,7 +250,7 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } } - d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器下的监听器", listenerIds)) + d.infos = append(d.infos, toStr("已查询到 ELB 负载均衡器下的监听器", hcListenerIds)) // 上传证书到 SCM uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) @@ -252,8 +262,8 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error // 批量更新监听器证书 var errs []error - for _, listenerId := range listenerIds { - if err := d.updateListenerCertificate(ctx, listenerId, uploadResult.CertId); err != nil { + for _, hcListenerId := range hcListenerIds { + if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { errs = append(errs, err) } } @@ -265,6 +275,11 @@ func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error } func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { + hcListenerId := d.option.DeployConfig.GetConfigAsString("listenerId") + if hcListenerId == "" { + return errors.New("`listenerId` is required") + } + // 上传证书到 SCM uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { @@ -274,7 +289,7 @@ func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { d.infos = append(d.infos, toStr("已上传证书", uploadResult)) // 更新监听器证书 - if err := d.updateListenerCertificate(ctx, d.option.DeployConfig.GetConfigAsString("listenerId"), uploadResult.CertId); err != nil { + if err := d.updateListenerCertificate(ctx, hcListenerId, uploadResult.CertId); err != nil { return err } @@ -292,7 +307,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err) } - d.infos = append(d.infos, toStr("已查询到到 ELB 监听器", showListenerResp)) + d.infos = append(d.infos, toStr("已查询到 ELB 监听器", showListenerResp)) // 更新监听器 // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html @@ -359,7 +374,7 @@ func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) } - d.infos = append(d.infos, toStr("已更新监听器", updateListenerResp)) + d.infos = append(d.infos, toStr("已更新 ELB 监听器", updateListenerResp)) return nil } diff --git a/internal/domain/domains.go b/internal/domain/domains.go index 78acbb3d..bed38ac2 100644 --- a/internal/domain/domains.go +++ b/internal/domain/domains.go @@ -18,7 +18,6 @@ type DeployConfig struct { Config map[string]any `json:"config"` } - // 以字符串形式获取配置项。 // // 入参: @@ -52,6 +51,39 @@ func (dc *DeployConfig) GetConfigOrDefaultAsString(key string, defaultValue stri return defaultValue } +// 以 32 位整数形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回 0。 +func (dc *DeployConfig) GetConfigAsInt32(key string) int32 { + return dc.GetConfigOrDefaultAsInt32(key, 0) +} + +// 以 32 位整数形式获取配置项。 +// +// 入参: +// - key: 配置项的键。 +// - defaultValue: 默认值。 +// +// 出参: +// - 配置项的值。如果配置项不存在或者类型不是 32 位整数,则返回默认值。 +func (dc *DeployConfig) GetConfigOrDefaultAsInt32(key string, defaultValue int32) int32 { + if dc.Config == nil { + return defaultValue + } + + if value, ok := dc.Config[key]; ok { + if result, ok := value.(int32); ok { + return result + } + } + + return defaultValue +} + // 以布尔形式获取配置项。 // // 入参: diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 7ef5f291..5fdc4ab1 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -11,6 +11,7 @@ import AccessEditDialog from "./AccessEditDialog"; import { Context as DeployEditContext } from "./DeployEdit"; import DeployToAliyunOSS from "./DeployToAliyunOSS"; import DeployToAliyunCDN from "./DeployToAliyunCDN"; +import DeployToAliyunCLB from "./DeployToAliyunCLB"; import DeployToTencentCDN from "./DeployToTencentCDN"; import DeployToTencentCLB from "./DeployToTencentCLB"; import DeployToTencentCOS from "./DeployToTencentCOS"; @@ -118,6 +119,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "aliyun-dcdn": childComponent = ; break; + case "aliyun-clb": + childComponent = ; + break; case "tencent-cdn": childComponent = ; break; diff --git a/ui/src/components/certimate/DeployToAliyunCLB.tsx b/ui/src/components/certimate/DeployToAliyunCLB.tsx new file mode 100644 index 00000000..6eb18d9e --- /dev/null +++ b/ui/src/components/certimate/DeployToAliyunCLB.tsx @@ -0,0 +1,156 @@ +import { useEffect } from "react"; +import { useTranslation } from "react-i18next"; +import { z } from "zod"; +import { produce } from "immer"; + +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { useDeployEditContext } from "./DeployEdit"; + +const DeployToAliyunCLB = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-hangzhou", + resourceType: "", + loadbalancerId: "", + listenerPort: "443", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.aliyun_clb_region.placeholder")), + resourceType: z.string().min(1, t("domain.deployment.form.aliyun_clb_resource_type.placeholder")), + loadbalancerId: z.string().optional(), + listenerPort: z.string().optional(), + }) + .refine((data) => (data.resourceType === "loadbalancer" || data.resourceType === "listener" ? !!data.loadbalancerId?.trim() : true), { + message: t("domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder"), + path: ["loadbalancerId"], + }) + .refine((data) => (data.resourceType === "listener" ? +data.listenerPort! > 0 && +data.listenerPort! < 65535 : true), { + message: t("domain.deployment.form.aliyun_clb_listener_port.placeholder"), + path: ["listenerPort"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + region: res.error.errors.find((e) => e.path[0] === "region")?.message, + resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, + loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerPort: res.error.errors.find((e) => e.path[0] === "listenerPort")?.message, + }); + } else { + setError({ + ...error, + region: undefined, + resourceType: undefined, + loadbalancerId: undefined, + listenerPort: undefined, + }); + } + }, [data]); + + return ( +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+ +
+ + +
{error?.resourceType}
+
+ +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.loadbalancerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.loadbalancerId}
+
+ + {data?.config?.resourceType === "listener" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.listenerPort = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.listenerPort}
+
+ ) : ( + <> + )} +
+ ); +}; + +export default DeployToAliyunCLB; diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx index 9cb5e686..b85203e8 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx @@ -44,7 +44,7 @@ const DeployToHuaweiCloudCDN = () => { message: t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder"), path: ["certificateId"], }) - .refine((data) => (data.resourceType === "loadbalancer" ? !!data.certificateId?.trim() : true), { + .refine((data) => (data.resourceType === "loadbalancer" ? !!data.loadbalancerId?.trim() : true), { message: t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder"), path: ["loadbalancerId"], }) diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index 97bb4ce1..4f5131df 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -75,6 +75,7 @@ export const deployTargetsMap: Map = new Map ["aliyun-oss", "common.provider.aliyun.oss", "/imgs/providers/aliyun.svg"], ["aliyun-cdn", "common.provider.aliyun.cdn", "/imgs/providers/aliyun.svg"], ["aliyun-dcdn", "common.provider.aliyun.dcdn", "/imgs/providers/aliyun.svg"], + ["aliyun-clb", "common.provider.aliyun.clb", "/imgs/providers/aliyun.svg"], ["tencent-cdn", "common.provider.tencent.cdn", "/imgs/providers/tencent.svg"], ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"], ["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"], diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 872ef19b..660f7abe 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -56,6 +56,7 @@ "common.provider.aliyun.oss": "Alibaba Cloud - OSS", "common.provider.aliyun.cdn": "Alibaba Cloud - CDN", "common.provider.aliyun.dcdn": "Alibaba Cloud - DCDN", + "common.provider.aliyun.clb": "Alibaba Cloud - CLB(SLB)", "common.provider.tencent": "Tencent Cloud", "common.provider.tencent.cdn": "Tencent Cloud - CDN", "common.provider.tencent.clb": "Tencent Cloud - CLB", diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index 80f1a4d7..996c3326 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -61,6 +61,16 @@ "domain.deployment.form.aliyun_oss_endpoint.placeholder": "Please enter endpoint", "domain.deployment.form.aliyun_oss_bucket.label": "Bucket", "domain.deployment.form.aliyun_oss_bucket.placeholder": "Please enter bucket", + "domain.deployment.form.aliyun_clb_region.label": "Region", + "domain.deployment.form.aliyun_clb_region.placeholder": "Please enter region (e.g. cn-hangzhou)", + "domain.deployment.form.aliyun_clb_resource_type.label": "Resource Type", + "domain.deployment.form.aliyun_clb_resource_type.placeholder": "Please select CLB resource type", + "domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "CLB LoadBalancer", + "domain.deployment.form.aliyun_clb_resource_type.option.listener.label": "CLB Listener", + "domain.deployment.form.aliyun_clb_loadbalancer_id.label": "LoadBalancer ID", + "domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder": "Please enter CLB loadbalancer ID", + "domain.deployment.form.aliyun_clb_listener_port.label": "Listener Port", + "domain.deployment.form.aliyun_clb_listener_port.placeholder": "Please enter CLB listener port", "domain.deployment.form.tencent_cos_region.label": "Region", "domain.deployment.form.tencent_cos_region.placeholder": "Please enter region (e.g. ap-guangzhou)", "domain.deployment.form.tencent_cos_bucket.label": "Bucket", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 818807ce..66187d90 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -55,6 +55,7 @@ "common.provider.aliyun.oss": "阿里云 - OSS", "common.provider.aliyun.cdn": "阿里云 - CDN", "common.provider.aliyun.dcdn": "阿里云 - DCDN", + "common.provider.aliyun.clb": "阿里云 - CLB(原 SLB)", "common.provider.tencent": "腾讯云", "common.provider.tencent.cos": "腾讯云 - COS", "common.provider.tencent.cdn": "腾讯云 - CDN", diff --git a/ui/src/i18n/locales/zh/nls.domain.json b/ui/src/i18n/locales/zh/nls.domain.json index ae7f4d0f..fdedf22c 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -61,6 +61,16 @@ "domain.deployment.form.aliyun_oss_endpoint.placeholder": "请输入 Endpoint", "domain.deployment.form.aliyun_oss_bucket.label": "存储桶", "domain.deployment.form.aliyun_oss_bucket.placeholder": "请输入存储桶名", + "domain.deployment.form.aliyun_clb_region.label": "地域", + "domain.deployment.form.aliyun_clb_region.placeholder": "请输入地域(如 cn-hangzhou)", + "domain.deployment.form.aliyun_clb_resource_type.label": "替换方式", + "domain.deployment.form.aliyun_clb_resource_type.placeholder": "请选择替换方式", + "domain.deployment.form.aliyun_clb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听的证书", + "domain.deployment.form.aliyun_clb_resource_type.option.listener.label": "替换指定负载均衡器监听的证书", + "domain.deployment.form.aliyun_clb_loadbalancer_id.label": "负载均衡器 ID", + "domain.deployment.form.aliyun_clb_loadbalancer_id.placeholder": "请输入负载均衡器 ID", + "domain.deployment.form.aliyun_clb_listener_port.label": "监听端口", + "domain.deployment.form.aliyun_clb_listener_port.placeholder": "请输入监听端口", "domain.deployment.form.tencent_cos_region.label": "地域", "domain.deployment.form.tencent_cos_region.placeholder": "请输入地域(如 ap-guangzhou)", "domain.deployment.form.tencent_cos_bucket.label": "存储桶", @@ -75,17 +85,17 @@ "domain.deployment.form.tencent_clb_domain.placeholder": "请输入部署到的域名, 如未开启 SNI, 可置空忽略此项", "domain.deployment.form.huaweicloud_elb_region.label": "地域", "domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1)", - "domain.deployment.form.huaweicloud_elb_resource_type.label": "资源类型替换方式", - "domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "请选择资源类型替换方式", - "domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "按证书替换", - "domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "按负载均衡器替换", - "domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "按监听器替换", + "domain.deployment.form.huaweicloud_elb_resource_type.label": "替换方式", + "domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "请选择替换方式", + "domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "替换指定证书", + "domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "替换指定负载均衡器的全部监听器的证书", + "domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "替换指定监听器", "domain.deployment.form.huaweicloud_elb_certificate_id.label": "证书 ID", - "domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "请输入证书 ID(可从华为云控制面板获取)", + "domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "请输入证书 ID", "domain.deployment.form.huaweicloud_elb_loadbalancer_id.label": "负载均衡器 ID", - "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "请输入负载均衡器 ID(可从华为云控制面板获取)", + "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "请输入负载均衡器 ID", "domain.deployment.form.huaweicloud_elb_listener_id.label": "监听器 ID", - "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID(可从华为云控制面板获取)", + "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "请输入监听器 ID", "domain.deployment.form.ssh_key_path.label": "私钥保存路径", "domain.deployment.form.ssh_key_path.placeholder": "请输入私钥保存路径", "domain.deployment.form.ssh_cert_path.label": "证书保存路径",