feat(provider): new purge provider: azure keyvault

This commit is contained in:
Fu Diwei
2026-08-27 17:14:32 +08:00
parent 335f57c194
commit dd84e4ef98
9 changed files with 311 additions and 0 deletions
@@ -0,0 +1,28 @@
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/azure-keyvault"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.PurgeProviderTypeAzureKeyVault, func(options *ProviderFactoryOptions) (core.Purger, error) {
credentials := domain.AccessConfigForAzure{}
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{
TenantId: credentials.TenantId,
ClientId: credentials.ClientId,
ClientSecret: credentials.ClientSecret,
CloudName: credentials.CloudName,
KeyVaultName: xmaps.GetString(options.ProviderExtendedConfig, "keyvaultName"),
})
return provider, err
})
}
+1
View File
@@ -495,6 +495,7 @@ const (
PurgeProviderTypeAliyunCAS = PurgeProviderType(AccessProviderTypeAliyun + "-cas")
PurgeProviderTypeAliyunCLB = PurgeProviderType(AccessProviderTypeAliyun + "-clb")
PurgeProviderTypeAliyunESA = PurgeProviderType(AccessProviderTypeAliyun + "-esa")
PurgeProviderTypeAzureKeyVault = PurgeProviderType(AccessProviderTypeAzure + "-keyvault")
PurgeProviderTypeBaiduCloudCert = PurgeProviderType(AccessProviderTypeBaiduCloud + "-cert")
PurgeProviderTypeHuaweiCloudELB = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-elb")
PurgeProviderTypeHuaweiCloudSCM = PurgeProviderType(AccessProviderTypeHuaweiCloud + "-scm")
@@ -0,0 +1,56 @@
//go:build tester
package azurekeyvault_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/azure-keyvault"
)
var (
fp = tester.InitArgs("AZUREKEYVAULT_")
fTenantId string
fClientId string
fClientSecret string
fCloudName string
fKeyVaultName string
)
func init() {
fp.DefineString(&fTenantId, "TENANTID")
fp.DefineString(&fClientId, "CLIENTID")
fp.DefineString(&fClientSecret, "CLIENTSECRET")
fp.DefineString(&fCloudName, "CLOUDNAME")
fp.DefineString(&fKeyVaultName, "KEYVAULTNAME")
}
/*
Shell command to run this test:
go test -tags=tester -v ./azure_keyvault_test.go -args \
--AZUREKEYVAULT_TENANTID="your-tenant-id" \
--AZUREKEYVAULT_CLIENTID="your-app-registration-client-id" \
--AZUREKEYVAULT_CLIENTSECRET="your-app-registration-client-secret" \
--AZUREKEYVAULT_CLOUDNAME="china" \
--AZUREKEYVAULT_KEYVAULTNAME="your-keyvault-name"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Purge", func(t *testing.T) {
provider, err := impl.NewPurger(&impl.PurgerConfig{
TenantId: fTenantId,
ClientId: fClientId,
ClientSecret: fClientSecret,
CloudName: fCloudName,
KeyVaultName: fKeyVaultName,
})
require.NoError(t, err)
tester.Purge(t, provider, tester.PurgeInput{})
})
}
@@ -0,0 +1,160 @@
package azurekeyvault
import (
"context"
"errors"
"fmt"
"log/slog"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
"github.com/Azure/azure-sdk-for-go/sdk/azidentity"
"github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
xloop "github.com/certimate-go/certimate/pkg/utils/loop"
xazure "github.com/certimate-go/certimate/pkg/utils/third-party/azure"
xwait "github.com/certimate-go/certimate/pkg/utils/wait"
)
type (
Provider = core.Purger
PurgeResult = core.PurgerPurgeResult
)
type PurgerConfig struct {
// Azure TenantId。
TenantId string `json:"tenantId"`
// Azure ClientId。
ClientId string `json:"clientId"`
// Azure ClientSecret。
ClientSecret string `json:"clientSecret"`
// Azure 主权云环境。
CloudName string `json:"cloudName,omitempty"`
// Key Vault 名称。
KeyVaultName string `json:"keyvaultName"`
}
type Purger struct {
config *PurgerConfig
logger *slog.Logger
sdkClient *azcertificates.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.CloudName, config.TenantId, config.ClientId, config.ClientSecret, config.KeyVaultName)
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([]azcertificates.ID, 0)
// 获取证书列表
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/get-certificates/get-certificates
listCertificatesPager := p.sdkClient.NewListCertificatePropertiesPager(&azcertificates.ListCertificatePropertiesOptions{})
for listCertificatesPager.More() {
page, err := listCertificatesPager.NextPage(ctx)
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'keyvault.GetCertificates': %w", err)
}
for _, certItem := range page.Value {
if certItem.Attributes == nil || certItem.Attributes.Expires == nil {
continue
}
certNotAfter := lo.FromPtr(certItem.Attributes.Expires)
if time.Since(certNotAfter) >= expiry {
purgingCertIds = append(purgingCertIds, *certItem.ID)
}
}
}
// 删除证书
// REF: https://learn.microsoft.com/en-us/rest/api/keyvault/certificates/delete-certificate/delete-certificate
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 azcertificates.ID, i int) error {
if i > 0 {
if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil {
return err
}
}
deleteCertificateResp, err := p.sdkClient.DeleteCertificate(ctx, certId.Name(), nil)
p.logger.Debug("sdk request 'keyvault.DeleteCertificate'", slog.String("params.certificateName", certId.Name()), slog.Any("response", deleteCertificateResp))
if err != nil {
var sdkErr *azcore.ResponseError
if errors.As(err, &sdkErr) {
if sdkErrCode := sdkErr.ErrorCode; sdkErrCode == "ResourceNotFound" || sdkErrCode == "CertificateNotFound" {
return nil
}
}
return fmt.Errorf("failed to execute sdk request 'keyvault.DeleteCertificate': %w", err)
}
purgedCount++
return nil
}); err != nil {
return nil, err
}
}
return &PurgeResult{
Count: purgedCount,
}, nil
}
func createSDKClient(cloudName, tenantId, clientId, clientSecret, keyvaultName string) (*azcertificates.Client, error) {
env, err := xazure.GetCloudEnvConfiguration(cloudName)
if err != nil {
return nil, err
}
clientOptions := azcore.ClientOptions{Cloud: env}
credential, err := azidentity.NewClientSecretCredential(tenantId, clientId, clientSecret,
&azidentity.ClientSecretCredentialOptions{ClientOptions: clientOptions})
if err != nil {
return nil, err
}
endpoint := fmt.Sprintf("https://%s.vault.azure.net", keyvaultName)
if xazure.IsUSGovernmentEnv(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.usgovcloudapi.net", keyvaultName)
} else if xazure.IsChinaEnv(cloudName) {
endpoint = fmt.Sprintf("https://%s.vault.azure.cn", keyvaultName)
}
client, err := azcertificates.NewClient(endpoint, credential, nil)
if err != nil {
return nil, err
}
return client, nil
}
@@ -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 BizPurgeNodeConfigFieldsProviderAzureKeyVault from "./BizPurgeNodeConfigFieldsProviderAzureKeyVault";
import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudELB";
import BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF from "./BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF";
import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP";
@@ -21,6 +22,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.AZURE_KEYVAULT]: BizPurgeNodeConfigFieldsProviderAzureKeyVault,
[PURGE_PROVIDERS.HUAWEICLOUD_ELB]: BizPurgeNodeConfigFieldsProviderHuaweiCloudELB,
[PURGE_PROVIDERS.HUAWEICLOUD_WAF]: BizPurgeNodeConfigFieldsProviderHuaweiCloudWAF,
[PURGE_PROVIDERS.TENCENTCLOUD_GAAP]: BizPurgeNodeConfigFieldsProviderTencentCloudGAAP,
@@ -0,0 +1,52 @@
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 BizPurgeNodeConfigFieldsProviderAzureKeyVault = () => {
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, "keyvaultName"]}
initialValue={initialValues.keyvaultName}
label={t("workflow_node.purge.form.azure_keyvault_name.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.purge.form.azure_keyvault_name.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.purge.form.azure_keyvault_name.placeholder")} />
</Form.Item>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
keyvaultName: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> }) => {
const { t: _ } = i18n;
return z.object({
keyvaultName: z.string().nonempty(),
});
};
const _default = Object.assign(BizPurgeNodeConfigFieldsProviderAzureKeyVault, {
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`,
AZURE_KEYVAULT: `${ACCESS_PROVIDERS.AZURE}-keyvault`,
BAIDUCLOUD_CERT: `${ACCESS_PROVIDERS.BAIDUCLOUD}-cert`,
HUAWEICLOUD_ELB: `${ACCESS_PROVIDERS.HUAWEICLOUD}-elb`,
HUAWEICLOUD_SCM: `${ACCESS_PROVIDERS.HUAWEICLOUD}-scm`,
@@ -1023,6 +1024,7 @@ export const purgeProvidersMap: Map<PurgeProvider["type"] | string, PurgeProvide
[PURGE_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud_elb"],
[PURGE_PROVIDERS.HUAWEICLOUD_WAF, "provider.huaweicloud_waf"],
[PURGE_PROVIDERS.VOLCENGINE_CERTCENTER, "provider.volcengine_certcenter"],
[PURGE_PROVIDERS.AZURE_KEYVAULT, "provider.azure_keyvault"],
[PURGE_PROVIDERS.BAIDUCLOUD_CERT, "provider.baiducloud_cert"],
[PURGE_PROVIDERS.JDCLOUD_SSL, "provider.jdcloud_ssl"],
[PURGE_PROVIDERS.UCLOUD_USSL, "provider.ucloud_ussl"],
@@ -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>"
},
"azure_keyvault_name": {
"label": "Azure KeyVault name",
"placeholder": "Please enter Azure KeyVault name",
"tooltip": "For more information, see <a href=\"https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates</a>"
},
"huaweicloud_elb_region": {
"label": "Huawei Cloud region",
"placeholder": "Please enter Huawei Cloud ELB region (e.g. cn-north-1)",
@@ -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>"
},
"azure_keyvault_name": {
"label": "Azure KeyVault 名称",
"placeholder": "请输入 Azure KeyVault 名称",
"tooltip": "这是什么?请参阅 <a href=\"https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates\" target=\"_blank\">https://learn.microsoft.com/zh-cn/azure/key-vault/general/about-keys-secrets-certificates</a>"
},
"huaweicloud_elb_region": {
"label": "华为云服务区域",
"placeholder": "请输入华为云 ELB 服务区域(例如:cn-north-1)",