From 4ded078aaf0f85508429a6f000dd8ef511137cf0 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 27 Aug 2026 17:14:30 +0800 Subject: [PATCH] feat(provider): new purge provider: ucloud ussl --- internal/certmgmt/purgers/sp_ucloud_ussl.go | 27 +++ internal/domain/provider.go | 1 + .../providers/ucloud-ualb/ucloud_ualb.go | 6 +- .../ucloud-ussl/ucloud_ussl_test.go | 46 ++++ .../providers/ucloud-ussl/ucloud_ussl.go | 213 ++++++++++++++++++ .../ucloud/ussl/api_delete_ssl_certificate.go | 41 ++++ .../BizPurgeNodeConfigFieldsProvider.tsx | 2 + ...urgeNodeConfigFieldsProviderUCloudUSSL.tsx | 50 ++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/resources/en/nls.provider.json | 1 + .../i18n/resources/en/nls.workflow.nodes.json | 5 + ui/src/i18n/resources/zh/nls.provider.json | 1 + .../i18n/resources/zh/nls.workflow.nodes.json | 5 + 13 files changed, 395 insertions(+), 5 deletions(-) create mode 100644 internal/certmgmt/purgers/sp_ucloud_ussl.go create mode 100644 pkg/core/purger/providers-tester/ucloud-ussl/ucloud_ussl_test.go create mode 100644 pkg/core/purger/providers/ucloud-ussl/ucloud_ussl.go create mode 100644 pkg/sdk3rd/ucloud/ussl/api_delete_ssl_certificate.go create mode 100644 ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderUCloudUSSL.tsx diff --git a/internal/certmgmt/purgers/sp_ucloud_ussl.go b/internal/certmgmt/purgers/sp_ucloud_ussl.go new file mode 100644 index 000000000..a33567632 --- /dev/null +++ b/internal/certmgmt/purgers/sp_ucloud_ussl.go @@ -0,0 +1,27 @@ +package purgers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + pgrimpl "github.com/certimate-go/certimate/pkg/core/purger/providers/ucloud-ussl" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.PurgeProviderTypeUCloudUSSL, func(options *ProviderFactoryOptions) (core.Purger, error) { + credentials := domain.AccessConfigForUCloud{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := pgrimpl.NewPurger(&pgrimpl.PurgerConfig{ + PrivateKey: credentials.PrivateKey, + PublicKey: credentials.PublicKey, + ProjectId: credentials.ProjectId, + Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"), + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 42ecfbdf9..01e6768af 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -500,6 +500,7 @@ const ( PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap") PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl") PurgeProviderTypeTencentCloudTSE = PurgeProviderType(AccessProviderTypeTencentCloud + "-tse") + PurgeProviderTypeUCloudUSSL = PurgeProviderType(AccessProviderTypeUCloud + "-ussl") ) type NotificationProviderType string diff --git a/pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go b/pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go index 18c7d1c90..6c9bfa0c1 100644 --- a/pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go +++ b/pkg/core/deployer/providers/ucloud-ualb/ucloud_ualb.go @@ -5,7 +5,6 @@ import ( "fmt" "log/slog" "strings" - "time" "github.com/samber/lo" "github.com/ucloud/ucloud-sdk-go/services/ulb" @@ -260,7 +259,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudLoadba return fmt.Errorf("failed to execute sdk request 'ulb.AddSSLBinding': %w", err) } - // 找出需要删除绑定的扩展证书 + // 获取扩展证书信息,并找出需要删除绑定的同域名证书 // REF: https://docs.ucloud.cn/api/ulb-api/describe_sslv2 sslIdsToDelete := make([]string, 0) for _, certItem := range cloudListenerInfo.Certificates { @@ -283,9 +282,6 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudLoadba if sslItem.Domains == d.config.Domain { sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 同域名证书需要删除 continue - } else if sslItem.NotAfter != 0 && int64(sslItem.NotAfter) < time.Now().Unix() { - sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 过期证书需要删除。TODO: remove on v0.5 - continue } } diff --git a/pkg/core/purger/providers-tester/ucloud-ussl/ucloud_ussl_test.go b/pkg/core/purger/providers-tester/ucloud-ussl/ucloud_ussl_test.go new file mode 100644 index 000000000..5cd130bfc --- /dev/null +++ b/pkg/core/purger/providers-tester/ucloud-ussl/ucloud_ussl_test.go @@ -0,0 +1,46 @@ +//go:build tester + +package ucloudussl_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + + tester "github.com/certimate-go/certimate/pkg/core/purger/providers-tester" + impl "github.com/certimate-go/certimate/pkg/core/purger/providers/ucloud-ussl" +) + +var ( + fp = tester.InitArgs("UCLOUDUSSL_") + fTestCertPath string + fTestKeyPath string + fPrivateKey string + fPublicKey string +) + +func init() { + fp.DefineString(&fPrivateKey, "PRIVATEKEY") + fp.DefineString(&fPublicKey, "PUBLICKEY") +} + +/* +Shell command to run this test: + + go test -tags=tester -v ./ucloud_ussl_test.go -args \ + --UCLOUDUSSL_PRIVATEKEY="your-private-key" \ + --UCLOUDUSSL_PUBLICKEY="your-public-key" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Purge", func(t *testing.T) { + provider, err := impl.NewPurger(&impl.PurgerConfig{ + PrivateKey: fPrivateKey, + PublicKey: fPublicKey, + }) + require.NoError(t, err) + + tester.Purge(t, provider, tester.PurgeInput{}) + }) +} diff --git a/pkg/core/purger/providers/ucloud-ussl/ucloud_ussl.go b/pkg/core/purger/providers/ucloud-ussl/ucloud_ussl.go new file mode 100644 index 000000000..710101a0f --- /dev/null +++ b/pkg/core/purger/providers/ucloud-ussl/ucloud_ussl.go @@ -0,0 +1,213 @@ +package ucloudussl + +import ( + "context" + "fmt" + "log/slog" + "strings" + "time" + + "github.com/ucloud/ucloud-sdk-go/ucloud" + "github.com/ucloud/ucloud-sdk-go/ucloud/auth" + + "github.com/certimate-go/certimate/pkg/core" + ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ussl" + xloop "github.com/certimate-go/certimate/pkg/utils/loop" + xwait "github.com/certimate-go/certimate/pkg/utils/wait" +) + +type ( + Provider = core.Purger + PurgeResult = core.PurgerPurgeResult +) + +type PurgerConfig struct { + // 优刻得 API 私钥。 + PrivateKey string `json:"privateKey"` + // 优刻得 API 公钥。 + PublicKey string `json:"publicKey"` + // 优刻得项目 ID。 + ProjectId string `json:"projectId,omitempty"` + // 优刻得接口端点。 + Endpoint string `json:"endpoint,omitempty"` +} + +type Purger struct { + config *PurgerConfig + logger *slog.Logger + sdkClient *ucloudsdk.USSLClient +} + +var _ Provider = (*Purger)(nil) + +func NewPurger(config *PurgerConfig) (*Purger, error) { + if config == nil { + return nil, fmt.Errorf("the configuration of the purger provider is nil") + } + + client, err := createSDKClient(config.PrivateKey, config.PublicKey, config.ProjectId, config.Endpoint) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + return &Purger{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (p *Purger) SetLogger(logger *slog.Logger) { + if logger == nil { + p.logger = slog.New(slog.DiscardHandler) + } else { + p.logger = logger + } +} + +func (p *Purger) Purge(ctx context.Context, expiry time.Duration) (*PurgeResult, error) { + purgingCertIds := make([]int, 0) + purgingCertId2ModeMap := make(map[int]string) + + // 查询用户托管证书列表 + // REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list + getCertificateListPage := 1 + getCertificateListPageSize := 1000 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + getCertificateListReq := p.sdkClient.NewGetCertificateListRequest() + getCertificateListReq.Mode = ucloud.String("trust") + getCertificateListReq.Sort = ucloud.String("2") + getCertificateListReq.Page = ucloud.Int(getCertificateListPage) + getCertificateListReq.PageSize = ucloud.Int(getCertificateListPageSize) + getCertificateListResp, err := p.sdkClient.GetCertificateList(getCertificateListReq) + p.logger.Debug("sdk request 'ussl.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateList': %w", err) + } + + for _, certItem := range getCertificateListResp.CertificateList { + certNotAfter := time.UnixMilli(certItem.NotAfter) + + if time.Since(certNotAfter) >= expiry { + purgingCertIds = append(purgingCertIds, certItem.CertificateID) + purgingCertId2ModeMap[certItem.CertificateID] = certItem.Mode + } + } + + if len(getCertificateListResp.CertificateList) < getCertificateListPageSize || + getCertificateListPage*getCertificateListPageSize >= getCertificateListResp.TotalCount { + break + } + + getCertificateListPage++ + } + + // 查询用户购买证书列表 + // REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list + getCertificateListPage = 1 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + getCertificateListReq := p.sdkClient.NewGetCertificateListRequest() + getCertificateListReq.Mode = ucloud.String("purchase") + getCertificateListReq.Sort = ucloud.String("2") + getCertificateListReq.Page = ucloud.Int(getCertificateListPage) + getCertificateListReq.PageSize = ucloud.Int(getCertificateListPageSize) + getCertificateListResp, err := p.sdkClient.GetCertificateList(getCertificateListReq) + p.logger.Debug("sdk request 'ussl.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateList': %w", err) + } + + for _, certItem := range getCertificateListResp.CertificateList { + certNotAfter := time.UnixMilli(certItem.NotAfter) + + if time.Since(certNotAfter) >= expiry { + purgingCertIds = append(purgingCertIds, certItem.CertificateID) + purgingCertId2ModeMap[certItem.CertificateID] = certItem.Mode + } + } + + if len(getCertificateListResp.CertificateList) < getCertificateListPageSize || + getCertificateListPage*getCertificateListPageSize >= getCertificateListResp.TotalCount { + break + } + + getCertificateListPage++ + } + + // 删除证书 + // REF: https://docs.ucloud.cn/api/usslcertificate-api/delete_ssl_certificate + purgedCount := 0 + if len(purgingCertIds) > 0 { + p.logger.Info("found certificates to purge", slog.Any("certIds", purgingCertIds)) + + if err := xloop.ForRangeAllWithContext(ctx, purgingCertIds, func(ctx context.Context, certId int, i int) error { + if i > 0 { + if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil { + return err + } + } + + deleteSSLCertificateReq := p.sdkClient.NewDeleteSSLCertificateRequest() + deleteSSLCertificateReq.CertificateMode = ucloud.String(purgingCertId2ModeMap[certId]) + deleteSSLCertificateReq.CertificateID = ucloud.Int(certId) + deleteSSLCertificateResp, err := p.sdkClient.DeleteSSLCertificate(deleteSSLCertificateReq) + p.logger.Debug("sdk request 'ussl.DeleteSSLCertificate'", slog.Any("request", deleteSSLCertificateReq), slog.Any("response", deleteSSLCertificateResp)) + if err != nil { + if deleteSSLCertificateResp != nil && deleteSSLCertificateResp.GetRetCode() == 80011 { + return nil + } + + return fmt.Errorf("failed to execute sdk request 'ussl.DeleteSSLCertificate': %w", err) + } + + purgedCount++ + return nil + }); err != nil { + return nil, err + } + } + + return &PurgeResult{ + Count: purgedCount, + }, nil +} + +func createSDKClient(privateKey, publicKey, projectId, endpoint string) (*ucloudsdk.USSLClient, error) { + if privateKey == "" { + return nil, fmt.Errorf("ucloud: invalid private key") + } + if publicKey == "" { + return nil, fmt.Errorf("ucloud: invalid public key") + } + + cfg := ucloud.NewConfig() + if projectId != "" { + cfg.ProjectId = projectId + } + if endpoint != "" { + if strings.Contains(endpoint, "://") { + cfg.BaseUrl = endpoint + } else { + cfg.BaseUrl = "https://" + endpoint + } + } + + credential := auth.NewCredential() + credential.PrivateKey = privateKey + credential.PublicKey = publicKey + + client := ucloudsdk.NewClient(&cfg, &credential) + return client, nil +} diff --git a/pkg/sdk3rd/ucloud/ussl/api_delete_ssl_certificate.go b/pkg/sdk3rd/ucloud/ussl/api_delete_ssl_certificate.go new file mode 100644 index 000000000..7b5d40e03 --- /dev/null +++ b/pkg/sdk3rd/ucloud/ussl/api_delete_ssl_certificate.go @@ -0,0 +1,41 @@ +package ussl + +import ( + "github.com/ucloud/ucloud-sdk-go/ucloud/request" + "github.com/ucloud/ucloud-sdk-go/ucloud/response" +) + +type DeleteSSLCertificateRequest struct { + request.CommonBase + + CertificateMode *string `required:"true"` + CertificateID *int `required:"true"` +} + +type DeleteSSLCertificateResponse struct { + response.CommonBase +} + +func (c *USSLClient) NewDeleteSSLCertificateRequest() *DeleteSSLCertificateRequest { + req := &DeleteSSLCertificateRequest{} + + c.Client.SetupRequest(req) + + req.SetRetryable(true) + req.SetEncoder(request.NewJSONEncoder(c.GetConfig(), c.GetCredential())) + return req +} + +func (c *USSLClient) DeleteSSLCertificate(req *DeleteSSLCertificateRequest) (*DeleteSSLCertificateResponse, error) { + var err error + var res DeleteSSLCertificateResponse + + reqCopier := *req + + err = c.Client.InvokeAction("DeleteSSLCertificate", &reqCopier, &res) + if err != nil { + return &res, err + } + + return &res, nil +} diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx index 4afce9102..b119a90f9 100644 --- a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx @@ -9,6 +9,7 @@ import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfig import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP"; import BizPurgeNodeConfigFieldsProviderTencentCloudSSL from "./BizPurgeNodeConfigFieldsProviderTencentCloudSSL"; import BizPurgeNodeConfigFieldsProviderTencentCloudTSE from "./BizPurgeNodeConfigFieldsProviderTencentCloudTSE"; +import BizPurgeNodeConfigFieldsProviderUCloudUSSL from "./BizPurgeNodeConfigFieldsProviderUCloudUSSL"; const providerComponentMap: Partial>> = { /* @@ -22,6 +23,7 @@ const providerComponentMap: Partial any; deps?: unknown[] }) => { diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderUCloudUSSL.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderUCloudUSSL.tsx new file mode 100644 index 000000000..8b92a6fef --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderUCloudUSSL.tsx @@ -0,0 +1,50 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const BizPurgeNodeConfigFieldsProviderUCloudUSSL = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return {}; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + endpoint: z.string().nullish(), + }); +}; + +const _default = Object.assign(BizPurgeNodeConfigFieldsProviderUCloudUSSL, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index e151ef147..ae339493d 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -994,6 +994,7 @@ export const PURGE_PROVIDERS = Object.freeze( TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`, TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`, TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`, + UCLOUD_USSL: `${ACCESS_PROVIDERS.UCLOUD}-ussl`, } as const ); @@ -1016,6 +1017,7 @@ export const purgeProvidersMap: Map ).map(([type, name]) => [ type, diff --git a/ui/src/i18n/resources/en/nls.provider.json b/ui/src/i18n/resources/en/nls.provider.json index 0edab55c9..4b3b80489 100644 --- a/ui/src/i18n/resources/en/nls.provider.json +++ b/ui/src/i18n/resources/en/nls.provider.json @@ -275,6 +275,7 @@ "ucloud_uewaf": "UCloud - UEWAF (Enterprise Web Application Firewall)", "ucloud_upathx": "UCloud - UPathX (Global Dynamic Acceleration)", "ucloud_us3": "UCloud - US3 (Object-based Storage)", + "ucloud_ussl": "UCloud - USSL (SSL Certificates)", "todaynic": "TodayNIC.com", "unicloud": "uniCloud (DCloud)", "unicloud_webhost": "uniCloud - Web Host", diff --git a/ui/src/i18n/resources/en/nls.workflow.nodes.json b/ui/src/i18n/resources/en/nls.workflow.nodes.json index 647b1a193..818a39a19 100644 --- a/ui/src/i18n/resources/en/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/en/nls.workflow.nodes.json @@ -3090,6 +3090,11 @@ "label": "Tencent Cloud TSE Cloud-native API gateway ID", "placeholder": "Please enter Tencent Cloud TSE Cloud-native API gateway ID", "tooltip": "For more information, see https://console.tencentcloud.com/rcc/governance" + }, + "ucloud_ussl_endpoint": { + "label": "UCloud API endpoint (Optional)", + "placeholder": "Please enter UCloud USSL API endpoint (e.g. api.ucloud-global.com)", + "tooltip": "
  • api.ucloud-global.com for UCloud Global
  • api.ucloud.cn for UCloud in China (Default)
" } } }, diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 585f39d40..384a9166d 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -276,6 +276,7 @@ "ucloud_uewaf": "优刻得 - 企业 Web 应用防火墙 UEWAF", "ucloud_upathx": "优刻得 - 全球动态加速 UPathX", "ucloud_us3": "优刻得 - 对象存储 US3", + "ucloud_ussl": "优刻得 - 证书管理 USSL", "unicloud": "uniCloud (DCloud)", "unicloud_webhost": "uniCloud - 前端网页托管", "upyun": "又拍云", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index a1d9ce9b6..d65f8ae32 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -3089,6 +3089,11 @@ "label": "腾讯云 TSE 云原生网关 ID", "placeholder": "请输入腾讯云 TSE 云原生网关 ID", "tooltip": "这是什么?请参阅 https://console.cloud.tencent.com.cn/rcc/governance" + }, + "ucloud_ussl_endpoint": { + "label": "优刻得接口端点(可选)", + "placeholder": "请输入优刻得 USSL 接口端点(例如:api.ucloud.cn)", + "tooltip": "这是什么?请参阅 https://docs.ucloud.cn/api/summary/gateway
国际站用户请填写 api.ucloud-global.com。" } } },