From 6fd59765f43eb3e56229a20c177325dee8615cbf Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 27 Aug 2026 17:14:28 +0800 Subject: [PATCH] feat(provider): new purge provider: tencentcloud gaap --- .../certmgmt/purgers/sp_tencentcloud_gaap.go | 27 +++ internal/domain/provider.go | 9 +- .../tencentcloud-gaap/tencentcloud_gaap.go | 2 +- .../tencentcloud_gaap_test.go | 44 +++++ .../purger/providers/aliyun-cas/aliyun_cas.go | 4 - .../purger/providers/aliyun-clb/aliyun_clb.go | 4 - .../tencentcloud-gaap/tencentcloud_gaap.go | 161 ++++++++++++++++++ .../tencentcloud/gaap/v20180529/client.go | 25 +++ .../tencentcloud/gaap/v20180529/models.go | 4 + .../BizPurgeNodeConfigFieldsProvider.tsx | 2 + ...deConfigFieldsProviderTencentCloudGAAP.tsx | 50 ++++++ ui/src/domain/provider.ts | 2 + .../i18n/resources/en/nls.workflow.nodes.json | 5 + .../i18n/resources/zh/nls.workflow.nodes.json | 5 + 14 files changed, 331 insertions(+), 13 deletions(-) create mode 100644 internal/certmgmt/purgers/sp_tencentcloud_gaap.go create mode 100644 pkg/core/purger/providers-tester/tencentcloud-gaap/tencentcloud_gaap_test.go create mode 100644 pkg/core/purger/providers/tencentcloud-gaap/tencentcloud_gaap.go create mode 100644 ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderTencentCloudGAAP.tsx diff --git a/internal/certmgmt/purgers/sp_tencentcloud_gaap.go b/internal/certmgmt/purgers/sp_tencentcloud_gaap.go new file mode 100644 index 000000000..0ec2381ae --- /dev/null +++ b/internal/certmgmt/purgers/sp_tencentcloud_gaap.go @@ -0,0 +1,27 @@ +package purgers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core" + prgimpl "github.com/certimate-go/certimate/pkg/core/purger/providers/tencentcloud-gaap" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.PurgeProviderTypeTencentCloudGAAP, func(options *ProviderFactoryOptions) (core.Purger, error) { + credentials := domain.AccessConfigForTencentCloud{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := prgimpl.NewPurger(&prgimpl.PurgerConfig{ + SecretId: credentials.SecretId, + SecretKey: credentials.SecretKey, + 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 d534fcd2a..8db1574f7 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -492,10 +492,11 @@ func (t PurgeProviderType) String() string { NOTICE: If you add new constant, please keep ASCII order. */ const ( - PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas") - PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb") - PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa") - PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl") + PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas") + PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb") + PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa") + PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap") + PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl") ) type NotificationProviderType string diff --git a/pkg/core/certmgr/providers/tencentcloud-gaap/tencentcloud_gaap.go b/pkg/core/certmgr/providers/tencentcloud-gaap/tencentcloud_gaap.go index f51c47b37..e288f530b 100644 --- a/pkg/core/certmgr/providers/tencentcloud-gaap/tencentcloud_gaap.go +++ b/pkg/core/certmgr/providers/tencentcloud-gaap/tencentcloud_gaap.go @@ -76,7 +76,7 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo } // 查询服务器证书列表,避免重复上传 - // REF: https://cloud.tencent.com/document/api/1364/98588 + // REF: https://cloud.tencent.com/document/api/608/36977 // REF: https://cloud.tencent.com/document/api/608/36978 describeCertificatesOffset := 0 describeCertificatesLimit := 100 diff --git a/pkg/core/purger/providers-tester/tencentcloud-gaap/tencentcloud_gaap_test.go b/pkg/core/purger/providers-tester/tencentcloud-gaap/tencentcloud_gaap_test.go new file mode 100644 index 000000000..a116828f5 --- /dev/null +++ b/pkg/core/purger/providers-tester/tencentcloud-gaap/tencentcloud_gaap_test.go @@ -0,0 +1,44 @@ +//go:build tester + +package tencentcloudgaap_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/tencentcloud-gaap" +) + +var ( + fp = tester.InitArgs("TENCENTCLOUDGAAP_") + fSecretId string + fSecretKey string +) + +func init() { + fp.DefineString(&fSecretId, "SECRETID") + fp.DefineString(&fSecretKey, "SECRETKEY") +} + +/* +Shell command to run this test: + + go test -tags=tester -v ./tencentcloud_gaap_test.go -args \ + --TENCENTCLOUDGAAP_SECRETID="your-secret-id" \ + --TENCENTCLOUDGAAP_SECRETKEY="your-secret-key" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Purge", func(t *testing.T) { + provider, err := impl.NewPurger(&impl.PurgerConfig{ + SecretId: fSecretId, + SecretKey: fSecretKey, + }) + require.NoError(t, err) + + tester.Purge(t, provider, tester.PurgeInput{}) + }) +} diff --git a/pkg/core/purger/providers/aliyun-cas/aliyun_cas.go b/pkg/core/purger/providers/aliyun-cas/aliyun_cas.go index 63ff10ee3..b5a2544c9 100644 --- a/pkg/core/purger/providers/aliyun-cas/aliyun_cas.go +++ b/pkg/core/purger/providers/aliyun-cas/aliyun_cas.go @@ -97,10 +97,6 @@ func (p *Purger) Purge(ctx context.Context, expiry time.Duration) (*PurgeResult, for _, certItem := range listCertificatesResp.Body.CertificateList { certNotAfter := time.UnixMilli(tea.Int64Value(certItem.NotAfter)) - if certNotAfter.IsZero() { - continue - } - if time.Since(certNotAfter) >= expiry { purgingCertIds = append(purgingCertIds, tea.StringValue(certItem.CertificateId)) } diff --git a/pkg/core/purger/providers/aliyun-clb/aliyun_clb.go b/pkg/core/purger/providers/aliyun-clb/aliyun_clb.go index ae21ab677..8809c3fc1 100644 --- a/pkg/core/purger/providers/aliyun-clb/aliyun_clb.go +++ b/pkg/core/purger/providers/aliyun-clb/aliyun_clb.go @@ -91,10 +91,6 @@ func (p *Purger) Purge(ctx context.Context, expiry time.Duration) (*PurgeResult, for _, certItem := range describeServerCertificatesResp.Body.ServerCertificates.ServerCertificate { certNotAfter := time.Unix(tea.Int64Value(certItem.ExpireTimeStamp), 0) - if certNotAfter.IsZero() { - continue - } - if time.Since(certNotAfter) >= expiry { purgingCertIds = append(purgingCertIds, tea.StringValue(certItem.ServerCertificateId)) } diff --git a/pkg/core/purger/providers/tencentcloud-gaap/tencentcloud_gaap.go b/pkg/core/purger/providers/tencentcloud-gaap/tencentcloud_gaap.go new file mode 100644 index 000000000..072100014 --- /dev/null +++ b/pkg/core/purger/providers/tencentcloud-gaap/tencentcloud_gaap.go @@ -0,0 +1,161 @@ +package tencentcloudgaap + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/samber/lo" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + tcerrors "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/errors" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + + tcgaap "github.com/certimate-go/certimate/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529" + + "github.com/certimate-go/certimate/pkg/core" + 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 { + // 腾讯云 SecretId。 + SecretId string `json:"secretId"` + // 腾讯云 SecretKey。 + SecretKey string `json:"secretKey"` + // 腾讯云项目 ID。 + ProjectId int64 `json:"projectId,omitempty"` + // 腾讯云接口端点。 + Endpoint string `json:"endpoint,omitempty"` +} + +type Purger struct { + config *PurgerConfig + logger *slog.Logger + sdkClient *tcgaap.Client +} + +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.SecretId, config.SecretKey, 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([]string, 0) + + // 查询服务器证书列表 + // REF: https://cloud.tencent.com/document/api/608/36977 + describeCertificatesOffset := 0 + describeCertificatesLimit := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeCertificatesReq := tcgaap.NewDescribeCertificatesRequest() + describeCertificatesReq.CertificateType = common.Int64Ptr(2) + describeCertificatesReq.Offset = common.Uint64Ptr(uint64(describeCertificatesOffset)) + describeCertificatesReq.Limit = common.Uint64Ptr(uint64(describeCertificatesLimit)) + describeCertificatesResp, err := p.sdkClient.DescribeCertificatesWithContext(ctx, describeCertificatesReq) + p.logger.Debug("sdk request 'gaap.DescribeCertificates'", slog.Any("request", describeCertificatesReq), slog.Any("response", describeCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'gaap.DescribeCertificates': %w", err) + } + + for _, certItem := range describeCertificatesResp.Response.CertificateSet { + certNotAfter := time.Unix(int64(lo.FromPtr(certItem.EndTime)), 0) + if time.Since(certNotAfter) >= expiry { + purgingCertIds = append(purgingCertIds, lo.FromPtr(certItem.CertificateId)) + } + } + + if len(describeCertificatesResp.Response.CertificateSet) < describeCertificatesLimit { + break + } + + describeCertificatesOffset += describeCertificatesLimit + } + + // 删除证书 + // REF: https://cloud.tencent.com/document/api/608/36979 + 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 string, i int) error { + if i > 0 { + if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil { + return err + } + } + + deleteCertificateReq := tcgaap.NewDeleteCertificateRequest() + deleteCertificateReq.CertificateId = common.StringPtr(certId) + deleteCertificateResp, err := p.sdkClient.DeleteCertificateWithContext(ctx, deleteCertificateReq) + p.logger.Debug("sdk request 'gaap.DeleteCertificate'", slog.Any("request", deleteCertificateReq), slog.Any("response", deleteCertificateResp)) + if err != nil { + if sdkErr, ok := err.(*tcerrors.TencentCloudSDKError); ok { + if sdkErrCode := sdkErr.Code; sdkErrCode == "ResourceNotFound" || sdkErrCode == "FailedOperation.CertificateIsUsing" { + return nil + } + } + + return fmt.Errorf("failed to execute sdk request 'gaap.DeleteCertificate': %w", err) + } + + purgedCount++ + return nil + }); err != nil { + return nil, err + } + } + + return &PurgeResult{ + Count: purgedCount, + }, nil +} + +func createSDKClient(secretId, secretKey, endpoint string) (*tcgaap.Client, error) { + credential := common.NewCredential(secretId, secretKey) + + cpf := profile.NewClientProfile() + if endpoint != "" { + cpf.HttpProfile.Endpoint = endpoint + } + + client, err := tcgaap.NewClient(credential, "", cpf) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/client.go b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/client.go index 7c3ef6b0a..315e62b7b 100644 --- a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/client.go +++ b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/client.go @@ -48,6 +48,31 @@ func (c *Client) CreateCertificateWithContext(ctx context.Context, request *Crea return } +func NewDeleteCertificateRequest() (request *DeleteCertificateRequest) { + return gaap.NewDeleteCertificateRequest() +} + +func NewDeleteCertificateResponse() (response *DeleteCertificateResponse) { + return gaap.NewDeleteCertificateResponse() +} + +func (c *Client) DeleteCertificateWithContext(ctx context.Context, request *DeleteCertificateRequest) (response *DeleteCertificateResponse, err error) { + if request == nil { + request = NewDeleteCertificateRequest() + } + c.InitBaseRequest(&request.BaseRequest, "gaap", APIVersion, "DeleteCertificate") + + if c.GetCredential() == nil { + return nil, errors.New("DeleteCertificate require credential") + } + + request.SetContext(ctx) + + response = NewDeleteCertificateResponse() + err = c.Send(request, response) + return +} + func NewDescribeCertificatesRequest() (request *DescribeCertificatesRequest) { return gaap.NewDescribeCertificatesRequest() } diff --git a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/models.go b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/models.go index 0f4c7afae..6e32e77d6 100644 --- a/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/models.go +++ b/pkg/sdk3rd-trimmed/github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/gaap/v20180529/models.go @@ -8,6 +8,10 @@ type CreateCertificateRequest = gaap.CreateCertificateRequest type CreateCertificateResponse = gaap.CreateCertificateResponse +type DeleteCertificateRequest = gaap.DeleteCertificateRequest + +type DeleteCertificateResponse = gaap.DeleteCertificateResponse + type DescribeCertificatesRequest = gaap.DescribeCertificatesRequest type DescribeCertificatesResponse = gaap.DescribeCertificatesResponse diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx index 1de8e8828..6810d0c96 100644 --- a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx @@ -5,6 +5,7 @@ import { PURGE_PROVIDERS, type PurgeProviderType } from "@/domain/provider"; import BizPurgeNodeConfigFieldsProviderAliyunCAS from "./BizPurgeNodeConfigFieldsProviderAliyunCAS"; import BizPurgeNodeConfigFieldsProviderAliyunCLB from "./BizPurgeNodeConfigFieldsProviderAliyunCLB"; import BizPurgeNodeConfigFieldsProviderAliyunESA from "./BizPurgeNodeConfigFieldsProviderAliyunESA"; +import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP"; import BizPurgeNodeConfigFieldsProviderTencentCloudSSL from "./BizPurgeNodeConfigFieldsProviderTencentCloudSSL"; const providerComponentMap: Partial>> = { @@ -15,6 +16,7 @@ const providerComponentMap: Partial { + 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(BizPurgeNodeConfigFieldsProviderTencentCloudGAAP, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 85d208792..db6350f70 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -989,6 +989,7 @@ export const PURGE_PROVIDERS = Object.freeze( ALIYUN_CAS: `${ACCESS_PROVIDERS.ALIYUN}-cas`, ALIYUN_CLB: `${ACCESS_PROVIDERS.ALIYUN}-clb`, ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`, + TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`, TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`, } as const ); @@ -1007,6 +1008,7 @@ export const purgeProvidersMap: Map ).map(([type, name]) => [ diff --git a/ui/src/i18n/resources/en/nls.workflow.nodes.json b/ui/src/i18n/resources/en/nls.workflow.nodes.json index 409d487d0..6a81613ad 100644 --- a/ui/src/i18n/resources/en/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/en/nls.workflow.nodes.json @@ -3052,6 +3052,11 @@ "placeholder": "Please enter Alibaba Cloud ESA site ID", "tooltip": "For more information, see https://esa.console.aliyun.com/siteManage/list" }, + "tencentcloud_gaap_endpoint": { + "label": "Tencent Cloud API endpoint (Optional)", + "placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)", + "tooltip": "
  • gaap.intl.tencentcloudapi.com for Tencent Cloud International
  • gaap.tencentcloudapi.com for Tencent Cloud in China (Default)
" + }, "tencentcloud_ssl_endpoint": { "label": "Tencent Cloud API endpoint (Optional)", "placeholder": "Please enter Tencent Cloud SSL API endpoint (e.g. ssl.intl.tencentcloudapi.com)", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index 3268b20c4..4ec0f2984 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -3051,6 +3051,11 @@ "placeholder": "请输入阿里云 ESA 站点 ID", "tooltip": "这是什么?请参阅 https://esa.console.aliyun.com/siteManage/list" }, + "tencentcloud_gaap_endpoint": { + "label": "腾讯云接口端点(可选)", + "placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)", + "tooltip": "这是什么?请参阅 https://cloud.tencent.com/document/product/608/36934
国际站用户请填写 gaap.intl.tencentcloudapi.com。" + }, "tencentcloud_ssl_endpoint": { "label": "腾讯云接口端点(可选)", "placeholder": "请输入腾讯云 SSL 接口端点(例如:ssl.tencentcloudapi.com)",