feat(provider): new purge provider: tencentcloud gaap

This commit is contained in:
Fu Diwei
2026-08-27 17:14:28 +08:00
parent b21823aa0b
commit 6fd59765f4
14 changed files with 331 additions and 13 deletions
@@ -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
})
}
+5 -4
View File
@@ -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
@@ -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
@@ -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{})
})
}
@@ -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))
}
@@ -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))
}
@@ -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
}
@@ -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()
}
@@ -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
@@ -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<Record<PurgeProviderType, React.ComponentType<any>>> = {
@@ -15,6 +16,7 @@ const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentTyp
[PURGE_PROVIDERS.ALIYUN_CAS]: BizPurgeNodeConfigFieldsProviderAliyunCAS,
[PURGE_PROVIDERS.ALIYUN_CLB]: BizPurgeNodeConfigFieldsProviderAliyunCLB,
[PURGE_PROVIDERS.ALIYUN_ESA]: BizPurgeNodeConfigFieldsProviderAliyunESA,
[PURGE_PROVIDERS.TENCENTCLOUD_GAAP]: BizPurgeNodeConfigFieldsProviderTencentCloudGAAP,
[PURGE_PROVIDERS.TENCENTCLOUD_SSL]: BizPurgeNodeConfigFieldsProviderTencentCloudSSL,
};
@@ -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 BizPurgeNodeConfigFieldsProviderTencentCloudGAAP = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const initialValues = getInitialValues();
return (
<>
<Form.Item
name={[parentNamePath, "endpoint"]}
initialValue={initialValues.endpoint}
label={t("workflow_node.purge.form.tencentcloud_gaap_endpoint.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.purge.form.tencentcloud_gaap_endpoint.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.purge.form.tencentcloud_gaap_endpoint.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
endpoint: z.string().nullish(),
});
};
const _default = Object.assign(BizPurgeNodeConfigFieldsProviderTencentCloudGAAP, {
getInitialValues,
getSchema,
});
export default _default;
+2
View File
@@ -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<PurgeProvider["type"] | string, PurgeProvide
[PURGE_PROVIDERS.ALIYUN_CAS, "provider.aliyun_cas"],
[PURGE_PROVIDERS.ALIYUN_CLB, "provider.aliyun_clb"],
[PURGE_PROVIDERS.ALIYUN_ESA, "provider.aliyun_esa"],
[PURGE_PROVIDERS.TENCENTCLOUD_GAAP, "provider.tencentcloud_gaap"],
[PURGE_PROVIDERS.TENCENTCLOUD_SSL, "provider.tencentcloud_ssl"],
] satisfies Array<[PurgeProviderType, string, "builtin"] | [PurgeProviderType, string]>
).map(([type, name]) => [
@@ -3052,6 +3052,11 @@
"placeholder": "Please enter Alibaba Cloud ESA site ID",
"tooltip": "For more information, see <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
},
"tencentcloud_gaap_endpoint": {
"label": "Tencent Cloud API endpoint (Optional)",
"placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.intl.tencentcloudapi.com)",
"tooltip": "<ul style=\"list-style: disc;\"><li><strong>gaap.intl.tencentcloudapi.com</strong> for Tencent Cloud International</li><li><strong>gaap.tencentcloudapi.com</strong> for Tencent Cloud in China (<em>Default</em>)</li></ul>"
},
"tencentcloud_ssl_endpoint": {
"label": "Tencent Cloud API endpoint (Optional)",
"placeholder": "Please enter Tencent Cloud SSL API endpoint (e.g. ssl.intl.tencentcloudapi.com)",
@@ -3051,6 +3051,11 @@
"placeholder": "请输入阿里云 ESA 站点 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://esa.console.aliyun.com/siteManage/list\" target=\"_blank\">https://esa.console.aliyun.com/siteManage/list</a>"
},
"tencentcloud_gaap_endpoint": {
"label": "腾讯云接口端点(可选)",
"placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)",
"tooltip": "这是什么?请参阅 <a href=\"https://cloud.tencent.com/document/product/608/36934\" target=\"_blank\">https://cloud.tencent.com/document/product/608/36934</a><br>国际站用户请填写 <em>gaap.intl.tencentcloudapi.com</em>。"
},
"tencentcloud_ssl_endpoint": {
"label": "腾讯云接口端点(可选)",
"placeholder": "请输入腾讯云 SSL 接口端点(例如:ssl.tencentcloudapi.com)",