feat(provider): new purge provider: volcengine certcenter

This commit is contained in:
Fu Diwei
2026-08-27 17:14:31 +08:00
parent f0ec7a12ea
commit 335f57c194
14 changed files with 400 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"
pgrimpl "github.com/certimate-go/certimate/pkg/core/purger/providers/volcengine-certcenter"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.PurgeProviderTypeVolcEngineCertCenter, func(options *ProviderFactoryOptions) (core.Purger, error) {
credentials := domain.AccessConfigForVolcEngine{}
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{
AccessKeyId: credentials.AccessKeyId,
SecretAccessKey: credentials.SecretAccessKey,
ProjectName: credentials.ProjectName,
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
})
return provider, err
})
}
+13 -12
View File
@@ -492,18 +492,19 @@ 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")
PurgeProviderTypeBaiduCloudCert = PurgeProviderType(AccessProviderTypeBaiduCloud + "-cert")
PurgeProviderTypeHuaweiCloudELB = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-elb")
PurgeProviderTypeHuaweiCloudSCM = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-scm")
PurgeProviderTypeHuaweiCloudWAF = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-waf")
PurgeProviderTypeJDCloudSSL = PurgeProviderType(AccessProviderTypeJDCloud + "-ssl")
PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap")
PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl")
PurgeProviderTypeTencentCloudTSE = PurgeProviderType(AccessProviderTypeTencentCloud + "-tse")
PurgeProviderTypeUCloudUSSL = PurgeProviderType(AccessProviderTypeUCloud + "-ussl")
PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas")
PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb")
PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa")
PurgeProviderTypeBaiduCloudCert = PurgeProviderType(AccessProviderTypeBaiduCloud + "-cert")
PurgeProviderTypeHuaweiCloudELB = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-elb")
PurgeProviderTypeHuaweiCloudSCM = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-scm")
PurgeProviderTypeHuaweiCloudWAF = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-waf")
PurgeProviderTypeJDCloudSSL = PurgeProviderType(AccessProviderTypeJDCloud + "-ssl")
PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap")
PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl")
PurgeProviderTypeTencentCloudTSE = PurgeProviderType(AccessProviderTypeTencentCloud + "-tse")
PurgeProviderTypeUCloudUSSL = PurgeProviderType(AccessProviderTypeUCloud + "-ussl")
PurgeProviderTypeVolcEngineCertCenter = PurgeProviderType(AccessProviderTypeVolcEngine + "-certcenter")
)
type NotificationProviderType string
@@ -74,7 +74,7 @@ func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*Uplo
return nil, err
}
// 查询证书列表,避免重复上传
// 获取证书列表,避免重复上传
// REF: https://www.volcengine.com/docs/6454/125709
listCertInfoPageNum := 1
listCertInfoPageSize := 100
@@ -0,0 +1,44 @@
//go:build tester
package volcenginecertcenter_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/volcengine-certcenter"
)
var (
fp = tester.InitArgs("VOLCENGINECERTCENTER_")
fAccessKeyId string
fSecretAccessKey string
)
func init() {
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
}
/*
Shell command to run this test:
go test -tags=tester -v ./volcengine_certcenter_test.go -args \
--VOLCENGINECERTCENTER_ACCESSKEYID="your-access-key-id" \
--VOLCENGINECERTCENTER_SECRETACCESSKEY="your-secret-access-key"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Purge", func(t *testing.T) {
provider, err := impl.NewPurger(&impl.PurgerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
require.NoError(t, err)
tester.Purge(t, provider, tester.PurgeInput{})
})
}
@@ -0,0 +1,172 @@
package volcenginecertcenter
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/samber/lo"
ve "github.com/volcengine/volcengine-go-sdk/volcengine"
vesession "github.com/volcengine/volcengine-go-sdk/volcengine/session"
veerr "github.com/volcengine/volcengine-go-sdk/volcengine/volcengineerr"
vecertificateservice "github.com/certimate-go/certimate/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice"
"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 {
// 火山引擎 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 火山引擎 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 火山引擎项目名称。
ProjectName string `json:"projectName,omitempty"`
// 火山引擎地域。
Region string `json:"region"`
}
type Purger struct {
config *PurgerConfig
logger *slog.Logger
sdkClient *vecertificateservice.CERTIFICATESERVICE
}
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.AccessKeyId, config.SecretAccessKey, config.Region)
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://docs.volcengine.com/docs/6638/1413343
certificateGetInstanceListPageNumber := 1
certificateGetInstanceListPageSize := 100
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
certificateGetInstanceListReq := &vecertificateservice.CertificateGetInstanceListInput{
ProjectName: lo.EmptyableToPtr(p.config.ProjectName),
PageNumber: lo.ToPtr(int32(certificateGetInstanceListPageNumber)),
PageSize: lo.ToPtr(int32(certificateGetInstanceListPageSize)),
}
certificateGetInstanceListResp, err := p.sdkClient.CertificateGetInstanceListWithContext(ctx, certificateGetInstanceListReq)
p.logger.Debug("sdk request 'certificateservice.CertificateGetInstanceList'", slog.Any("request", certificateGetInstanceListReq), slog.Any("response", certificateGetInstanceListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'certificateservice.CertificateGetInstanceList': %w", err)
}
for _, certItem := range certificateGetInstanceListResp.Instances {
certNotAfter, err := time.ParseInLocation(time.DateTime, lo.FromPtr(certItem.NotAfter), time.FixedZone("CST", 8*60*60))
if err != nil {
continue
}
if time.Since(certNotAfter) >= expiry {
purgingCertIds = append(purgingCertIds, lo.FromPtr(certItem.InstanceId))
}
}
if len(certificateGetInstanceListResp.Instances) < certificateGetInstanceListPageSize ||
certificateGetInstanceListPageNumber*certificateGetInstanceListPageSize >= int(lo.FromPtr(certificateGetInstanceListResp.TotalCount)) {
break
}
certificateGetInstanceListPageNumber++
}
// 删除证书实例
// REF: https://docs.volcengine.com/docs/6638/1412931
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
}
}
certificateDeleteInstanceReq := &vecertificateservice.CertificateDeleteInstanceInput{
InstanceId: lo.ToPtr(certId),
}
certificateDeleteInstanceResp, err := p.sdkClient.CertificateDeleteInstanceWithContext(ctx, certificateDeleteInstanceReq)
p.logger.Debug("sdk request 'certificateservice.CertificateDeleteInstance'", slog.Any("request", certificateDeleteInstanceReq), slog.Any("response", certificateDeleteInstanceResp))
if err != nil {
var sdkErr veerr.RequestFailure
if errors.As(err, &sdkErr) {
if sdkErrCode := sdkErr.Code(); sdkErrCode == "NotFound.CertificateInstance" || sdkErrCode == "Delete.Certificate" {
return nil
}
}
return fmt.Errorf("failed to execute sdk request 'certificateservice.CertificateDeleteInstance': %w", err)
}
purgedCount++
return nil
}); err != nil {
return nil, err
}
}
return &PurgeResult{
Count: purgedCount,
}, nil
}
func createSDKClient(accessKeyId, secretAccessKey, region string) (*vecertificateservice.CERTIFICATESERVICE, error) {
if region == "" {
region = "cn-beijing" // 证书中心默认区域:北京
}
config := ve.NewConfig().
WithAkSk(accessKeyId, secretAccessKey).
WithRegion(region)
session, err := vesession.NewSession(config)
if err != nil {
return nil, err
}
client := vecertificateservice.New(session, config)
return client, nil
}
@@ -0,0 +1,39 @@
package certificateservice
import (
"github.com/volcengine/volcengine-go-sdk/service/certificateservice"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
)
const opCertificateDeleteInstance = "CertificateDeleteInstance"
func (c *CERTIFICATESERVICE) CertificateDeleteInstanceRequest(input *CertificateDeleteInstanceInput) (req *request.Request, output *CertificateDeleteInstanceOutput) {
op := &request.Operation{
Name: opCertificateDeleteInstance,
HTTPMethod: "POST",
HTTPPath: "/",
}
if input == nil {
input = &CertificateDeleteInstanceInput{}
}
output = &CertificateDeleteInstanceOutput{}
req = c.newRequest(op, input, output)
req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8")
return
}
func (c *CERTIFICATESERVICE) CertificateDeleteInstanceWithContext(ctx volcengine.Context, input *CertificateDeleteInstanceInput, opts ...request.Option) (*CertificateDeleteInstanceOutput, error) {
req, out := c.CertificateDeleteInstanceRequest(input)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return out, req.Send()
}
type CertificateDeleteInstanceInput = certificateservice.CertificateDeleteInstanceInput
type CertificateDeleteInstanceOutput = certificateservice.CertificateDeleteInstanceOutput
@@ -0,0 +1,39 @@
package certificateservice
import (
"github.com/volcengine/volcengine-go-sdk/service/certificateservice"
"github.com/volcengine/volcengine-go-sdk/volcengine"
"github.com/volcengine/volcengine-go-sdk/volcengine/request"
)
const opCertificateGetInstanceList = "CertificateGetInstanceList"
func (c *CERTIFICATESERVICE) CertificateGetInstanceListRequest(input *CertificateGetInstanceListInput) (req *request.Request, output *CertificateGetInstanceListOutput) {
op := &request.Operation{
Name: opCertificateGetInstanceList,
HTTPMethod: "POST",
HTTPPath: "/",
}
if input == nil {
input = &CertificateGetInstanceListInput{}
}
output = &CertificateGetInstanceListOutput{}
req = c.newRequest(op, input, output)
req.HTTPRequest.Header.Set("Content-Type", "application/json; charset=utf-8")
return
}
func (c *CERTIFICATESERVICE) CertificateGetInstanceListWithContext(ctx volcengine.Context, input *CertificateGetInstanceListInput, opts ...request.Option) (*CertificateGetInstanceListOutput, error) {
req, out := c.CertificateGetInstanceListRequest(input)
req.SetContext(ctx)
req.ApplyOptions(opts...)
return out, req.Send()
}
type CertificateGetInstanceListInput = certificateservice.CertificateGetInstanceListInput
type CertificateGetInstanceListOutput = certificateservice.CertificateGetInstanceListOutput
@@ -11,6 +11,7 @@ import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConf
import BizPurgeNodeConfigFieldsProviderTencentCloudSSL from "./BizPurgeNodeConfigFieldsProviderTencentCloudSSL";
import BizPurgeNodeConfigFieldsProviderTencentCloudTSE from "./BizPurgeNodeConfigFieldsProviderTencentCloudTSE";
import BizPurgeNodeConfigFieldsProviderUCloudUSSL from "./BizPurgeNodeConfigFieldsProviderUCloudUSSL";
import BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter from "./BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter";
const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentType<any>>> = {
/*
@@ -26,6 +27,7 @@ const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentTyp
[PURGE_PROVIDERS.TENCENTCLOUD_SSL]: BizPurgeNodeConfigFieldsProviderTencentCloudSSL,
[PURGE_PROVIDERS.TENCENTCLOUD_TSE]: BizPurgeNodeConfigFieldsProviderTencentCloudTSE,
[PURGE_PROVIDERS.UCLOUD_USSL]: BizPurgeNodeConfigFieldsProviderUCloudUSSL,
[PURGE_PROVIDERS.VOLCENGINE_CERTCENTER]: BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter,
};
const useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {
@@ -0,0 +1,51 @@
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 BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter = () => {
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, "region"]}
initialValue={initialValues.region}
label={t("workflow_node.purge.form.volcengine_certcenter_region.label")}
rules={[formRule]}
>
<Input placeholder={t("workflow_node.purge.form.volcengine_certcenter_region.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
region: "cn-beijing",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
region: z.string().nonempty(),
});
};
const _default = Object.assign(BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter, {
getInitialValues,
getSchema,
});
export default _default;
+2
View File
@@ -998,6 +998,7 @@ export const PURGE_PROVIDERS = Object.freeze(
TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`,
TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`,
UCLOUD_USSL: `${ACCESS_PROVIDERS.UCLOUD}-ussl`,
VOLCENGINE_CERTCENTER: `${ACCESS_PROVIDERS.VOLCENGINE}-certcenter`,
} as const
);
@@ -1021,6 +1022,7 @@ export const purgeProvidersMap: Map<PurgeProvider["type"] | string, PurgeProvide
[PURGE_PROVIDERS.HUAWEICLOUD_SCM, "provider.huaweicloud_scm"],
[PURGE_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud_elb"],
[PURGE_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud_waf"],
[PURGE_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine_certcenter"],
[PURGE_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud_cert"],
[PURGE_PROVIDERS.JDCLOUD_SSL, "provider.jdcloud_ssl"],
[PURGE_PROVIDERS.UCLOUD_USSL, "provider.ucloud_ussl"],
@@ -289,6 +289,7 @@
"volcengine_alb": "Volcengine - ALB (Application Load Balancer)",
"volcengine_apig": "Volcengine - API Gateway",
"volcengine_cdn": "Volcengine - CDN (Content Delivery Network)",
"volcengine_certcenter": "Volcengine - Certificate Center",
"volcengine_certcenter_upload": "Volcengine - Upload to Certificate Center",
"volcengine_clb": "Volcengine - CLB (Classic Load Balancer)",
"volcengine_dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)",
@@ -3100,6 +3100,10 @@
"label": "UCloud API endpoint (Optional)",
"placeholder": "Please enter UCloud USSL API endpoint (e.g. api.ucloud-global.com)",
"tooltip": "<ul style=\"list-style: disc;\"><li><strong>api.ucloud-global.com</strong> for UCloud Global</li><li><strong>api.ucloud.cn</strong> for UCloud in China (<em>Default</em>)</li></ul>"
},
"volcengine_certcenter_region": {
"label": "VolcEngine region",
"placeholder": "Please enter VolcEngine Certificate Center region (e.g. cn-beijing)"
}
}
},
@@ -289,6 +289,7 @@
"volcengine_alb": "火山引擎 - 应用型负载均衡 ALB",
"volcengine_apig": "火山引擎 - API 网关",
"volcengine_cdn": "火山引擎 - 内容分发网络 CDN",
"volcengine_certcenter": "火山引擎 - 证书中心",
"volcengine_certcenter_upload": "火山引擎 - 上传到证书中心",
"volcengine_clb": "火山引擎 - 传统型负载均衡 CLB",
"volcengine_dcdn": "火山引擎 - 全站加速 DCDN",
@@ -3099,6 +3099,10 @@
"label": "优刻得接口端点(可选)",
"placeholder": "请输入优刻得 USSL 接口端点(例如:api.ucloud.cn)",
"tooltip": "这是什么?请参阅 <a href=\"https://docs.ucloud.cn/api/summary/gateway\" target=\"_blank\">https://docs.ucloud.cn/api/summary/gateway</a><br>国际站用户请填写 <em>api.ucloud-global.com</em>。"
},
"volcengine_certcenter_region": {
"label": "火山引擎服务地域",
"placeholder": "请输入火山引擎证书中心服务地域(例如:cn-beijing)"
}
}
},