diff --git a/internal/certmgmt/purgers/sp_volcengine_certcenter.go b/internal/certmgmt/purgers/sp_volcengine_certcenter.go new file mode 100644 index 000000000..c201022c7 --- /dev/null +++ b/internal/certmgmt/purgers/sp_volcengine_certcenter.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/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 + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index c71cc510d..f5d371922 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -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 diff --git a/pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go b/pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go index a1a1c88ec..19bc626f6 100644 --- a/pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go +++ b/pkg/core/certmgr/providers/volcengine-cdn/volcengine_cdn.go @@ -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 diff --git a/pkg/core/purger/providers-tester/volcengine-certcenter/volcengine_certcenter_test.go b/pkg/core/purger/providers-tester/volcengine-certcenter/volcengine_certcenter_test.go new file mode 100644 index 000000000..88ee2696a --- /dev/null +++ b/pkg/core/purger/providers-tester/volcengine-certcenter/volcengine_certcenter_test.go @@ -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{}) + }) +} diff --git a/pkg/core/purger/providers/volcengine-certcenter/volcengine_certcenter.go b/pkg/core/purger/providers/volcengine-certcenter/volcengine_certcenter.go new file mode 100644 index 000000000..3b55f84e1 --- /dev/null +++ b/pkg/core/purger/providers/volcengine-certcenter/volcengine_certcenter.go @@ -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 +} diff --git a/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_delete_instance.go b/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_delete_instance.go new file mode 100644 index 000000000..a8d10e3ae --- /dev/null +++ b/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_delete_instance.go @@ -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 diff --git a/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_get_instance_list.go b/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_get_instance_list.go new file mode 100644 index 000000000..c0d5a4487 --- /dev/null +++ b/pkg/sdk3rd-trimmed/github.com/volcengine/volcengine-go-sdk/service/certificateservice/api_certificate_get_instance_list.go @@ -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 diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx index ad26e0e0c..2e7d5a4d5 100644 --- a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx @@ -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>> = { /* @@ -26,6 +27,7 @@ const providerComponentMap: Partial any; deps?: unknown[] }) => { diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter.tsx new file mode 100644 index 000000000..e281ac7a6 --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter.tsx @@ -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 ( + <> + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + region: "cn-beijing", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + region: z.string().nonempty(), + }); +}; + +const _default = Object.assign(BizPurgeNodeConfigFieldsProviderVolcEngineCertCenter, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 5dcfecfb6..636b17e73 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -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
  • api.ucloud-global.com for UCloud Global
  • api.ucloud.cn for UCloud in China (Default)
  • " + }, + "volcengine_certcenter_region": { + "label": "VolcEngine region", + "placeholder": "Please enter VolcEngine Certificate Center region (e.g. cn-beijing)" } } }, diff --git a/ui/src/i18n/resources/zh/nls.provider.json b/ui/src/i18n/resources/zh/nls.provider.json index 0bb712da5..18c61c0cf 100644 --- a/ui/src/i18n/resources/zh/nls.provider.json +++ b/ui/src/i18n/resources/zh/nls.provider.json @@ -289,6 +289,7 @@ "volcengine_alb": "火山引擎 - 应用型负载均衡 ALB", "volcengine_apig": "火山引擎 - API 网关", "volcengine_cdn": "火山引擎 - 内容分发网络 CDN", + "volcengine_certcenter": "火山引擎 - 证书中心", "volcengine_certcenter_upload": "火山引擎 - 上传到证书中心", "volcengine_clb": "火山引擎 - 传统型负载均衡 CLB", "volcengine_dcdn": "火山引擎 - 全站加速 DCDN", diff --git a/ui/src/i18n/resources/zh/nls.workflow.nodes.json b/ui/src/i18n/resources/zh/nls.workflow.nodes.json index d1a55dd19..3e9e6a72b 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -3099,6 +3099,10 @@ "label": "优刻得接口端点(可选)", "placeholder": "请输入优刻得 USSL 接口端点(例如:api.ucloud.cn)", "tooltip": "这是什么?请参阅 https://docs.ucloud.cn/api/summary/gateway
    国际站用户请填写 api.ucloud-global.com。" + }, + "volcengine_certcenter_region": { + "label": "火山引擎服务地域", + "placeholder": "请输入火山引擎证书中心服务地域(例如:cn-beijing)" } } },