From c6e48816d0d2d13f05b3af6fcaa8f0ecd268f564 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Thu, 27 Aug 2026 17:14:31 +0800 Subject: [PATCH] feat(provider): new purge provider: huaweicloud waf --- .../certmgmt/purgers/sp_huaweicloud_waf.go | 27 +++ internal/domain/provider.go | 1 + .../huaweicloud-waf/huaweicloud_waf_test.go | 48 +++++ .../huaweicloud-waf/huaweicloud_waf.go | 193 ++++++++++++++++++ .../services/waf/v1/waf_client.go | 10 + .../services/waf/v1/waf_meta.go | 4 + .../BizPurgeNodeConfigFieldsProvider.tsx | 2 + ...NodeConfigFieldsProviderHuaweiCloudWAF.tsx | 52 +++++ ui/src/domain/provider.ts | 2 + .../i18n/resources/en/nls.workflow.nodes.json | 5 + .../i18n/resources/zh/nls.workflow.nodes.json | 5 + 11 files changed, 349 insertions(+) create mode 100644 internal/certmgmt/purgers/sp_huaweicloud_waf.go create mode 100644 pkg/core/purger/providers-tester/huaweicloud-waf/huaweicloud_waf_test.go create mode 100644 pkg/core/purger/providers/huaweicloud-waf/huaweicloud_waf.go create mode 100644 ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF.tsx diff --git a/internal/certmgmt/purgers/sp_huaweicloud_waf.go b/internal/certmgmt/purgers/sp_huaweicloud_waf.go new file mode 100644 index 000000000..d25a0e1d2 --- /dev/null +++ b/internal/certmgmt/purgers/sp_huaweicloud_waf.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/huaweicloud-waf" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.PurgeProviderTypeHuaweiCloudWAF, func(options *ProviderFactoryOptions) (core.Purger, error) { + credentials := domain.AccessConfigForHuaweiCloud{} + 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, + EnterpriseProjectId: credentials.EnterpriseProjectId, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), + }) + return provider, err + }) +} diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 01e6768af..999acff9e 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -497,6 +497,7 @@ const ( PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa") PurgeProviderTypeHuaweiCloudELB = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-elb") PurgeProviderTypeHuaweiCloudSCM = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-scm") + PurgeProviderTypeHuaweiCloudWAF = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-waf") PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap") PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl") PurgeProviderTypeTencentCloudTSE = PurgeProviderType(AccessProviderTypeTencentCloud + "-tse") diff --git a/pkg/core/purger/providers-tester/huaweicloud-waf/huaweicloud_waf_test.go b/pkg/core/purger/providers-tester/huaweicloud-waf/huaweicloud_waf_test.go new file mode 100644 index 000000000..574c8fe77 --- /dev/null +++ b/pkg/core/purger/providers-tester/huaweicloud-waf/huaweicloud_waf_test.go @@ -0,0 +1,48 @@ +//go:build tester + +package huaweicloudwaf_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/huaweicloud-waf" +) + +var ( + fp = tester.InitArgs("HUAWEICLOUDWAF_") + fAccessKeyId string + fSecretAccessKey string + fRegion string +) + +func init() { + fp.DefineString(&fAccessKeyId, "ACCESSKEYID") + fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY") + fp.DefineString(&fRegion, "REGION") +} + +/* +Shell command to run this test: + + go test -tags=tester -v ./huaweicloud_waf_test.go -args \ + --HUAWEICLOUDWAF_ACCESSKEYID="your-access-key-id" \ + --HUAWEICLOUDWAF_SECRETACCESSKEY="your-access-key-secret" \ + --HUAWEICLOUDWAF_REGION="cn-north-4" +*/ +func TestProvider(t *testing.T) { + fp.Parse() + + t.Run("Purge", func(t *testing.T) { + provider, err := impl.NewPurger(&impl.PurgerConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + Region: fRegion, + }) + require.NoError(t, err) + + tester.Purge(t, provider, tester.PurgeInput{}) + }) +} diff --git a/pkg/core/purger/providers/huaweicloud-waf/huaweicloud_waf.go b/pkg/core/purger/providers/huaweicloud-waf/huaweicloud_waf.go new file mode 100644 index 000000000..d2a3b61db --- /dev/null +++ b/pkg/core/purger/providers/huaweicloud-waf/huaweicloud_waf.go @@ -0,0 +1,193 @@ +package huaweicloudwaf + +import ( + "context" + "fmt" + "log/slog" + "time" + + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/basic" + "github.com/huaweicloud/huaweicloud-sdk-go-v3/core/auth/global" + hwwafmodel "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/model" + hwwafregion "github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/region" + "github.com/samber/lo" + + hwwaf "github.com/certimate-go/certimate/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1" + + "github.com/certimate-go/certimate/pkg/core" + xloop "github.com/certimate-go/certimate/pkg/utils/loop" + xhuaweicloud "github.com/certimate-go/certimate/pkg/utils/third-party/huaweicloud" + 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"` + // 华为云企业项目 ID。 + EnterpriseProjectId string `json:"enterpriseProjectId,omitempty"` + // 华为云区域。 + Region string `json:"region"` +} + +type Purger struct { + config *PurgerConfig + logger *slog.Logger + sdkClient *hwwaf.WafClient +} + +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://support.huaweicloud.com/api-waf/ListCertificates.html + listCertificatesPage := 1 + listCertificatesPageSize := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listCertificatesReq := &hwwafmodel.ListCertificatesRequest{ + EnterpriseProjectId: lo.EmptyableToPtr(p.config.EnterpriseProjectId), + ExpStatus: lo.ToPtr(hwwafmodel.GetListCertificatesRequestExpStatusEnum().E_1), + Page: lo.ToPtr(int32(listCertificatesPage)), + Pagesize: lo.ToPtr(int32(listCertificatesPageSize)), + } + listCertificatesResp, err := p.sdkClient.ListCertificates(listCertificatesReq) + p.logger.Debug("sdk request 'waf.ListCertificates'", slog.Any("request", listCertificatesReq), slog.Any("response", listCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'waf.ListCertificates': %w", err) + } + + for _, certItem := range *listCertificatesResp.Items { + certNotAfter := time.UnixMilli(lo.FromPtr(certItem.ExpireTime)) + if time.Since(certNotAfter) >= expiry { + purgingCertIds = append(purgingCertIds, certItem.Id) + } + } + + if len(*listCertificatesResp.Items) < listCertificatesPageSize { + break + } + + listCertificatesPage++ + } + + // 删除证书 + // REF: https://support.huaweicloud.com/api-waf/DeleteCertificate.html + 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 := &hwwafmodel.DeleteCertificateRequest{ + EnterpriseProjectId: lo.EmptyableToPtr(p.config.EnterpriseProjectId), + CertificateId: certId, + } + deleteCertificateResp, err := p.sdkClient.DeleteCertificate(deleteCertificateReq) + p.logger.Debug("sdk request 'waf.DeleteCertificate'", slog.Any("request", deleteCertificateReq), slog.Any("response", deleteCertificateResp)) + if err != nil { + if deleteCertificateResp != nil && deleteCertificateResp.HttpStatusCode == 404 { + return nil + } + + return fmt.Errorf("failed to execute sdk request 'waf.DeleteCertificate': %w", err) + } + + purgedCount++ + return nil + }); err != nil { + return nil, err + } + } + + return &PurgeResult{ + Count: purgedCount, + }, nil +} + +func createSDKClient(accessKeyId, secretAccessKey, region string) (*hwwaf.WafClient, error) { + projectId, err := getSDKProjectId(accessKeyId, secretAccessKey, region) + if err != nil { + return nil, err + } + + auth, err := basic.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + WithProjectId(projectId). + SafeBuild() + if err != nil { + return nil, err + } + + hcRegion, err := hwwafregion.SafeValueOf(region) + if err != nil { + return nil, err + } + + hcClient, err := hwwaf.WafClientBuilder(). + WithRegion(hcRegion). + WithCredential(auth). + SafeBuild() + if err != nil { + return nil, err + } + + client := hwwaf.NewWafClient(hcClient) + return client, nil +} + +func getSDKProjectId(accessKeyId, secretAccessKey, region string) (string, error) { + auth, err := global.NewCredentialsBuilder(). + WithAk(accessKeyId). + WithSk(secretAccessKey). + SafeBuild() + if err != nil { + return "", err + } + + return xhuaweicloud.GetKeystoneProjectIDWithRegion(auth, region) +} diff --git a/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_client.go b/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_client.go index 0bf385ad1..d160fadbe 100644 --- a/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_client.go +++ b/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_client.go @@ -28,6 +28,16 @@ func (c *WafClient) CreateCertificate(request *model.CreateCertificateRequest) ( } } +func (c *WafClient) DeleteCertificate(request *model.DeleteCertificateRequest) (*model.DeleteCertificateResponse, error) { + requestDef := GenReqDefForDeleteCertificate() + + if resp, err := c.HcClient.Sync(request, requestDef); err != nil { + return nil, err + } else { + return resp.(*model.DeleteCertificateResponse), nil + } +} + func (c *WafClient) ListCertificates(request *model.ListCertificatesRequest) (*model.ListCertificatesResponse, error) { requestDef := GenReqDefForListCertificates() diff --git a/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_meta.go b/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_meta.go index 40106e203..a53a45726 100644 --- a/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_meta.go +++ b/pkg/sdk3rd-trimmed/github.com/huaweicloud/huaweicloud-sdk-go-v3/services/waf/v1/waf_meta.go @@ -9,6 +9,10 @@ func GenReqDefForCreateCertificate() *def.HttpRequestDef { return waf.GenReqDefForCreateCertificate() } +func GenReqDefForDeleteCertificate() *def.HttpRequestDef { + return waf.GenReqDefForDeleteCertificate() +} + func GenReqDefForListCertificates() *def.HttpRequestDef { return waf.GenReqDefForListCertificates() } diff --git a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx index b119a90f9..ad26e0e0c 100644 --- a/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizPurgeNodeConfigFieldsProvider.tsx @@ -6,6 +6,7 @@ import BizPurgeNodeConfigFieldsProviderAliyunCAS from "./BizPurgeNodeConfigField import BizPurgeNodeConfigFieldsProviderAliyunCLB from "./BizPurgeNodeConfigFieldsProviderAliyunCLB"; import BizPurgeNodeConfigFieldsProviderAliyunESA from "./BizPurgeNodeConfigFieldsProviderAliyunESA"; import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudELB"; +import BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF"; import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP"; import BizPurgeNodeConfigFieldsProviderTencentCloudSSL from "./BizPurgeNodeConfigFieldsProviderTencentCloudSSL"; import BizPurgeNodeConfigFieldsProviderTencentCloudTSE from "./BizPurgeNodeConfigFieldsProviderTencentCloudTSE"; @@ -20,6 +21,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 { + region: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t: _ } = i18n; + + return z.object({ + region: z.string().nonempty(), + }); +}; + +const _default = Object.assign(BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index ae339493d..236bc0bd2 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -991,6 +991,7 @@ export const PURGE_PROVIDERS = Object.freeze( ALIYUN_ESA: `${ACCESS_PROVIDERS.ALIYUN}-esa`, HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`, HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`, + HUAWEICLOUD_WAF: `${ACCESS_PROVIDERS.HUAWEICLOUD}-waf`, TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`, TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`, TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`, @@ -1017,6 +1018,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 818a39a19..74f4ad1b9 100644 --- a/ui/src/i18n/resources/en/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/en/nls.workflow.nodes.json @@ -3057,6 +3057,11 @@ "placeholder": "Please enter Huawei Cloud ELB region (e.g. cn-north-1)", "tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint/elb" }, + "huaweicloud_waf_region": { + "label": "Huawei Cloud region", + "placeholder": "Please enter Huawei Cloud WAF region (e.g. cn-north-1)", + "tooltip": "For more information, see https://console-intl.huaweicloud.com/apiexplorer/#/endpoint/waf" + }, "tencentcloud_gaap_endpoint": { "label": "Tencent Cloud API endpoint (Optional)", "placeholder": "Please enter Tencent Cloud GAAP API endpoint (e.g. gaap.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 d65f8ae32..d1a55dd19 100644 --- a/ui/src/i18n/resources/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/resources/zh/nls.workflow.nodes.json @@ -3056,6 +3056,11 @@ "placeholder": "请输入华为云 ELB 服务区域(例如:cn-north-1)", "tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint/elb" }, + "huaweicloud_waf_region": { + "label": "华为云服务区域", + "placeholder": "请输入华为云 WAF 服务区域(例如:cn-north-1)", + "tooltip": "这是什么?请参阅 https://console.huaweicloud.com/apiexplorer/#/endpoint/waf" + }, "tencentcloud_gaap_endpoint": { "label": "腾讯云接口端点(可选)", "placeholder": "请输入腾讯云 GAAP 接口端点(例如:gaap.tencentcloudapi.com)",