feat(provider): new purge provider: ucloud ussl

This commit is contained in:
Fu Diwei
2026-08-27 17:14:30 +08:00
parent 4d26e4a9bf
commit 4ded078aaf
13 changed files with 395 additions and 5 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/ucloud-ussl"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
Registries.MustRegister(domain.PurgeProviderTypeUCloudUSSL, func(options *ProviderFactoryOptions) (core.Purger, error) {
credentials := domain.AccessConfigForUCloud{}
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{
PrivateKey: credentials.PrivateKey,
PublicKey: credentials.PublicKey,
ProjectId: credentials.ProjectId,
Endpoint: xmaps.GetString(options.ProviderExtendedConfig, "endpoint"),
})
return provider, err
})
}
+1
View File
@@ -500,6 +500,7 @@ const (
PurgeProviderTypeTencentCloudGAAP = PurgeProviderType(AccessProviderTypeTencentCloud + "-gaap")
PurgeProviderTypeTencentCloudSSL = PurgeProviderType(AccessProviderTypeTencentCloud + "-ssl")
PurgeProviderTypeTencentCloudTSE = PurgeProviderType(AccessProviderTypeTencentCloud + "-tse")
PurgeProviderTypeUCloudUSSL = PurgeProviderType(AccessProviderTypeUCloud + "-ussl")
)
type NotificationProviderType string
@@ -5,7 +5,6 @@ import (
"fmt"
"log/slog"
"strings"
"time"
"github.com/samber/lo"
"github.com/ucloud/ucloud-sdk-go/services/ulb"
@@ -260,7 +259,7 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudLoadba
return fmt.Errorf("failed to execute sdk request 'ulb.AddSSLBinding': %w", err)
}
// 找出需要删除绑定的扩展证书
// 获取扩展证书信息,并找出需要删除绑定的同域名证书
// REF: https://docs.ucloud.cn/api/ulb-api/describe_sslv2
sslIdsToDelete := make([]string, 0)
for _, certItem := range cloudListenerInfo.Certificates {
@@ -283,9 +282,6 @@ func (d *Deployer) updateListenerSniCertificate(ctx context.Context, cloudLoadba
if sslItem.Domains == d.config.Domain {
sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 同域名证书需要删除
continue
} else if sslItem.NotAfter != 0 && int64(sslItem.NotAfter) < time.Now().Unix() {
sslIdsToDelete = append(sslIdsToDelete, sslItem.SSLId) // 过期证书需要删除。TODO: remove on v0.5
continue
}
}
@@ -0,0 +1,46 @@
//go:build tester
package ucloudussl_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/ucloud-ussl"
)
var (
fp = tester.InitArgs("UCLOUDUSSL_")
fTestCertPath string
fTestKeyPath string
fPrivateKey string
fPublicKey string
)
func init() {
fp.DefineString(&fPrivateKey, "PRIVATEKEY")
fp.DefineString(&fPublicKey, "PUBLICKEY")
}
/*
Shell command to run this test:
go test -tags=tester -v ./ucloud_ussl_test.go -args \
--UCLOUDUSSL_PRIVATEKEY="your-private-key" \
--UCLOUDUSSL_PUBLICKEY="your-public-key"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Purge", func(t *testing.T) {
provider, err := impl.NewPurger(&impl.PurgerConfig{
PrivateKey: fPrivateKey,
PublicKey: fPublicKey,
})
require.NoError(t, err)
tester.Purge(t, provider, tester.PurgeInput{})
})
}
@@ -0,0 +1,213 @@
package ucloudussl
import (
"context"
"fmt"
"log/slog"
"strings"
"time"
"github.com/ucloud/ucloud-sdk-go/ucloud"
"github.com/ucloud/ucloud-sdk-go/ucloud/auth"
"github.com/certimate-go/certimate/pkg/core"
ucloudsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ucloud/ussl"
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 {
// 优刻得 API 私钥。
PrivateKey string `json:"privateKey"`
// 优刻得 API 公钥。
PublicKey string `json:"publicKey"`
// 优刻得项目 ID。
ProjectId string `json:"projectId,omitempty"`
// 优刻得接口端点。
Endpoint string `json:"endpoint,omitempty"`
}
type Purger struct {
config *PurgerConfig
logger *slog.Logger
sdkClient *ucloudsdk.USSLClient
}
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.PrivateKey, config.PublicKey, config.ProjectId, 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([]int, 0)
purgingCertId2ModeMap := make(map[int]string)
// 查询用户托管证书列表
// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list
getCertificateListPage := 1
getCertificateListPageSize := 1000
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getCertificateListReq := p.sdkClient.NewGetCertificateListRequest()
getCertificateListReq.Mode = ucloud.String("trust")
getCertificateListReq.Sort = ucloud.String("2")
getCertificateListReq.Page = ucloud.Int(getCertificateListPage)
getCertificateListReq.PageSize = ucloud.Int(getCertificateListPageSize)
getCertificateListResp, err := p.sdkClient.GetCertificateList(getCertificateListReq)
p.logger.Debug("sdk request 'ussl.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateList': %w", err)
}
for _, certItem := range getCertificateListResp.CertificateList {
certNotAfter := time.UnixMilli(certItem.NotAfter)
if time.Since(certNotAfter) >= expiry {
purgingCertIds = append(purgingCertIds, certItem.CertificateID)
purgingCertId2ModeMap[certItem.CertificateID] = certItem.Mode
}
}
if len(getCertificateListResp.CertificateList) < getCertificateListPageSize ||
getCertificateListPage*getCertificateListPageSize >= getCertificateListResp.TotalCount {
break
}
getCertificateListPage++
}
// 查询用户购买证书列表
// REF: https://docs.ucloud.cn/api/usslcertificate-api/get_certificate_list
getCertificateListPage = 1
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
getCertificateListReq := p.sdkClient.NewGetCertificateListRequest()
getCertificateListReq.Mode = ucloud.String("purchase")
getCertificateListReq.Sort = ucloud.String("2")
getCertificateListReq.Page = ucloud.Int(getCertificateListPage)
getCertificateListReq.PageSize = ucloud.Int(getCertificateListPageSize)
getCertificateListResp, err := p.sdkClient.GetCertificateList(getCertificateListReq)
p.logger.Debug("sdk request 'ussl.GetCertificateList'", slog.Any("request", getCertificateListReq), slog.Any("response", getCertificateListResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'ussl.GetCertificateList': %w", err)
}
for _, certItem := range getCertificateListResp.CertificateList {
certNotAfter := time.UnixMilli(certItem.NotAfter)
if time.Since(certNotAfter) >= expiry {
purgingCertIds = append(purgingCertIds, certItem.CertificateID)
purgingCertId2ModeMap[certItem.CertificateID] = certItem.Mode
}
}
if len(getCertificateListResp.CertificateList) < getCertificateListPageSize ||
getCertificateListPage*getCertificateListPageSize >= getCertificateListResp.TotalCount {
break
}
getCertificateListPage++
}
// 删除证书
// REF: https://docs.ucloud.cn/api/usslcertificate-api/delete_ssl_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 int, i int) error {
if i > 0 {
if err := xwait.DelayWithContext(ctx, 1*time.Second); err != nil {
return err
}
}
deleteSSLCertificateReq := p.sdkClient.NewDeleteSSLCertificateRequest()
deleteSSLCertificateReq.CertificateMode = ucloud.String(purgingCertId2ModeMap[certId])
deleteSSLCertificateReq.CertificateID = ucloud.Int(certId)
deleteSSLCertificateResp, err := p.sdkClient.DeleteSSLCertificate(deleteSSLCertificateReq)
p.logger.Debug("sdk request 'ussl.DeleteSSLCertificate'", slog.Any("request", deleteSSLCertificateReq), slog.Any("response", deleteSSLCertificateResp))
if err != nil {
if deleteSSLCertificateResp != nil && deleteSSLCertificateResp.GetRetCode() == 80011 {
return nil
}
return fmt.Errorf("failed to execute sdk request 'ussl.DeleteSSLCertificate': %w", err)
}
purgedCount++
return nil
}); err != nil {
return nil, err
}
}
return &PurgeResult{
Count: purgedCount,
}, nil
}
func createSDKClient(privateKey, publicKey, projectId, endpoint string) (*ucloudsdk.USSLClient, error) {
if privateKey == "" {
return nil, fmt.Errorf("ucloud: invalid private key")
}
if publicKey == "" {
return nil, fmt.Errorf("ucloud: invalid public key")
}
cfg := ucloud.NewConfig()
if projectId != "" {
cfg.ProjectId = projectId
}
if endpoint != "" {
if strings.Contains(endpoint, "://") {
cfg.BaseUrl = endpoint
} else {
cfg.BaseUrl = "https://" + endpoint
}
}
credential := auth.NewCredential()
credential.PrivateKey = privateKey
credential.PublicKey = publicKey
client := ucloudsdk.NewClient(&cfg, &credential)
return client, nil
}
@@ -0,0 +1,41 @@
package ussl
import (
"github.com/ucloud/ucloud-sdk-go/ucloud/request"
"github.com/ucloud/ucloud-sdk-go/ucloud/response"
)
type DeleteSSLCertificateRequest struct {
request.CommonBase
CertificateMode *string `required:"true"`
CertificateID *int `required:"true"`
}
type DeleteSSLCertificateResponse struct {
response.CommonBase
}
func (c *USSLClient) NewDeleteSSLCertificateRequest() *DeleteSSLCertificateRequest {
req := &DeleteSSLCertificateRequest{}
c.Client.SetupRequest(req)
req.SetRetryable(true)
req.SetEncoder(request.NewJSONEncoder(c.GetConfig(), c.GetCredential()))
return req
}
func (c *USSLClient) DeleteSSLCertificate(req *DeleteSSLCertificateRequest) (*DeleteSSLCertificateResponse, error) {
var err error
var res DeleteSSLCertificateResponse
reqCopier := *req
err = c.Client.InvokeAction("DeleteSSLCertificate", &reqCopier, &res)
if err != nil {
return &res, err
}
return &res, nil
}
@@ -9,6 +9,7 @@ import BizPurgeNodeConfigFieldsProviderHuaweiCloudELB from "./BizPurgeNodeConfig
import BizPurgeNodeConfigFieldsProviderTencentCloudGAAP from "./BizPurgeNodeConfigFieldsProviderTencentCloudGAAP";
import BizPurgeNodeConfigFieldsProviderTencentCloudSSL from "./BizPurgeNodeConfigFieldsProviderTencentCloudSSL";
import BizPurgeNodeConfigFieldsProviderTencentCloudTSE from "./BizPurgeNodeConfigFieldsProviderTencentCloudTSE";
import BizPurgeNodeConfigFieldsProviderUCloudUSSL from "./BizPurgeNodeConfigFieldsProviderUCloudUSSL";
const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentType<any>>> = {
/*
@@ -22,6 +23,7 @@ const providerComponentMap: Partial<Record<PurgeProviderType, React.ComponentTyp
[PURGE_PROVIDERS.TENCENTCLOUD_GAAP]: BizPurgeNodeConfigFieldsProviderTencentCloudGAAP,
[PURGE_PROVIDERS.TENCENTCLOUD_SSL]: BizPurgeNodeConfigFieldsProviderTencentCloudSSL,
[PURGE_PROVIDERS.TENCENTCLOUD_TSE]: BizPurgeNodeConfigFieldsProviderTencentCloudTSE,
[PURGE_PROVIDERS.UCLOUD_USSL]: BizPurgeNodeConfigFieldsProviderUCloudUSSL,
};
const useComponent = (provider: string, { initProps, deps = [] }: { initProps?: (provider: string) => any; deps?: unknown[] }) => {
@@ -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 BizPurgeNodeConfigFieldsProviderUCloudUSSL = () => {
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.ucloud_ussl_endpoint.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.purge.form.ucloud_ussl_endpoint.tooltip") }}></span>}
>
<Input allowClear placeholder={t("workflow_node.purge.form.ucloud_ussl_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(BizPurgeNodeConfigFieldsProviderUCloudUSSL, {
getInitialValues,
getSchema,
});
export default _default;
+2
View File
@@ -994,6 +994,7 @@ export const PURGE_PROVIDERS = Object.freeze(
TENCENTCLOUD_GAAP: `${ACCESS_PROVIDERS.TENCENTCLOUD}-gaap`,
TENCENTCLOUD_SSL: `${ACCESS_PROVIDERS.TENCENTCLOUD}-ssl`,
TENCENTCLOUD_TSE: `${ACCESS_PROVIDERS.TENCENTCLOUD}-tse`,
UCLOUD_USSL: `${ACCESS_PROVIDERS.UCLOUD}-ussl`,
} as const
);
@@ -1016,6 +1017,7 @@ export const purgeProvidersMap: Map<PurgeProvider["type"] | string, PurgeProvide
[PURGE_PROVIDERS.TENCENTCLOUD_TSE, "provider.tencentcloud_tse"],
[PURGE_PROVIDERS.HUAWEICLOUD_SCM, "provider.huaweicloud_scm"],
[PURGE_PROVIDERS.HUAWEICLOUD_ELB, "provider.huaweicloud_elb"],
[PURGE_PROVIDERS.UCLOUD_USSL, "provider.ucloud_ussl"],
] satisfies Array<[PurgeProviderType, string, "builtin"] | [PurgeProviderType, string]>
).map(([type, name]) => [
type,
@@ -275,6 +275,7 @@
"ucloud_uewaf": "UCloud - UEWAF (Enterprise Web Application Firewall)",
"ucloud_upathx": "UCloud - UPathX (Global Dynamic Acceleration)",
"ucloud_us3": "UCloud - US3 (Object-based Storage)",
"ucloud_ussl": "UCloud - USSL (SSL Certificates)",
"todaynic": "TodayNIC.com",
"unicloud": "uniCloud (DCloud)",
"unicloud_webhost": "uniCloud - Web Host",
@@ -3090,6 +3090,11 @@
"label": "Tencent Cloud TSE Cloud-native API gateway ID",
"placeholder": "Please enter Tencent Cloud TSE Cloud-native API gateway ID",
"tooltip": "For more information, see <a href=\"https://console.tencentcloud.com/rcc/governance\" target=\"_blank\">https://console.tencentcloud.com/rcc/governance</a>"
},
"ucloud_ussl_endpoint": {
"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>"
}
}
},
@@ -276,6 +276,7 @@
"ucloud_uewaf": "优刻得 - 企业 Web 应用防火墙 UEWAF",
"ucloud_upathx": "优刻得 - 全球动态加速 UPathX",
"ucloud_us3": "优刻得 - 对象存储 US3",
"ucloud_ussl": "优刻得 - 证书管理 USSL",
"unicloud": "uniCloud (DCloud)",
"unicloud_webhost": "uniCloud - 前端网页托管",
"upyun": "又拍云",
@@ -3089,6 +3089,11 @@
"label": "腾讯云 TSE 云原生网关 ID",
"placeholder": "请输入腾讯云 TSE 云原生网关 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com.cn/rcc/governance\" target=\"_blank\">https://console.cloud.tencent.com.cn/rcc/governance</a>"
},
"ucloud_ussl_endpoint": {
"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>。"
}
}
},