diff --git a/README.md b/README.md index bf7ea29c..63d7898b 100644 --- a/README.md +++ b/README.md @@ -74,8 +74,8 @@ make local.run | 服务商 | 支持申请证书 | 支持部署证书 | 备注 | | :--------: | :----------: | :----------: | ------------------------------------------------------------ | | 阿里云 | √ | √ | 可签发在阿里云注册的域名;可部署到阿里云 OSS、CDN | -| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 CDN、COS、CLB | -| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN | +| 腾讯云 | √ | √ | 可签发在腾讯云注册的域名;可部署到腾讯云 COS、CDN、CLB | +| 华为云 | √ | √ | 可签发在华为云注册的域名;可部署到华为云 CDN、ELB | | 七牛云 | | √ | 可部署到七牛云 CDN | | AWS | √ | | 可签发在 AWS Route53 托管的域名 | | CloudFlare | √ | | 可签发在 CloudFlare 注册的域名;CloudFlare 服务自带 SSL 证书 | diff --git a/README_EN.md b/README_EN.md index 7cfbec6f..5bb30b38 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 CDN, COS, CLB | -| Huawei Cloud | √ | √ | Supports domains registered on Huawei Cloud; supports deployment to Huawei Cloud CDN | -| 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 | +| 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/deployer.go b/internal/deployer/deployer.go index d7fc67cd..512fd748 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -22,6 +22,7 @@ const ( targetTencentCLB = "tencent-clb" targetTencentCOS = "tencent-cos" targetHuaweiCloudCDN = "huaweicloud-cdn" + targetHuaweiCloudELB = "huaweicloud-elb" targetQiniuCdn = "qiniu-cdn" targetLocal = "local" targetSSH = "ssh" @@ -113,6 +114,8 @@ func getWithDeployConfig(record *models.Record, cert *applicant.Certificate, dep return NewTencentCOSDeployer(option) case targetHuaweiCloudCDN: return NewHuaweiCloudCDNDeployer(option) + case targetHuaweiCloudELB: + return NewHuaweiCloudELBDeployer(option) case targetQiniuCdn: return NewQiniuCDNDeployer(option) case targetLocal: diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index b6429be3..f7835dcb 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -7,24 +7,53 @@ import ( "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" - cdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" - cdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" - cdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" + hcCdn "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2" + hcCdnModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/model" + hcCdnRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/cdn/v2/region" "github.com/usual2970/certimate/internal/domain" - uploader "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/core/uploader" "github.com/usual2970/certimate/internal/pkg/utils/cast" ) type HuaweiCloudCDNDeployer struct { option *DeployerOption infos []string + + sdkClient *hcCdn.CdnClient + sslUploader uploader.Uploader } func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.HuaweiCloudAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, err + } + + client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient( + access.AccessKeyId, + access.SecretAccessKey, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 + uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ + Region: "", + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + }) + if err != nil { + return nil, err + } + return &HuaweiCloudCDNDeployer{ - option: option, - infos: make([]string, 0), + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, }, nil } @@ -37,25 +66,12 @@ func (d *HuaweiCloudCDNDeployer) GetInfo() []string { } func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { - access := &domain.HuaweiCloudAccess{} - if err := json.Unmarshal([]byte(d.option.Access), access); err != nil { - return err - } - - // TODO: CDN 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 - client, err := d.createClient("", access.AccessKeyId, access.SecretAccessKey) - if err != nil { - return err - } - - d.infos = append(d.infos, toStr("SDK 客户端创建成功", nil)) - // 查询加速域名配置 // REF: https://support.huaweicloud.com/api-cdn/ShowDomainFullConfig.html - showDomainFullConfigReq := &cdnModel.ShowDomainFullConfigRequest{ + showDomainFullConfigReq := &hcCdnModel.ShowDomainFullConfigRequest{ DomainName: d.option.DeployConfig.GetConfigAsString("domain"), } - showDomainFullConfigResp, err := client.ShowDomainFullConfig(showDomainFullConfigReq) + showDomainFullConfigResp, err := d.sdkClient.ShowDomainFullConfig(showDomainFullConfigReq) if err != nil { return err } @@ -68,19 +84,10 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { updateDomainMultiCertificatesReqBodyContent := &huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent{} updateDomainMultiCertificatesReqBodyContent.DomainName = d.option.DeployConfig.GetConfigAsString("domain") updateDomainMultiCertificatesReqBodyContent.HttpsSwitch = 1 - var updateDomainMultiCertificatesResp *cdnModel.UpdateDomainMultiCertificatesResponse + var updateDomainMultiCertificatesResp *hcCdnModel.UpdateDomainMultiCertificatesResponse if d.option.DeployConfig.GetConfigAsBool("useSCM") { - uploader, err := uploader.NewHuaweiCloudSCMUploader(&uploader.HuaweiCloudSCMUploaderConfig{ - Region: "", // TODO: SCM 服务与 DNS 服务所支持的区域可能不一致,这里暂时不传而是使用默认值,仅支持华为云国内版 - AccessKeyId: access.AccessKeyId, - SecretAccessKey: access.SecretAccessKey, - }) - if err != nil { - return err - } - // 上传证书到 SCM - uploadResult, err := uploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) if err != nil { return err } @@ -102,7 +109,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { Https: updateDomainMultiCertificatesReqBodyContent, }, } - updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(client, updateDomainMultiCertificatesReq) + updateDomainMultiCertificatesResp, err = executeHuaweiCloudCDNUploadDomainMultiCertificates(d.sdkClient, updateDomainMultiCertificatesReq) if err != nil { return err } @@ -112,7 +119,11 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { return nil } -func (d *HuaweiCloudCDNDeployer) createClient(region, accessKeyId, secretAccessKey string) (*cdn.CdnClient, error) { +func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, error) { + if region == "" { + region = "cn-north-1" // CDN 服务默认区域:华北一北京 + } + auth, err := global.NewCredentialsBuilder(). WithAk(accessKeyId). WithSk(secretAccessKey). @@ -121,16 +132,12 @@ func (d *HuaweiCloudCDNDeployer) createClient(region, accessKeyId, secretAccessK return nil, err } - if region == "" { - region = "cn-north-1" // CDN 服务默认区域:华北一北京 - } - - hcRegion, err := cdnRegion.SafeValueOf(region) + hcRegion, err := hcCdnRegion.SafeValueOf(region) if err != nil { return nil, err } - hcClient, err := cdn.CdnClientBuilder(). + hcClient, err := hcCdn.CdnClientBuilder(). WithRegion(hcRegion). WithCredential(auth). SafeBuild() @@ -138,12 +145,12 @@ func (d *HuaweiCloudCDNDeployer) createClient(region, accessKeyId, secretAccessK return nil, err } - client := cdn.NewCdnClient(hcClient) + client := hcCdn.NewCdnClient(hcClient) return client, nil } type huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent struct { - cdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"` + hcCdnModel.UpdateDomainMultiCertificatesRequestBodyContent `json:",inline"` SCMCertificateId *string `json:"scm_certificate_id,omitempty"` } @@ -156,20 +163,20 @@ type huaweicloudCDNUpdateDomainMultiCertificatesRequest struct { Body *huaweicloudCDNUpdateDomainMultiCertificatesRequestBody `json:"body,omitempty"` } -func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *cdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*cdnModel.UpdateDomainMultiCertificatesResponse, error) { +func executeHuaweiCloudCDNUploadDomainMultiCertificates(client *hcCdn.CdnClient, request *huaweicloudCDNUpdateDomainMultiCertificatesRequest) (*hcCdnModel.UpdateDomainMultiCertificatesResponse, error) { // 华为云官方 SDK 中目前提供的字段缺失,这里暂时先需自定义请求 // 可能需要等之后 SDK 更新 - requestDef := cdn.GenReqDefForUpdateDomainMultiCertificates() + requestDef := hcCdn.GenReqDefForUpdateDomainMultiCertificates() if resp, err := client.HcClient.Sync(request, requestDef); err != nil { return nil, err } else { - return resp.(*cdnModel.UpdateDomainMultiCertificatesResponse), nil + return resp.(*hcCdnModel.UpdateDomainMultiCertificatesResponse), nil } } -func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent { +func mergeHuaweiCloudCDNConfig(src *hcCdnModel.ConfigsGetBody, dest *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent) *huaweicloudCDNUpdateDomainMultiCertificatesRequestBodyContent { if src == nil { return dest } @@ -186,7 +193,7 @@ func mergeHuaweiCloudCDNConfig(src *cdnModel.ConfigsGetBody, dest *huaweicloudCD } if src.ForceRedirect != nil { - dest.ForceRedirectConfig = &cdnModel.ForceRedirect{} + dest.ForceRedirectConfig = &hcCdnModel.ForceRedirect{} if src.ForceRedirect.Status == "on" { dest.ForceRedirectConfig.Switch = 1 diff --git a/internal/deployer/huaweicloud_elb.go b/internal/deployer/huaweicloud_elb.go new file mode 100644 index 00000000..e9a6f243 --- /dev/null +++ b/internal/deployer/huaweicloud_elb.go @@ -0,0 +1,365 @@ +package deployer + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "sort" + "strings" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" + hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" + hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" + hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region" + hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" + hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" + hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" + + "github.com/usual2970/certimate/internal/domain" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/cast" +) + +type HuaweiCloudELBDeployer struct { + option *DeployerOption + infos []string + + sdkClient *hcElb.ElbClient + sslUploader uploader.Uploader +} + +func NewHuaweiCloudELBDeployer(option *DeployerOption) (Deployer, error) { + access := &domain.HuaweiCloudAccess{} + if err := json.Unmarshal([]byte(option.Access), access); err != nil { + return nil, err + } + + client, err := (&HuaweiCloudELBDeployer{}).createSdkClient( + access.AccessKeyId, + access.SecretAccessKey, + option.DeployConfig.GetConfigAsString("region"), + ) + if err != nil { + return nil, err + } + + uploader, err := uploader.NewHuaweiCloudELBUploader(&uploader.HuaweiCloudELBUploaderConfig{ + Region: option.DeployConfig.GetConfigAsString("region"), + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + }) + if err != nil { + return nil, err + } + + return &HuaweiCloudELBDeployer{ + option: option, + infos: make([]string, 0), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *HuaweiCloudELBDeployer) GetID() string { + return fmt.Sprintf("%s-%s", d.option.AccessRecord.GetString("name"), d.option.AccessRecord.Id) +} + +func (d *HuaweiCloudELBDeployer) GetInfo() []string { + return d.infos +} + +func (d *HuaweiCloudELBDeployer) Deploy(ctx context.Context) error { + switch d.option.DeployConfig.GetConfigAsString("resourceType") { + case "certificate": + if err := d.deployToCertificate(ctx); err != nil { + return err + } + case "loadbalancer": + if err := d.deployToLoadbalancer(ctx); err != nil { + return err + } + case "listener": + if err := d.deployToListener(ctx); err != nil { + return err + } + default: + return errors.New("unsupported resource type") + } + + return nil +} + +func (d *HuaweiCloudELBDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { + if region == "" { + region = "cn-north-4" // ELB 服务默认区域:华北四北京 + } + + projectId, err := (&HuaweiCloudELBDeployer{}).getSdkProjectId( + accessKeyId, + secretAccessKey, + region, + ) + if err != nil { + return nil, err + } + + auth, err := basic.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + WithProjectId(projectId). + SafeBuild() + if err != nil { + return nil, err + } + + hcRegion, err := hcElbRegion.SafeValueOf(region) + if err != nil { + return nil, err + } + + hcClient, err := hcElb.ElbClientBuilder(). + WithRegion(hcRegion). + WithCredential(auth). + SafeBuild() + if err != nil { + return nil, err + } + + client := hcElb.NewElbClient(hcClient) + return client, nil +} + +func (u *HuaweiCloudELBDeployer) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) { + if region == "" { + region = "cn-north-4" // IAM 服务默认区域:华北四北京 + } + + auth, err := global.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + SafeBuild() + if err != nil { + return "", err + } + + hcRegion, err := hcIamRegion.SafeValueOf(region) + if err != nil { + return "", err + } + + hcClient, err := hcIam.IamClientBuilder(). + WithRegion(hcRegion). + WithCredential(auth). + SafeBuild() + if err != nil { + return "", err + } + + client := hcIam.NewIamClient(hcClient) + if err != nil { + return "", err + } + + request := &hcIamModel.KeystoneListProjectsRequest{ + Name: ®ion, + } + response, err := client.KeystoneListProjects(request) + if err != nil { + return "", err + } else if response.Projects == nil || len(*response.Projects) == 0 { + return "", fmt.Errorf("no project found") + } + + return (*response.Projects)[0].Id, nil +} + +func (d *HuaweiCloudELBDeployer) deployToCertificate(ctx context.Context) error { + // 更新证书 + // REF: https://support.huaweicloud.com/api-elb/UpdateCertificate.html + updateCertificateReq := &hcElbModel.UpdateCertificateRequest{ + CertificateId: d.option.DeployConfig.GetConfigAsString("certificateId"), + Body: &hcElbModel.UpdateCertificateRequestBody{ + Certificate: &hcElbModel.UpdateCertificateOption{ + Certificate: cast.StringPtr(d.option.Certificate.Certificate), + PrivateKey: cast.StringPtr(d.option.Certificate.PrivateKey), + }, + }, + } + updateCertificateResp, err := d.sdkClient.UpdateCertificate(updateCertificateReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.UpdateCertificate': %w", err) + } + + d.infos = append(d.infos, toStr("已更新 ELB 证书", updateCertificateResp)) + + return nil +} + +func (d *HuaweiCloudELBDeployer) deployToLoadbalancer(ctx context.Context) error { + // 查询负载均衡器详情 + // REF: https://support.huaweicloud.com/api-elb/ShowLoadBalancer.html + showLoadBalancerReq := &hcElbModel.ShowLoadBalancerRequest{ + LoadbalancerId: d.option.DeployConfig.GetConfigAsString("loadbalancerId"), + } + 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)) + + // 查询监听器列表 + // REF: https://support.huaweicloud.com/api-elb/ListListeners.html + listenerIds := make([]string, 0) + listListenersLimit := int32(2000) + var listListenersMarker *string = nil + for { + listListenersReq := &hcElbModel.ListListenersRequest{ + Limit: cast.Int32Ptr(listListenersLimit), + Marker: listListenersMarker, + Protocol: &[]string{"HTTPS", "TERMINATED_HTTPS"}, + LoadbalancerId: &[]string{showLoadBalancerResp.Loadbalancer.Id}, + } + listListenersResp, err := d.sdkClient.ListListeners(listListenersReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.ListListeners': %w", err) + } + + if listListenersResp.Listeners != nil { + for _, listener := range *listListenersResp.Listeners { + listenerIds = append(listenerIds, listener.Id) + } + } + + if listListenersResp.Listeners == nil || len(*listListenersResp.Listeners) < int(listListenersLimit) { + break + } else { + listListenersMarker = listListenersResp.PageInfo.NextMarker + } + } + + d.infos = append(d.infos, toStr("已查询到到 ELB 负载均衡器下的监听器", listenerIds)) + + // 上传证书到 SCM + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 批量更新监听器证书 + var errs []error + for _, listenerId := range listenerIds { + if err := d.updateListenerCertificate(ctx, listenerId, uploadResult.CertId); err != nil { + errs = append(errs, err) + } + } + if len(errs) > 0 { + return errors.Join(errs...) + } + + return nil +} + +func (d *HuaweiCloudELBDeployer) deployToListener(ctx context.Context) error { + // 上传证书到 SCM + uploadResult, err := d.sslUploader.Upload(ctx, d.option.Certificate.Certificate, d.option.Certificate.PrivateKey) + if err != nil { + return err + } + + d.infos = append(d.infos, toStr("已上传证书", uploadResult)) + + // 更新监听器证书 + if err := d.updateListenerCertificate(ctx, d.option.DeployConfig.GetConfigAsString("listenerId"), uploadResult.CertId); err != nil { + return err + } + + return nil +} + +func (d *HuaweiCloudELBDeployer) updateListenerCertificate(ctx context.Context, hcListenerId string, hcCertId string) error { + // 查询监听器详情 + // REF: https://support.huaweicloud.com/api-elb/ShowListener.html + showListenerReq := &hcElbModel.ShowListenerRequest{ + ListenerId: hcListenerId, + } + showListenerResp, err := d.sdkClient.ShowListener(showListenerReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.ShowListener': %w", err) + } + + d.infos = append(d.infos, toStr("已查询到到 ELB 监听器", showListenerResp)) + + // 更新监听器 + // REF: https://support.huaweicloud.com/api-elb/UpdateListener.html + updateListenerReq := &hcElbModel.UpdateListenerRequest{ + ListenerId: hcListenerId, + Body: &hcElbModel.UpdateListenerRequestBody{ + Listener: &hcElbModel.UpdateListenerOption{ + DefaultTlsContainerRef: cast.StringPtr(hcCertId), + }, + }, + } + if showListenerResp.Listener.SniContainerRefs != nil { + if len(showListenerResp.Listener.SniContainerRefs) > 0 { + // 如果开启 SNI,需替换同 SAN 的证书 + sniCertIds := make([]string, 0) + sniCertIds = append(sniCertIds, hcCertId) + + listOldCertificateReq := &hcElbModel.ListCertificatesRequest{ + Id: &showListenerResp.Listener.SniContainerRefs, + } + listOldCertificateResp, err := d.sdkClient.ListCertificates(listOldCertificateReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.ListCertificates': %w", err) + } + + showNewCertificateReq := &hcElbModel.ShowCertificateRequest{ + CertificateId: hcCertId, + } + showNewCertificateResp, err := d.sdkClient.ShowCertificate(showNewCertificateReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.ShowCertificate': %w", err) + } + + for _, certificate := range *listOldCertificateResp.Certificates { + oldCertificate := certificate + newCertificate := showNewCertificateResp.Certificate + + if oldCertificate.SubjectAlternativeNames != nil && newCertificate.SubjectAlternativeNames != nil { + oldCertificateSans := oldCertificate.SubjectAlternativeNames + newCertificateSans := newCertificate.SubjectAlternativeNames + sort.Strings(*oldCertificateSans) + sort.Strings(*newCertificateSans) + if strings.Join(*oldCertificateSans, ";") == strings.Join(*newCertificateSans, ";") { + continue + } + } else { + if oldCertificate.Domain == newCertificate.Domain { + continue + } + } + + sniCertIds = append(sniCertIds, certificate.Id) + } + + updateListenerReq.Body.Listener.SniContainerRefs = &sniCertIds + } + + if showListenerResp.Listener.SniMatchAlgo != "" { + updateListenerReq.Body.Listener.SniMatchAlgo = cast.StringPtr(showListenerResp.Listener.SniMatchAlgo) + } + } + updateListenerResp, err := d.sdkClient.UpdateListener(updateListenerReq) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'elb.UpdateListener': %w", err) + } + + d.infos = append(d.infos, toStr("已更新监听器", updateListenerResp)) + + return nil +} diff --git a/internal/pkg/core/uploader/uploader.go b/internal/pkg/core/uploader/uploader.go index 7694cfcf..87a4d633 100644 --- a/internal/pkg/core/uploader/uploader.go +++ b/internal/pkg/core/uploader/uploader.go @@ -9,13 +9,13 @@ type Uploader interface { // 上传证书。 // // 入参: - // - ctx: - // - certPem:证书 PEM 内容 - // - privkeyPem:私钥 PEM 内容 + // - ctx:上下文。 + // - certPem:证书 PEM 内容。 + // - privkeyPem:私钥 PEM 内容。 // // 出参: - // - res: - // - err: + // - res:上传结果。 + // - err: 错误。 Upload(ctx context.Context, certPem string, privkeyPem string) (res *UploadResult, err error) } diff --git a/internal/pkg/core/uploader/uploader_aliyun_cas.go b/internal/pkg/core/uploader/uploader_aliyun_cas.go index 95ef9c57..64d2e94c 100644 --- a/internal/pkg/core/uploader/uploader_aliyun_cas.go +++ b/internal/pkg/core/uploader/uploader_aliyun_cas.go @@ -26,8 +26,12 @@ type AliyunCASUploader struct { sdkRuntime *util.RuntimeOptions } -func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (*AliyunCASUploader, error) { - client, err := (&AliyunCASUploader{config: config}).createSdkClient() +func NewAliyunCASUploader(config *AliyunCASUploaderConfig) (Uploader, error) { + client, err := (&AliyunCASUploader{}).createSdkClient( + config.Region, + config.AccessKeyId, + config.AccessKeySecret, + ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -98,11 +102,11 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP if listUserCertificateOrderResp.Body.CertificateOrderList == nil || len(listUserCertificateOrderResp.Body.CertificateOrderList) < int(listUserCertificateOrderLimit) { break - } - - listUserCertificateOrderPage += 1 - if listUserCertificateOrderPage > 99 { // 避免死循环 - break + } else { + listUserCertificateOrderPage += 1 + if listUserCertificateOrderPage > 99 { // 避免死循环 + break + } } } @@ -129,10 +133,7 @@ func (u *AliyunCASUploader) Upload(ctx context.Context, certPem string, privkeyP }, nil } -func (u *AliyunCASUploader) createSdkClient() (*cas20200407.Client, error) { - region := u.config.Region - accessKeyId := u.config.AccessKeyId - accessKeySecret := u.config.AccessKeySecret +func (u *AliyunCASUploader) createSdkClient(region, accessKeyId, accessKeySecret string) (*cas20200407.Client, error) { if region == "" { region = "cn-hangzhou" // CAS 服务默认区域:华东一杭州 } diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go index 859b844a..090362af 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go +++ b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go @@ -6,19 +6,22 @@ import ( "time" "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" hcElb "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3" hcElbModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/model" hcElbRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/elb/v3/region" + hcIam "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3" + hcIamModel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/model" + hcIamRegion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/iam/v3/region" "github.com/usual2970/certimate/internal/pkg/utils/cast" "github.com/usual2970/certimate/internal/pkg/utils/x509" ) type HuaweiCloudELBUploaderConfig struct { - Region string `json:"region"` - ProjectId string `json:"projectId"` AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` + Region string `json:"region"` } type HuaweiCloudELBUploader struct { @@ -26,8 +29,12 @@ type HuaweiCloudELBUploader struct { sdkClient *hcElb.ElbClient } -func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (*HuaweiCloudELBUploader, error) { - client, err := (&HuaweiCloudELBUploader{config: config}).createSdkClient() +func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, error) { + client, err := (&HuaweiCloudELBUploader{}).createSdkClient( + config.AccessKeyId, + config.SecretAccessKey, + config.Region, + ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -87,13 +94,20 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) { break + } else { + listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker + listCertificatesPage++ + if listCertificatesPage >= 9 { // 避免死循环 + break + } } + } - listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker - listCertificatesPage++ - if listCertificatesPage >= 9 { // 避免死循环 - break - } + // 获取项目 ID + // REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html + projectId, err := u.getSdkProjectId(u.config.Region, u.config.AccessKeyId, u.config.SecretAccessKey) + if err != nil { + return nil, fmt.Errorf("failed to get SDK project id: %w", err) } // 生成新证书名(需符合华为云命名规则) @@ -105,7 +119,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri createCertificateReq := &hcElbModel.CreateCertificateRequest{ Body: &hcElbModel.CreateCertificateRequestBody{ Certificate: &hcElbModel.CreateCertificateOption{ - ProjectId: cast.StringPtr(u.config.ProjectId), + ProjectId: cast.StringPtr(projectId), Name: cast.StringPtr(certName), Certificate: cast.StringPtr(certPem), PrivateKey: cast.StringPtr(privkeyPem), @@ -125,10 +139,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri }, nil } -func (u *HuaweiCloudELBUploader) createSdkClient() (*hcElb.ElbClient, error) { - region := u.config.Region - accessKeyId := u.config.AccessKeyId - secretAccessKey := u.config.SecretAccessKey +func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { if region == "" { region = "cn-north-4" // ELB 服务默认区域:华北四北京 } @@ -157,3 +168,47 @@ func (u *HuaweiCloudELBUploader) createSdkClient() (*hcElb.ElbClient, error) { client := hcElb.NewElbClient(hcClient) return client, nil } + +func (u *HuaweiCloudELBUploader) getSdkProjectId(accessKeyId, secretAccessKey, region string) (string, error) { + if region == "" { + region = "cn-north-4" // IAM 服务默认区域:华北四北京 + } + + auth, err := global.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + SafeBuild() + if err != nil { + return "", err + } + + hcRegion, err := hcIamRegion.SafeValueOf(region) + if err != nil { + return "", err + } + + hcClient, err := hcIam.IamClientBuilder(). + WithRegion(hcRegion). + WithCredential(auth). + SafeBuild() + if err != nil { + return "", err + } + + client := hcIam.NewIamClient(hcClient) + if err != nil { + return "", err + } + + request := &hcIamModel.KeystoneListProjectsRequest{ + Name: ®ion, + } + response, err := client.KeystoneListProjects(request) + if err != nil { + return "", err + } else if response.Projects == nil || len(*response.Projects) == 0 { + return "", fmt.Errorf("no project found") + } + + return (*response.Projects)[0].Id, nil +} diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go index f397ca29..2b09ca19 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go +++ b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go @@ -15,9 +15,9 @@ import ( ) type HuaweiCloudSCMUploaderConfig struct { - Region string `json:"region"` AccessKeyId string `json:"accessKeyId"` SecretAccessKey string `json:"secretAccessKey"` + Region string `json:"region"` } type HuaweiCloudSCMUploader struct { @@ -25,8 +25,12 @@ type HuaweiCloudSCMUploader struct { sdkClient *hcScm.ScmClient } -func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (*HuaweiCloudSCMUploader, error) { - client, err := (&HuaweiCloudSCMUploader{config: config}).createSdkClient() +func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, error) { + client, err := (&HuaweiCloudSCMUploader{}).createSdkClient( + config.AccessKeyId, + config.SecretAccessKey, + config.Region, + ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -99,12 +103,12 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) { break - } - - listCertificatesOffset += listCertificatesLimit - listCertificatesPage += 1 - if listCertificatesPage > 99 { // 避免死循环 - break + } else { + listCertificatesOffset += listCertificatesLimit + listCertificatesPage += 1 + if listCertificatesPage > 99 { // 避免死循环 + break + } } } @@ -133,10 +137,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri }, nil } -func (u *HuaweiCloudSCMUploader) createSdkClient() (*hcScm.ScmClient, error) { - region := u.config.Region - accessKeyId := u.config.AccessKeyId - secretAccessKey := u.config.SecretAccessKey +func (u *HuaweiCloudSCMUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) { if region == "" { region = "cn-north-4" // SCM 服务默认区域:华北四北京 } diff --git a/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go b/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go index e099fe1a..2a34e5e6 100644 --- a/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go +++ b/internal/pkg/core/uploader/uploader_tencentcloud_ssl.go @@ -23,8 +23,12 @@ type TencentCloudSSLUploader struct { sdkClient *tcSsl.Client } -func NewTencentCloudSSLUploader(config *TencentCloudSSLUploaderConfig) (*TencentCloudSSLUploader, error) { - client, err := (&TencentCloudSSLUploader{config: config}).createSdkClient() +func NewTencentCloudSSLUploader(config *TencentCloudSSLUploaderConfig) (Uploader, error) { + client, err := (&TencentCloudSSLUploader{}).createSdkClient( + config.Region, + config.SecretId, + config.SecretKey, + ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -73,10 +77,7 @@ func (u *TencentCloudSSLUploader) Upload(ctx context.Context, certPem string, pr }, nil } -func (u *TencentCloudSSLUploader) createSdkClient() (*tcSsl.Client, error) { - region := u.config.Region - secretId := u.config.SecretId - secretKey := u.config.SecretKey +func (u *TencentCloudSSLUploader) createSdkClient(region, secretId, secretKey string) (*tcSsl.Client, error) { if region == "" { region = "ap-guangzhou" // SSL 服务默认区域:广州 } diff --git a/internal/pkg/utils/x509/x509.go b/internal/pkg/utils/x509/x509.go index f5f1e05b..0239df69 100644 --- a/internal/pkg/utils/x509/x509.go +++ b/internal/pkg/utils/x509/x509.go @@ -13,8 +13,8 @@ import ( // - certPem: 证书 PEM 内容。 // // 出参: -// - cert: -// - err: +// - cert: x509.Certificate 对象。 +// - err: 错误。 func ParseCertificateFromPEM(certPem string) (cert *x509.Certificate, err error) { pemData := []byte(certPem) diff --git a/ui/src/components/certimate/DeployEdit.tsx b/ui/src/components/certimate/DeployEdit.tsx index 0b22fcd2..11903b79 100644 --- a/ui/src/components/certimate/DeployEdit.tsx +++ b/ui/src/components/certimate/DeployEdit.tsx @@ -6,7 +6,7 @@ type DeployEditContext = { deploy: DeployConfig; error: Record; setDeploy: (deploy: DeployConfig) => void; - setError: (error: Record) => void; + setError: (error: Record) => void; }; export const Context = createContext({} as DeployEditContext); diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 24149ff5..35a7345c 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -15,6 +15,7 @@ import DeployToTencentCDN from "./DeployToTencentCDN"; import DeployToTencentCLB from "./DeployToTencentCLB"; import DeployToTencentCOS from "./DeployToTencentCOS"; import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; +import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB"; import DeployToQiniuCDN from "./DeployToQiniuCDN"; import DeployToSSH from "./DeployToSSH"; import DeployToWebhook from "./DeployToWebhook"; @@ -83,7 +84,7 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro return true; } - return item.configType === locDeployConfig.type.split("-")[0]; + return item.configType === deployTargetsMap.get(locDeployConfig.type)?.provider; }); const handleSaveClick = () => { @@ -129,6 +130,9 @@ const DeployEditDialog = ({ trigger, deployConfig, onSave }: DeployEditDialogPro case "huaweicloud-cdn": childComponent = ; break; + case "huaweicloud-elb": + childComponent = ; + break; case "qiniu-cdn": childComponent = ; break; diff --git a/ui/src/components/certimate/DeployToAliyunCDN.tsx b/ui/src/components/certimate/DeployToAliyunCDN.tsx index d6735473..074f27a0 100644 --- a/ui/src/components/certimate/DeployToAliyunCDN.tsx +++ b/ui/src/components/certimate/DeployToAliyunCDN.tsx @@ -12,6 +12,17 @@ const DeployToAliyunCDN = () => { const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + domain: "", + }, + }); + } + }, []); + useEffect(() => { setError({}); }, []); diff --git a/ui/src/components/certimate/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx index 7a79da2c..ccfcc870 100644 --- a/ui/src/components/certimate/DeployToAliyunOSS.tsx +++ b/ui/src/components/certimate/DeployToAliyunOSS.tsx @@ -8,9 +8,22 @@ import { Label } from "@/components/ui/label"; import { useDeployEditContext } from "./DeployEdit"; const DeployToAliyunOSS = () => { + const { t } = useTranslation(); + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); - const { t } = useTranslation(); + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + endpoint: "oss-cn-hangzhou.aliyuncs.com", + bucket: "", + domain: "", + }, + }); + } + }, []); useEffect(() => { setError({}); @@ -32,11 +45,11 @@ const DeployToAliyunOSS = () => { }, [data]); useEffect(() => { - const bucketResp = bucketSchema.safeParse(data.config?.domain); - if (!bucketResp.success) { + const resp = bucketSchema.safeParse(data.config?.bucket); + if (!resp.success) { setError({ ...error, - bucket: JSON.parse(bucketResp.error.message)[0].message, + bucket: JSON.parse(resp.error.message)[0].message, }); } else { setError({ @@ -44,35 +57,22 @@ const DeployToAliyunOSS = () => { bucket: "", }); } - }, []); - - useEffect(() => { - if (!data.id) { - setDeploy({ - ...data, - config: { - endpoint: "oss-cn-hangzhou.aliyuncs.com", - bucket: "", - domain: "", - }, - }); - } - }, []); + }, [data]); const domainSchema = z.string().regex(/^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { message: t("common.errmsg.domain_invalid"), }); const bucketSchema = z.string().min(1, { - message: t("domain.deployment.form.oss_bucket.placeholder"), + message: t("domain.deployment.form.aliyun_oss_bucket.placeholder"), }); return (
- + { @@ -91,9 +91,9 @@ const DeployToAliyunOSS = () => {
- + { diff --git a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx index 4e61c652..bdf968c4 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx @@ -12,6 +12,18 @@ const DeployToHuaweiCloudCDN = () => { const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-north-1", + domain: "", + }, + }); + } + }, []); + useEffect(() => { setError({}); }, []); @@ -37,6 +49,23 @@ const DeployToHuaweiCloudCDN = () => { return (
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+
{ className="w-full mt-1" value={data?.config?.domain} onChange={(e) => { - const temp = e.target.value; - - const resp = domainSchema.safeParse(temp); - if (!resp.success) { - setError({ - ...error, - domain: JSON.parse(resp.error.message)[0].message, - }); - } else { - setError({ - ...error, - domain: "", - }); - } - const newData = produce(data, (draft) => { - if (!draft.config) { - draft.config = {}; - } - draft.config.domain = temp; + draft.config ??= {}; + draft.config.domain = e.target.value?.trim(); }); setDeploy(newData); }} diff --git a/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx new file mode 100644 index 00000000..9cb5e686 --- /dev/null +++ b/ui/src/components/certimate/DeployToHuaweiCloudELB.tsx @@ -0,0 +1,190 @@ +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 DeployToHuaweiCloudCDN = () => { + const { t } = useTranslation(); + + const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + region: "cn-north-1", + resourceType: "", + certificateId: "", + loadbalancerId: "", + listenerId: "", + }, + }); + } + }, []); + + useEffect(() => { + setError({}); + }, []); + + const formSchema = z + .object({ + region: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_region.placeholder")), + resourceType: z.string().min(1, t("domain.deployment.form.huaweicloud_elb_resource_type.placeholder")), + certificateId: z.string().optional(), + loadbalancerId: z.string().optional(), + listenerId: z.string().optional(), + }) + .refine((data) => (data.resourceType === "certificate" ? !!data.certificateId?.trim() : true), { + message: t("domain.deployment.form.huaweicloud_elb_certificate_id.placeholder"), + path: ["certificateId"], + }) + .refine((data) => (data.resourceType === "loadbalancer" ? !!data.certificateId?.trim() : true), { + message: t("domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder"), + path: ["loadbalancerId"], + }) + .refine((data) => (data.resourceType === "listener" ? !!data.listenerId?.trim() : true), { + message: t("domain.deployment.form.huaweicloud_elb_listener_id.placeholder"), + path: ["listenerId"], + }); + + useEffect(() => { + const res = formSchema.safeParse(data.config); + if (!res.success) { + setError({ + ...error, + region: res.error.errors.find((e) => e.path[0] === "region")?.message, + resourceType: res.error.errors.find((e) => e.path[0] === "resourceType")?.message, + certificateId: res.error.errors.find((e) => e.path[0] === "certificateId")?.message, + loadbalancerId: res.error.errors.find((e) => e.path[0] === "loadbalancerId")?.message, + listenerId: res.error.errors.find((e) => e.path[0] === "listenerId")?.message, + }); + } else { + setError({ + ...error, + region: undefined, + resourceType: undefined, + certificateId: undefined, + loadbalancerId: undefined, + listenerId: undefined, + }); + } + }, [data]); + + return ( +
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.region}
+
+ +
+ + +
{error?.resourceType}
+
+ + {data?.config?.resourceType === "certificate" ? ( +
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.certificateId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.certificateId}
+
+ ) : ( + <> + )} + + {data?.config?.resourceType === "loadbalancer" ? ( +
+ + { + 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.listenerId = e.target.value?.trim(); + }); + setDeploy(newData); + }} + /> +
{error?.listenerId}
+
+ ) : ( + <> + )} +
+ ); +}; + +export default DeployToHuaweiCloudCDN; diff --git a/ui/src/components/certimate/DeployToKubernetesSecret.tsx b/ui/src/components/certimate/DeployToKubernetesSecret.tsx index c5129324..c7b8e2a8 100644 --- a/ui/src/components/certimate/DeployToKubernetesSecret.tsx +++ b/ui/src/components/certimate/DeployToKubernetesSecret.tsx @@ -8,13 +8,8 @@ import { useDeployEditContext } from "./DeployEdit"; const DeployToKubernetesSecret = () => { const { t } = useTranslation(); - const { setError } = useDeployEditContext(); - useEffect(() => { - setError({}); - }, []); - - const { deploy: data, setDeploy } = useDeployEditContext(); + const { deploy: data, setDeploy, setError } = useDeployEditContext(); useEffect(() => { if (!data.id) { @@ -30,6 +25,10 @@ const DeployToKubernetesSecret = () => { } }, []); + useEffect(() => { + setError({}); + }, []); + return ( <>
diff --git a/ui/src/components/certimate/DeployToQiniuCDN.tsx b/ui/src/components/certimate/DeployToQiniuCDN.tsx index 8b8f0509..508939cd 100644 --- a/ui/src/components/certimate/DeployToQiniuCDN.tsx +++ b/ui/src/components/certimate/DeployToQiniuCDN.tsx @@ -12,6 +12,17 @@ const DeployToQiniuCDN = () => { const { deploy: data, setDeploy, error, setError } = useDeployEditContext(); + useEffect(() => { + if (!data.id) { + setDeploy({ + ...data, + config: { + domain: "", + }, + }); + } + }, []); + useEffect(() => { setError({}); }, []); diff --git a/ui/src/components/certimate/DeployToTencentCLB.tsx b/ui/src/components/certimate/DeployToTencentCLB.tsx index ff50b8fe..80c6e761 100644 --- a/ui/src/components/certimate/DeployToTencentCLB.tsx +++ b/ui/src/components/certimate/DeployToTencentCLB.tsx @@ -76,7 +76,6 @@ const DeployToTencentCLB = () => { } }, []); - useEffect(() => { if (!data.id) { setDeploy({ @@ -92,7 +91,7 @@ const DeployToTencentCLB = () => { }, []); const regionSchema = z.string().regex(/^ap-[a-z]+$/, { - message: t("domain.deployment.form.clb_region.placeholder"), + message: t("domain.deployment.form.tencent_clb_region.placeholder"), }); const domainSchema = z.string().regex(/^$|^(?:\*\.)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,}$/, { @@ -100,19 +99,19 @@ const DeployToTencentCLB = () => { }); const clbIdSchema = z.string().regex(/^lb-[a-zA-Z0-9]{8}$/, { - message: t("domain.deployment.form.clb_id.placeholder"), + message: t("domain.deployment.form.tencent_clb_id.placeholder"), }); const lsnIdSchema = z.string().regex(/^lbl-.{8}$/, { - message: t("domain.deployment.form.clb_listener.placeholder"), + message: t("domain.deployment.form.tencent_clb_listener.placeholder"), }); return (
- + { @@ -144,9 +143,9 @@ const DeployToTencentCLB = () => {
- + { @@ -178,9 +177,9 @@ const DeployToTencentCLB = () => {
- + { @@ -212,9 +211,9 @@ const DeployToTencentCLB = () => {
- + { diff --git a/ui/src/components/certimate/DeployToTencentCOS.tsx b/ui/src/components/certimate/DeployToTencentCOS.tsx index 9aae611d..7fcb5e2a 100644 --- a/ui/src/components/certimate/DeployToTencentCOS.tsx +++ b/ui/src/components/certimate/DeployToTencentCOS.tsx @@ -89,9 +89,9 @@ const DeployToTencentCOS = () => { return (
- + { @@ -123,9 +123,9 @@ const DeployToTencentCOS = () => {
- + { diff --git a/ui/src/domain/domain.ts b/ui/src/domain/domain.ts index cc67a54d..97bb4ce1 100644 --- a/ui/src/domain/domain.ts +++ b/ui/src/domain/domain.ts @@ -65,6 +65,7 @@ export type Statistic = { type DeployTarget = { type: string; + provider: string; name: string; icon: string; }; @@ -78,10 +79,11 @@ export const deployTargetsMap: Map = new Map ["tencent-clb", "common.provider.tencent.clb", "/imgs/providers/tencent.svg"], ["tencent-cos", "common.provider.tencent.cos", "/imgs/providers/tencent.svg"], ["huaweicloud-cdn", "common.provider.huaweicloud.cdn", "/imgs/providers/huaweicloud.svg"], + ["huaweicloud-elb", "common.provider.huaweicloud.elb", "/imgs/providers/huaweicloud.svg"], ["qiniu-cdn", "common.provider.qiniu.cdn", "/imgs/providers/qiniu.svg"], ["local", "common.provider.local", "/imgs/providers/local.svg"], ["ssh", "common.provider.ssh", "/imgs/providers/ssh.svg"], ["webhook", "common.provider.webhook", "/imgs/providers/webhook.svg"], ["k8s-secret", "common.provider.kubernetes.secret", "/imgs/providers/k8s.svg"], - ].map(([type, name, icon]) => [type, { type, name, icon }]) + ].map(([type, name, icon]) => [type, { type, provider: type.split("-")[0], name, icon }]) ); diff --git a/ui/src/i18n/locales/en/nls.common.json b/ui/src/i18n/locales/en/nls.common.json index 0f142747..872ef19b 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -56,14 +56,15 @@ "common.provider.aliyun.oss": "Alibaba Cloud - OSS", "common.provider.aliyun.cdn": "Alibaba Cloud - CDN", "common.provider.aliyun.dcdn": "Alibaba Cloud - DCDN", - "common.provider.tencent": "Tencent", - "common.provider.tencent.cdn": "Tencent - CDN", - "common.provider.tencent.clb": "Tencent - CLB", - "common.provider.tencent.cos": "Tencent - COS", + "common.provider.tencent": "Tencent Cloud", + "common.provider.tencent.cdn": "Tencent Cloud - CDN", + "common.provider.tencent.clb": "Tencent Cloud - CLB", + "common.provider.tencent.cos": "Tencent Cloud - COS", "common.provider.huaweicloud": "Huawei Cloud", "common.provider.huaweicloud.cdn": "Huawei Cloud - CDN", - "common.provider.qiniu": "Qiniu", - "common.provider.qiniu.cdn": "Qiniu - CDN", + "common.provider.huaweicloud.elb": "Huawei Cloud - ELB", + "common.provider.qiniu": "Qiniu Cloud", + "common.provider.qiniu.cdn": "Qiniu Cloud - CDN", "common.provider.aws": "AWS", "common.provider.cloudflare": "Cloudflare", "common.provider.namesilo": "Namesilo", diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index c1ead943..80f1a4d7 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -54,21 +54,38 @@ "domain.deployment.form.access.label": "Access Configuration", "domain.deployment.form.access.placeholder": "Please select provider authorization configuration", "domain.deployment.form.access.list": "Provider Authorization Configurations", - "domain.deployment.form.cos_region.label": "Region", - "domain.deployment.form.cos_region.placeholder": "Please enter region, e.g. ap-guangzhou", - "domain.deployment.form.cos_bucket.label": "Bucket", - "domain.deployment.form.cos_bucket.placeholder": "Please enter bucket, e.g. example-1250000000", - "domain.deployment.form.clb_region.label": "region(please distinguish between region and availability zone)", - "domain.deployment.form.clb_region.placeholder": "Please enter region, e.g. ap-guangzhou", - "domain.deployment.form.clb_id.label": "CLB id", - "domain.deployment.form.clb_id.placeholder": "Please enter CLB id, e.g. lb-xxxxxxxx", - "domain.deployment.form.clb_listener.label": "Listener id", - "domain.deployment.form.clb_listener.placeholder": "Please enter listener id, e.g. lbl-xxxxxxxx. The specific listener should have set the corresponding domain HTTPS forwarding, and the original certificate domain should be consistent with the certificate to be deployed.", - "domain.deployment.form.clb_domain.label": "Deploy to domain (Wildcard domain is also supported)", - "domain.deployment.form.clb_domain.placeholder": "Please enter domain to be deployed. If SNI is not enabled, you can leave it blank.", "domain.deployment.form.domain.label": "Deploy to domain (Single domain only, not wildcard domain)", "domain.deployment.form.domain.label.wildsupported": "Deploy to domain (Wildcard domain is also supported)", "domain.deployment.form.domain.placeholder": "Please enter domain to be deployed", + "domain.deployment.form.aliyun_oss_endpoint.label": "Endpoint", + "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.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", + "domain.deployment.form.tencent_cos_bucket.placeholder": "Please enter bucket", + "domain.deployment.form.tencent_clb_region.label": "Region", + "domain.deployment.form.tencent_clb_region.placeholder": "Please enter region (e.g. ap-guangzhou)", + "domain.deployment.form.tencent_clb_id.label": "CLB ID", + "domain.deployment.form.tencent_clb_id.placeholder": "Please enter CLB ID (e.g. lb-xxxxxxxx)", + "domain.deployment.form.tencent_clb_listener.label": "Listener ID", + "domain.deployment.form.tencent_clb_listener.placeholder": "Please enter listener ID (e.g. lbl-xxxxxxxx). The specific listener should have set the corresponding domain HTTPS forwarding, and the original certificate domain should be consistent with the certificate to be deployed.", + "domain.deployment.form.tencent_clb_domain.label": "Deploy to domain (Wildcard domain is also supported)", + "domain.deployment.form.tencent_clb_domain.placeholder": "Please enter domain to be deployed. If SNI is not enabled, you can leave it blank.", + "domain.deployment.form.huaweicloud_elb_region.label": "Region", + "domain.deployment.form.huaweicloud_elb_region.placeholder": "Please enter region (e.g. cn-north-1)", + "domain.deployment.form.huaweicloud_elb_resource_type.label": "Resource Type", + "domain.deployment.form.huaweicloud_elb_resource_type.placeholder": "Please select ELB resource type", + "domain.deployment.form.huaweicloud_elb_resource_type.option.certificate.label": "ELB Certificate", + "domain.deployment.form.huaweicloud_elb_resource_type.option.loadbalancer.label": "ELB LoadBalancer", + "domain.deployment.form.huaweicloud_elb_resource_type.option.listener.label": "ELB Listener", + "domain.deployment.form.huaweicloud_elb_certificate_id.label": "Certificate ID", + "domain.deployment.form.huaweicloud_elb_certificate_id.placeholder": "Please enter ELB certificate ID", + "domain.deployment.form.huaweicloud_elb_loadbalancer_id.label": "LoadBalancer ID", + "domain.deployment.form.huaweicloud_elb_loadbalancer_id.placeholder": "Please enter ELB loadbalancer ID", + "domain.deployment.form.huaweicloud_elb_listener_id.label": "Listener ID", + "domain.deployment.form.huaweicloud_elb_listener_id.placeholder": "Please enter ELB listener ID", "domain.deployment.form.ssh_key_path.label": "Private Key Save Path", "domain.deployment.form.ssh_key_path.placeholder": "Please enter private key save path", "domain.deployment.form.ssh_cert_path.label": "Certificate Save Path", @@ -77,10 +94,6 @@ "domain.deployment.form.ssh_pre_command.placeholder": "Command to be executed before deploying the certificate", "domain.deployment.form.ssh_command.label": "Command", "domain.deployment.form.ssh_command.placeholder": "Please enter command", - "domain.deployment.form.oss_endpoint.label": "Endpoint", - "domain.deployment.form.oss_endpoint.placeholder": "Please enter endpoint", - "domain.deployment.form.oss_bucket.label": "Bucket", - "domain.deployment.form.oss_bucket.placeholder": "Please enter bucket", "domain.deployment.form.k8s_namespace.label": "Namespace", "domain.deployment.form.k8s_namespace.placeholder": "Please enter namespace", "domain.deployment.form.k8s_secret_name.label": "Secret Name", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 6a27105c..818807ce 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -51,17 +51,17 @@ "common.errmsg.host_invalid": "请输入正确的域名或 IP 地址", "common.errmsg.ip_invalid": "请输入正确的 IP 地址", "common.errmsg.url_invalid": "请输入正确的 URL", - - "common.provider.tencent": "腾讯云", - "common.provider.tencent.cdn": "腾讯云 - CDN", - "common.provider.tencent.clb": "腾讯云 - CLB", - "common.provider.tencent.cos": "腾讯云 - COS", "common.provider.aliyun": "阿里云", "common.provider.aliyun.oss": "阿里云 - OSS", "common.provider.aliyun.cdn": "阿里云 - CDN", "common.provider.aliyun.dcdn": "阿里云 - DCDN", + "common.provider.tencent": "腾讯云", + "common.provider.tencent.cos": "腾讯云 - COS", + "common.provider.tencent.cdn": "腾讯云 - CDN", + "common.provider.tencent.clb": "腾讯云 - CLB", "common.provider.huaweicloud": "华为云", "common.provider.huaweicloud.cdn": "华为云 - CDN", + "common.provider.huaweicloud.elb": "华为云 - ELB", "common.provider.qiniu": "七牛云", "common.provider.qiniu.cdn": "七牛云 - CDN", "common.provider.aws": "AWS", @@ -79,4 +79,3 @@ "common.provider.telegram": "Telegram", "common.provider.lark": "飞书" } - diff --git a/ui/src/i18n/locales/zh/nls.domain.json b/ui/src/i18n/locales/zh/nls.domain.json index 430b9314..ae7f4d0f 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -54,21 +54,38 @@ "domain.deployment.form.access.label": "授权配置", "domain.deployment.form.access.placeholder": "请选择授权配置", "domain.deployment.form.access.list": "服务商授权配置列表", - "domain.deployment.form.cos_region.label": "region", - "domain.deployment.form.cos_region.placeholder": "请输入 region, 如 ap-guangzhou", - "domain.deployment.form.cos_bucket.label": "存储桶", - "domain.deployment.form.cos_bucket.placeholder": "请输入存储桶名, 如 example-1250000000", - "domain.deployment.form.clb_region.label": "region(地域, 请准确区分地域和可用区)", - "domain.deployment.form.clb_region.placeholder": "请输入 region, 如 ap-guangzhou", - "domain.deployment.form.clb_id.label": "CLB id", - "domain.deployment.form.clb_id.placeholder": "请输入CLB实例id, 如 lb-xxxxxxxx", - "domain.deployment.form.clb_listener.label": "监听器 id(对应监听器应已设置对应域名HTTPS转发, 且原证书对应域名应与待部署证书的一致)", - "domain.deployment.form.clb_listener.placeholder": "请输入监听器id, 如 lbl-xxxxxxxx", - "domain.deployment.form.clb_domain.label": "部署到域名(支持泛域名)", - "domain.deployment.form.clb_domain.placeholder": "请输入部署到的域名, 如未开启SNI, 可置空忽略此项", "domain.deployment.form.domain.label": "部署到域名(仅支持单个域名;不支持泛域名)", "domain.deployment.form.domain.label.wildsupported": "部署到域名(支持泛域名)", "domain.deployment.form.domain.placeholder": "请输入部署到的域名", + "domain.deployment.form.aliyun_oss_endpoint.label": "Endpoint", + "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.tencent_cos_region.label": "地域", + "domain.deployment.form.tencent_cos_region.placeholder": "请输入地域(如 ap-guangzhou)", + "domain.deployment.form.tencent_cos_bucket.label": "存储桶", + "domain.deployment.form.tencent_cos_bucket.placeholder": "请输入存储桶名", + "domain.deployment.form.tencent_clb_region.label": "地域", + "domain.deployment.form.tencent_clb_region.placeholder": "请输入地域(如 ap-guangzhou)", + "domain.deployment.form.tencent_clb_id.label": "负载均衡器 ID", + "domain.deployment.form.tencent_clb_id.placeholder": "请输入负载均衡器实例 ID(如 lb-xxxxxxxx)", + "domain.deployment.form.tencent_clb_listener.label": "监听器 ID(对应监听器应已设置对应域名 HTTPS 转发, 且原证书对应域名应与待部署证书的一致)", + "domain.deployment.form.tencent_clb_listener.placeholder": "请输入监听器 ID(如 lb-xxxxxxxx)", + "domain.deployment.form.tencent_clb_domain.label": "部署到域名(支持泛域名)", + "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_certificate_id.label": "证书 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_listener_id.label": "监听器 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": "证书保存路径", @@ -77,10 +94,6 @@ "domain.deployment.form.ssh_pre_command.placeholder": "在部署证书前执行的命令", "domain.deployment.form.ssh_command.label": "命令", "domain.deployment.form.ssh_command.placeholder": "请输入要执行的命令", - "domain.deployment.form.oss_endpoint.label": "Endpoint", - "domain.deployment.form.oss_endpoint.placeholder": "请输入 Endpoint", - "domain.deployment.form.oss_bucket.label": "存储桶", - "domain.deployment.form.oss_bucket.placeholder": "请输入存储桶名", "domain.deployment.form.k8s_namespace.label": "命名空间", "domain.deployment.form.k8s_namespace.placeholder": "请输入 K8S 命名空间", "domain.deployment.form.k8s_secret_name.label": "Secret 名称",