From cea6be37dc495626217c02ebd8595def4546b119 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 24 Oct 2024 20:16:23 +0800 Subject: [PATCH 1/4] feat: allow set a different region on deployment to huaweicloud cdn --- internal/deployer/huaweicloud_cdn.go | 101 ++++++++++-------- internal/pkg/core/uploader/uploader.go | 10 +- .../pkg/core/uploader/uploader_aliyun_cas.go | 23 ++-- .../core/uploader/uploader_huaweicloud_elb.go | 25 ++--- .../core/uploader/uploader_huaweicloud_scm.go | 25 ++--- .../uploader/uploader_tencentcloud_ssl.go | 13 +-- internal/pkg/utils/x509/x509.go | 4 +- .../certimate/DeployToAliyunOSS.tsx | 10 +- .../certimate/DeployToHuaweiCloudCDN.tsx | 17 +++ .../certimate/DeployToTencentCOS.tsx | 10 +- ui/src/i18n/locales/en/nls.domain.json | 18 ++-- ui/src/i18n/locales/zh/nls.domain.json | 18 ++-- 12 files changed, 153 insertions(+), 121 deletions(-) diff --git a/internal/deployer/huaweicloud_cdn.go b/internal/deployer/huaweicloud_cdn.go index b6429be3..bf87fb89 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( + option.DeployConfig.GetConfigAsString("region"), + access.AccessKeyId, + access.SecretAccessKey, + ) + 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(region, accessKeyId, secretAccessKey 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/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..5eb60d88 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_elb.go +++ b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go @@ -26,8 +26,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.Region, + config.AccessKeyId, + config.SecretAccessKey, + ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) } @@ -87,12 +91,12 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri if listCertificatesResp.Certificates == nil || len(*listCertificatesResp.Certificates) < int(listCertificatesLimit) { break - } - - listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker - listCertificatesPage++ - if listCertificatesPage >= 9 { // 避免死循环 - break + } else { + listCertificatesMarker = listCertificatesResp.PageInfo.NextMarker + listCertificatesPage++ + if listCertificatesPage >= 9 { // 避免死循环 + break + } } } @@ -125,10 +129,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(region, accessKeyId, secretAccessKey string) (*hcElb.ElbClient, error) { if region == "" { region = "cn-north-4" // ELB 服务默认区域:华北四北京 } diff --git a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go index f397ca29..30864a48 100644 --- a/internal/pkg/core/uploader/uploader_huaweicloud_scm.go +++ b/internal/pkg/core/uploader/uploader_huaweicloud_scm.go @@ -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.Region, + config.AccessKeyId, + config.SecretAccessKey, + ) 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(region, accessKeyId, secretAccessKey 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 ca467478..5bc3f287 100644 --- a/internal/pkg/utils/x509/x509.go +++ b/internal/pkg/utils/x509/x509.go @@ -12,8 +12,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/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx index 7a79da2c..160cd5a6 100644 --- a/ui/src/components/certimate/DeployToAliyunOSS.tsx +++ b/ui/src/components/certimate/DeployToAliyunOSS.tsx @@ -64,15 +64,15 @@ const DeployToAliyunOSS = () => { }); 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..738fd4a9 100644 --- a/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx +++ b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx @@ -37,6 +37,23 @@ const DeployToHuaweiCloudCDN = () => { return (
+
+ + { + const newData = produce(data, (draft) => { + draft.config ??= {}; + draft.config.region = e.target.value; + }); + setDeploy(newData); + }} + /> +
{error?.domain}
+
+
{ }); const bucketSchema = z.string().min(1, { - message: t("domain.deployment.form.cos_region.placeholder"), + message: t("domain.deployment.form.tencent_cos_region.placeholder"), }); return (
- + { @@ -91,9 +91,9 @@ const DeployToTencentCOS = () => {
- + { diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index 98d7dcce..4bd4b837 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -54,12 +54,18 @@ "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.domain.label": "Deploy to domain (Single domain only, not wildcard domain)", "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.huaweicloud_elb_region.label": "Region", + "domain.deployment.form.huaweicloud_elb_region.placeholder": "Please enter region (e.g. cn-north-1)", "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", @@ -68,10 +74,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.domain.json b/ui/src/i18n/locales/zh/nls.domain.json index 861b9259..9a670d1d 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -54,12 +54,18 @@ "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.domain.label": "部署到域名(仅支持单个域名;不支持泛域名)", "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.huaweicloud_elb_region.label": "地域", + "domain.deployment.form.huaweicloud_elb_region.placeholder": "请输入地域(如 cn-north-1)", "domain.deployment.form.ssh_key_path.label": "私钥保存路径", "domain.deployment.form.ssh_key_path.placeholder": "请输入私钥保存路径", "domain.deployment.form.ssh_cert_path.label": "证书保存路径", @@ -68,10 +74,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 名称", From ee531dd186fbb12470e0225cf0a3963e93c0a1f6 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 24 Oct 2024 20:49:51 +0800 Subject: [PATCH 2/4] fix: aliyun oss deploy config validation error --- ui/src/components/certimate/DeployToAliyunOSS.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/src/components/certimate/DeployToAliyunOSS.tsx b/ui/src/components/certimate/DeployToAliyunOSS.tsx index 160cd5a6..ea110cde 100644 --- a/ui/src/components/certimate/DeployToAliyunOSS.tsx +++ b/ui/src/components/certimate/DeployToAliyunOSS.tsx @@ -32,11 +32,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({ From af3e20709d13e67a9b6cd5c40c30ddc6f8ddf418 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 24 Oct 2024 21:42:39 +0800 Subject: [PATCH 3/4] refactor: clean code --- ui/src/components/certimate/DeployEdit.tsx | 2 +- .../certimate/DeployToAliyunCDN.tsx | 11 +++++++ .../certimate/DeployToAliyunOSS.tsx | 30 +++++++++---------- .../certimate/DeployToKubernetesSecret.tsx | 11 ++++--- .../components/certimate/DeployToQiniuCDN.tsx | 11 +++++++ 5 files changed, 43 insertions(+), 22 deletions(-) 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/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 ea110cde..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({}); @@ -44,20 +57,7 @@ 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"), 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 327edce8..e4749206 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({}); }, []); From dc720a5d999b5fd2656487b8b8700a3563c87e2a Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 24 Oct 2024 22:37:55 +0800 Subject: [PATCH 4/4] feat: add huaweicloud elb deployer --- internal/deployer/deployer.go | 3 + internal/deployer/huaweicloud_cdn.go | 4 +- internal/deployer/huaweicloud_elb.go | 365 ++++++++++++++++++ .../core/uploader/uploader_huaweicloud_elb.go | 64 ++- .../core/uploader/uploader_huaweicloud_scm.go | 6 +- .../components/certimate/DeployEditDialog.tsx | 6 +- .../certimate/DeployToHuaweiCloudCDN.tsx | 37 +- .../certimate/DeployToHuaweiCloudELB.tsx | 190 +++++++++ ui/src/domain/domain.ts | 4 +- ui/src/i18n/locales/en/nls.common.json | 1 + ui/src/i18n/locales/en/nls.domain.json | 11 + ui/src/i18n/locales/zh/nls.common.json | 8 +- ui/src/i18n/locales/zh/nls.domain.json | 11 + 13 files changed, 673 insertions(+), 37 deletions(-) create mode 100644 internal/deployer/huaweicloud_elb.go create mode 100644 ui/src/components/certimate/DeployToHuaweiCloudELB.tsx diff --git a/internal/deployer/deployer.go b/internal/deployer/deployer.go index 9d296065..8ae0014a 100644 --- a/internal/deployer/deployer.go +++ b/internal/deployer/deployer.go @@ -21,6 +21,7 @@ const ( targetTencentCDN = "tencent-cdn" targetTencentCOS = "tencent-cos" targetHuaweiCloudCDN = "huaweicloud-cdn" + targetHuaweiCloudELB = "huaweicloud-elb" targetQiniuCdn = "qiniu-cdn" targetLocal = "local" targetSSH = "ssh" @@ -110,6 +111,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 bf87fb89..f7835dcb 100644 --- a/internal/deployer/huaweicloud_cdn.go +++ b/internal/deployer/huaweicloud_cdn.go @@ -31,9 +31,9 @@ func NewHuaweiCloudCDNDeployer(option *DeployerOption) (Deployer, error) { } client, err := (&HuaweiCloudCDNDeployer{}).createSdkClient( - option.DeployConfig.GetConfigAsString("region"), access.AccessKeyId, access.SecretAccessKey, + option.DeployConfig.GetConfigAsString("region"), ) if err != nil { return nil, err @@ -119,7 +119,7 @@ func (d *HuaweiCloudCDNDeployer) Deploy(ctx context.Context) error { return nil } -func (d *HuaweiCloudCDNDeployer) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcCdn.CdnClient, error) { +func (d *HuaweiCloudCDNDeployer) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcCdn.CdnClient, error) { if region == "" { region = "cn-north-1" // CDN 服务默认区域:华北一北京 } 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_huaweicloud_elb.go b/internal/pkg/core/uploader/uploader_huaweicloud_elb.go index 5eb60d88..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 { @@ -28,9 +31,9 @@ type HuaweiCloudELBUploader struct { func NewHuaweiCloudELBUploader(config *HuaweiCloudELBUploaderConfig) (Uploader, error) { client, err := (&HuaweiCloudELBUploader{}).createSdkClient( - config.Region, config.AccessKeyId, config.SecretAccessKey, + config.Region, ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) @@ -100,6 +103,13 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri } } + // 获取项目 ID + // REF: https://support.huaweicloud.com/api-iam/iam_06_0001.html + projectId, err := u.getSdkProjectId(u.config.Region, u.config.AccessKeyId, u.config.SecretAccessKey) + if err != nil { + return nil, fmt.Errorf("failed to get SDK project id: %w", err) + } + // 生成新证书名(需符合华为云命名规则) var certId, certName string certName = fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) @@ -109,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), @@ -129,7 +139,7 @@ func (u *HuaweiCloudELBUploader) Upload(ctx context.Context, certPem string, pri }, nil } -func (u *HuaweiCloudELBUploader) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcElb.ElbClient, error) { +func (u *HuaweiCloudELBUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcElb.ElbClient, error) { if region == "" { region = "cn-north-4" // ELB 服务默认区域:华北四北京 } @@ -158,3 +168,47 @@ func (u *HuaweiCloudELBUploader) createSdkClient(region, accessKeyId, secretAcce 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 30864a48..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 { @@ -27,9 +27,9 @@ type HuaweiCloudSCMUploader struct { func NewHuaweiCloudSCMUploader(config *HuaweiCloudSCMUploaderConfig) (Uploader, error) { client, err := (&HuaweiCloudSCMUploader{}).createSdkClient( - config.Region, config.AccessKeyId, config.SecretAccessKey, + config.Region, ) if err != nil { return nil, fmt.Errorf("failed to create sdk client: %w", err) @@ -137,7 +137,7 @@ func (u *HuaweiCloudSCMUploader) Upload(ctx context.Context, certPem string, pri }, nil } -func (u *HuaweiCloudSCMUploader) createSdkClient(region, accessKeyId, secretAccessKey string) (*hcScm.ScmClient, error) { +func (u *HuaweiCloudSCMUploader) createSdkClient(accessKeyId, secretAccessKey, region string) (*hcScm.ScmClient, error) { if region == "" { region = "cn-north-4" // SCM 服务默认区域:华北四北京 } diff --git a/ui/src/components/certimate/DeployEditDialog.tsx b/ui/src/components/certimate/DeployEditDialog.tsx index 54710e49..974dc590 100644 --- a/ui/src/components/certimate/DeployEditDialog.tsx +++ b/ui/src/components/certimate/DeployEditDialog.tsx @@ -14,6 +14,7 @@ import DeployToAliyunCDN from "./DeployToAliyunCDN"; import DeployToTencentCDN from "./DeployToTencentCDN"; import DeployToTencentCOS from "./DeployToTencentCOS"; import DeployToHuaweiCloudCDN from "./DeployToHuaweiCloudCDN"; +import DeployToHuaweiCloudELB from "./DeployToHuaweiCloudELB"; import DeployToQiniuCDN from "./DeployToQiniuCDN"; import DeployToSSH from "./DeployToSSH"; import DeployToWebhook from "./DeployToWebhook"; @@ -82,7 +83,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 = () => { @@ -125,6 +126,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/DeployToHuaweiCloudCDN.tsx b/ui/src/components/certimate/DeployToHuaweiCloudCDN.tsx index 738fd4a9..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({}); }, []); @@ -46,12 +58,12 @@ const DeployToHuaweiCloudCDN = () => { onChange={(e) => { const newData = produce(data, (draft) => { draft.config ??= {}; - draft.config.region = e.target.value; + draft.config.region = e.target.value?.trim(); }); setDeploy(newData); }} /> -
{error?.domain}
+
{error?.region}
@@ -61,26 +73,9 @@ const DeployToHuaweiCloudCDN = () => { 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/domain/domain.ts b/ui/src/domain/domain.ts index 350f89a4..f8bfc691 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; }; @@ -77,10 +78,11 @@ export const deployTargetsMap: Map = new Map ["tencent-cdn", "common.provider.tencent.cdn", "/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 d3a17019..cf22bdd1 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -61,6 +61,7 @@ "common.provider.tencent.cos": "Tencent - COS", "common.provider.huaweicloud": "Huawei Cloud", "common.provider.huaweicloud.cdn": "Huawei Cloud - CDN", + "common.provider.huaweicloud.elb": "Huawei Cloud - ELB", "common.provider.qiniu": "Qiniu", "common.provider.qiniu.cdn": "Qiniu - CDN", "common.provider.aws": "AWS", diff --git a/ui/src/i18n/locales/en/nls.domain.json b/ui/src/i18n/locales/en/nls.domain.json index 4bd4b837..3a5d5ba2 100644 --- a/ui/src/i18n/locales/en/nls.domain.json +++ b/ui/src/i18n/locales/en/nls.domain.json @@ -66,6 +66,17 @@ "domain.deployment.form.tencent_cos_bucket.placeholder": "Please enter bucket", "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", diff --git a/ui/src/i18n/locales/zh/nls.common.json b/ui/src/i18n/locales/zh/nls.common.json index 212d7b2e..8a2431a4 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -52,15 +52,16 @@ "common.errmsg.ip_invalid": "请输入正确的 IP 地址", "common.errmsg.url_invalid": "请输入正确的 URL", - "common.provider.tencent": "腾讯云", - "common.provider.tencent.cdn": "腾讯云 - CDN", - "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.cdn": "腾讯云 - CDN", + "common.provider.tencent.cos": "腾讯云 - COS", "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", @@ -78,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 9a670d1d..78d77319 100644 --- a/ui/src/i18n/locales/zh/nls.domain.json +++ b/ui/src/i18n/locales/zh/nls.domain.json @@ -66,6 +66,17 @@ "domain.deployment.form.tencent_cos_bucket.placeholder": "请输入存储桶名", "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": "证书保存路径",