refactor: re-implement ksyun services with custom sdk, to lightweight package size

This commit is contained in:
Fu Diwei
2026-06-22 16:32:18 +08:00
committed by RHQYZ
parent 54277380d2
commit 63d444a739
32 changed files with 1561 additions and 174 deletions
-2
View File
@@ -7,7 +7,6 @@ require (
github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.13.1
github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azcertificates v1.5.0
github.com/G-Core/gcorelabscdn-go v1.0.37
github.com/KscSDK/ksc-sdk-go v0.22.0
github.com/akamai/AkamaiOPEN-edgegrid-golang/v13 v13.2.0
github.com/alibabacloud-go/alb-20200616/v2 v2.3.2
github.com/alibabacloud-go/apig-20240327/v7 v7.0.5
@@ -107,7 +106,6 @@ require (
github.com/AzureAD/microsoft-authentication-library-for-go v1.7.2 // indirect
github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect
github.com/avast/retry-go v3.0.0+incompatible // indirect
github.com/aws/aws-sdk-go v1.55.8 // indirect
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.8 // indirect
github.com/benbjohnson/clock v1.3.5 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
-6
View File
@@ -75,8 +75,6 @@ github.com/G-Core/gcorelabscdn-go v1.0.37/go.mod h1:iSGXaTvZBzDHQW+rKFS918BgFVpO
github.com/HdrHistogram/hdrhistogram-go v1.1.0/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/HdrHistogram/hdrhistogram-go v1.1.2/go.mod h1:yDgFjdqOqDEKOvasDdhWNXYg9BVp4O+o5f6V/ehm6Oo=
github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0=
github.com/KscSDK/ksc-sdk-go v0.22.0 h1:wIstq/89k+5nexhPLvhQLJaLl6gL3u+I+N01CK/PPaA=
github.com/KscSDK/ksc-sdk-go v0.22.0/go.mod h1:isHlJZi429ff5JLemSc10h7nznNgzJAY4MmNM8u7SBo=
github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU=
github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo=
github.com/Shopify/sarama v1.30.1/go.mod h1:hGgx05L/DiW8XYBXeJdKIN6V2QUy2H6JqME5VT1NLRw=
@@ -193,10 +191,7 @@ github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3d
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0=
github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY=
github.com/aws/aws-sdk-go v1.25.3/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.40.45/go.mod h1:585smgzpB/KqRA+K3y/NL/oYRqQvpNJYvLm+LY1U59Q=
github.com/aws/aws-sdk-go v1.55.8 h1:JRmEUbU52aJQZ2AjX4q4Wu7t4uZjOu71uyNmaWlUkJQ=
github.com/aws/aws-sdk-go v1.55.8/go.mod h1:ZkViS9AqA6otK+JBBNH2++sx1sgxrPKcSzPPvQkUtXk=
github.com/aws/aws-sdk-go-v2 v1.9.1/go.mod h1:cK/D0BBs0b/oWPIcX/Z/obahJK1TT7IPVjy53i/mX/4=
github.com/aws/aws-sdk-go-v2 v1.41.12 h1:DIKX2c31ekm9RA2D9FBj1EWXx++9AdAqRw+e78Tq2Ck=
github.com/aws/aws-sdk-go-v2 v1.41.12/go.mod h1:27+ACypSLljLAEKsCYOmrjKh83vuTRkuAe9Uv/3A4bg=
@@ -622,7 +617,6 @@ github.com/jdcloud-api/jdcloud-sdk-go v1.66.0 h1:mWxIv+mnWMZ/+a/VpAkzC9BqqmK185w
github.com/jdcloud-api/jdcloud-sdk-go v1.66.0/go.mod h1:UrKjuULIWLjHFlG6aSPunArE5QX57LftMmStAZJBEX8=
github.com/jlaffaye/ftp v0.2.0 h1:lXNvW7cBu7R/68bknOX3MrRIIqZ61zELs1P2RAiA3lg=
github.com/jlaffaye/ftp v0.2.0/go.mod h1:is2Ds5qkhceAPy2xD6RLI6hmp/qysSoymZ+Z2uTnspI=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
@@ -19,6 +19,7 @@ func init() {
provider, err := dplyimpl.NewDeployer(&dplyimpl.DeployerConfig{
AccessKeyId: credentials.AccessKeyId,
SecretAccessKey: credentials.SecretAccessKey,
Region: xmaps.GetString(options.ProviderExtendedConfig, "region"),
DeployTarget: xmaps.GetString(options.ProviderExtendedConfig, "deployTarget"),
CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"),
})
@@ -0,0 +1,184 @@
package ksyunkcm
import (
"context"
"crypto/sha1"
"encoding/hex"
"fmt"
"log/slog"
"strings"
"time"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
ksyunkcmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/kcm"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type (
Provider = core.Certmgr
UploadResult = core.CertmgrUploadResult
ReplaceResult = core.CertmgrReplaceResult
)
type CertmgrConfig struct {
// 金山云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 金山云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 金山云项目 ID。
ProjectId int64 `json:"projectId,omitempty"`
}
type Certmgr struct {
config *CertmgrConfig
logger *slog.Logger
sdkClient *ksyunkcmsdk.Client
}
var _ Provider = (*Certmgr)(nil)
func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the certmgr provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
return &Certmgr{
config: config,
logger: slog.Default(),
sdkClient: client,
}, nil
}
func (c *Certmgr) SetLogger(logger *slog.Logger) {
if logger == nil {
c.logger = slog.New(slog.DiscardHandler)
} else {
c.logger = logger
}
}
func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, error) {
// 避免重复上传
if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {
return nil, err
} else if upok {
c.logger.Info("ssl certificate already exists")
return upres, nil
}
// 上传证书
uploadCertificateReq := &ksyunkcmsdk.UploadCertificateRequest{
ProjectId: lo.ToPtr(c.config.ProjectId),
CertName: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
CertFile: lo.ToPtr(certPEM),
CertKey: lo.ToPtr(privkeyPEM),
}
uploadCertificateResp, err := c.sdkClient.UploadCertificateWithContext(ctx, uploadCertificateReq)
c.logger.Debug("sdk request 'kcm.UploadCertificate'", slog.Any("request", uploadCertificateReq), slog.Any("response", uploadCertificateResp))
if err != nil {
if uploadCertificateResp != nil &&
uploadCertificateResp.Error != nil && uploadCertificateResp.Error.Message == "重复的证书文件" {
if upres, upok, err := c.tryGetResultIfCertExists(ctx, certPEM); err != nil {
return nil, err
} else if !upok {
return nil, fmt.Errorf("could not find ssl certificate, may be upload failed")
} else {
c.logger.Info("ssl certificate already exists")
return upres, nil
}
}
return nil, fmt.Errorf("failed to execute sdk request 'kcm.UploadCertificate': %w", err)
}
return &UploadResult{
CertId: uploadCertificateResp.Ret.CertId,
CertName: uploadCertificateResp.Ret.CertName,
}, nil
}
func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) {
return nil, core.ErrUnsupported
}
func (c *Certmgr) tryGetResultIfCertExists(ctx context.Context, certPEM string) (*UploadResult, bool, error) {
certX509, err := xcert.ParseCertificateFromPEM(certPEM)
if err != nil {
return nil, false, err
}
listUserCertificatesPage := 1
listUserCertificatesPageSize := 100
for {
select {
case <-ctx.Done():
return nil, false, ctx.Err()
default:
}
listUserCertificatesReq := &ksyunkcmsdk.ListUserCertificatesRequest{
Page: lo.ToPtr(int32(listUserCertificatesPage)),
PageSize: lo.ToPtr(int32(listUserCertificatesPageSize)),
}
listUserCertificatesResp, err := c.sdkClient.ListUserCertificatesWithContext(ctx, listUserCertificatesReq)
c.logger.Debug("sdk request 'kcm.ListUserCertificates'", slog.Any("request", listUserCertificatesReq), slog.Any("response", listUserCertificatesResp))
if err != nil {
return nil, false, fmt.Errorf("failed to execute sdk request 'kcm.ListUserCertificates': %w", err)
}
if listUserCertificatesResp.Ret == nil || listUserCertificatesResp.Ret.Certs == nil {
break
}
for _, certItem := range listUserCertificatesResp.Ret.Certs {
// 对比证书多域名
if !strings.EqualFold(strings.Join(certX509.DNSNames, ","), strings.Join(certItem.Domains, ",")) {
continue
}
// 对比证书颁发者
if certX509.Issuer.CommonName != certItem.CA {
continue
}
// 对比证书指纹
fingerprint := sha1.Sum(certX509.Raw)
fingerprintHex := hex.EncodeToString(fingerprint[:])
if !strings.EqualFold(fingerprintHex, certItem.FingerPrint) {
continue
}
// 如果以上信息都一致,则视为已存在相同证书,直接返回
return &UploadResult{
CertId: certItem.CertId,
CertName: certItem.CertName,
}, true, nil
}
if len(listUserCertificatesResp.Ret.Certs) < listUserCertificatesPageSize {
break
}
listUserCertificatesPage++
}
return nil, false, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ksyunkcmsdk.Client, error) {
client, err := ksyunkcmsdk.NewClient(
ksyunkcmsdk.WithAkSk(accessKeyId, secretAccessKey),
)
if err != nil {
return nil, err
}
return client, nil
}
@@ -0,0 +1,49 @@
package ksyunkcm_test
import (
"testing"
"github.com/certimate-go/certimate/pkg/core/certmgr/internal/tester"
impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ksyun-kcm"
)
var (
fp = tester.Args("KSYUNKCM_")
fTestCertPath string
fTestKeyPath string
fAccessKeyId string
fSecretAccessKey string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
}
/*
Shell command to run this test:
go test -v ./ksyun_kcm_test.go -args \
--KSYUNKCM_TESTCERTPATH="/path/to/your-test-cert.pem" \
--KSYUNKCM_TESTKEYPATH="/path/to/your-test-key.pem" \
--KSYUNKCM_ACCESSKEYID="your-access-key-id" \
--KSYUNKCM_SECRETACCESSKEY="your-secret-access-key"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Upload", func(t *testing.T) {
provider, err := impl.NewCertmgr(&impl.CertmgrConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestUpload(t, provider, tester.TestUploadArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -0,0 +1,183 @@
package ksyunslb
import (
"context"
"fmt"
"log/slog"
"time"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ksyun-kcm"
ksyunkcmsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/kcm"
xcert "github.com/certimate-go/certimate/pkg/utils/cert"
)
type (
Provider = core.Certmgr
UploadResult = core.CertmgrUploadResult
ReplaceResult = core.CertmgrReplaceResult
)
type CertmgrConfig struct {
// 金山云 AccessKeyId。
AccessKeyId string `json:"accessKeyId"`
// 金山云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 金山云项目 ID。
ProjectId int64 `json:"projectId,omitempty"`
// 金山云地域。
Region string `json:"region"`
}
type Certmgr struct {
config *CertmgrConfig
logger *slog.Logger
sdkClient *ksyunkcmsdk.Client
sdkCertmgr core.Certmgr
}
var _ Provider = (*Certmgr)(nil)
func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the certmgr provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
}
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
ProjectId: config.ProjectId,
})
if err != nil {
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Certmgr{
config: config,
logger: slog.Default(),
sdkClient: client,
sdkCertmgr: pcertmgr,
}, nil
}
func (c *Certmgr) SetLogger(logger *slog.Logger) {
if logger == nil {
c.logger = slog.New(slog.DiscardHandler)
} else {
c.logger = logger
}
c.sdkCertmgr.SetLogger(logger)
}
func (c *Certmgr) Upload(ctx context.Context, certPEM, privkeyPEM string) (*UploadResult, error) {
// 描述证书,避免重复上传
describeCertificatesPage := 1
describeCertificatesPageSize := 100
for {
select {
case <-ctx.Done():
return nil, ctx.Err()
default:
}
describeCertificatesReq := &ksyunkcmsdk.DescribeCertificatesRequest{
Region: lo.ToPtr(c.config.Region),
Page: lo.ToPtr(int32(describeCertificatesPage)),
PageSize: lo.ToPtr(int32(describeCertificatesPageSize)),
}
describeCertificatesResp, err := c.sdkClient.DescribeCertificatesWithContext(ctx, describeCertificatesReq)
c.logger.Debug("sdk request 'kcm.DescribeCertificates'", slog.Any("request", describeCertificatesReq), slog.Any("response", describeCertificatesResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'kcm.DescribeCertificates': %w", err)
}
if describeCertificatesResp.CertificateSet == nil {
break
}
for _, certItem := range describeCertificatesResp.CertificateSet {
// 如果已存在相同证书,直接返回
if xcert.EqualCertificatesFromPEM(certPEM, certItem.PublicKey) {
c.logger.Info("ssl certificate already exists")
return &UploadResult{
CertId: certItem.CertificateId,
CertName: certItem.CertificateName,
}, nil
}
}
if len(describeCertificatesResp.CertificateSet) < describeCertificatesPageSize {
break
}
describeCertificatesPage++
}
// 托管证书到 KCM
upres, err := c.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
// 创建证书
// REF: https://apiexplorer.ksyun.com/#/api/96/CreateCertificate/2016-03-04/1013
createCertificateReq := &ksyunkcmsdk.CreateCertificateRequest{
Region: lo.ToPtr(c.config.Region),
CertificateName: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
Description: lo.ToPtr("upload from certimate"),
Source: lo.ToPtr("kcm"),
SSLCertificateId: lo.ToPtr(upres.CertId),
}
createCertificateResp, err := c.sdkClient.CreateCertificateWithContext(ctx, createCertificateReq)
c.logger.Debug("sdk request 'kcm.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'kcm.CreateCertificate': %w", err)
}
return &UploadResult{
CertId: createCertificateResp.Certificate.CertificateId,
CertName: createCertificateResp.Certificate.CertificateName,
}, nil
}
func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*ReplaceResult, error) {
// 托管证书到 KCM
upres, err := c.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM)
if err != nil {
return nil, fmt.Errorf("failed to upload certificate file: %w", err)
}
// 更新证书
// REF: https://apiexplorer.ksyun.com/#/api/96/ModifyCertificate/2016-03-04/1013
modifyCertificateReq := &ksyunkcmsdk.ModifyCertificateRequest{
Region: lo.ToPtr(c.config.Region),
Description: lo.ToPtr("upload from certimate"),
SSLCertificateId: lo.ToPtr(upres.CertId),
}
modifyCertificateResp, err := c.sdkClient.ModifyCertificateWithContext(ctx, modifyCertificateReq)
c.logger.Debug("sdk request 'kcm.ModifyCertificate'", slog.Any("request", modifyCertificateReq), slog.Any("response", modifyCertificateResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'kcm.ModifyCertificate': %w", err)
}
return &ReplaceResult{}, nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ksyunkcmsdk.Client, error) {
client, err := ksyunkcmsdk.NewClient(
ksyunkcmsdk.WithAkSk(accessKeyId, secretAccessKey),
)
if err != nil {
return nil, err
}
return client, nil
}
@@ -0,0 +1,53 @@
package ksyunslb_test
import (
"testing"
"github.com/certimate-go/certimate/pkg/core/certmgr/internal/tester"
impl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ksyun-slb"
)
var (
fp = tester.Args("KSYUNSLB_")
fTestCertPath string
fTestKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
)
func init() {
fp.DefineString(&fTestCertPath, "TESTCERTPATH")
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
fp.DefineString(&fRegion, "REGION")
}
/*
Shell command to run this test:
go test -v ./ksyun_slb_test.go -args \
--KSYUNSLB_TESTCERTPATH="/path/to/your-test-cert.pem" \
--KSYUNSLB_TESTKEYPATH="/path/to/your-test-key.pem" \
--KSYUNSLB_ACCESSKEYID="your-access-key-id" \
--KSYUNSLB_SECRETACCESSKEY="your-secret-access-key" \
--KSYUNSLB_REGION="cn-beijing-6"
*/
func TestProvider(t *testing.T) {
fp.Parse()
t.Run("Upload", func(t *testing.T) {
provider, err := impl.NewCertmgr(&impl.CertmgrConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
})
if err != nil {
t.Errorf("err: %+v", err)
return
}
tester.TestUpload(t, provider, tester.TestUploadArgs{CertPath: fTestCertPath, KeyPath: fTestKeyPath})
})
}
@@ -5,15 +5,12 @@ import (
"errors"
"fmt"
"log/slog"
"strings"
"time"
"github.com/KscSDK/ksc-sdk-go/ksc"
ksccdnv1 "github.com/KscSDK/ksc-sdk-go/service/cdnv1"
"github.com/go-viper/mapstructure/v2"
"github.com/samber/lo"
"github.com/certimate-go/certimate/pkg/core"
ksyuncdnsdk "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/cdn"
xcerthostname "github.com/certimate-go/certimate/pkg/utils/cert/hostname"
)
@@ -27,6 +24,8 @@ type DeployerConfig struct {
AccessKeyId string `json:"accessKeyId"`
// 金山云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 金山云项目 ID。
ProjectId int64 `json:"projectId,omitempty"`
// 部署目标。
DeployTarget string `json:"deployTarget"`
// 域名匹配模式。
@@ -42,7 +41,7 @@ type DeployerConfig struct {
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ksccdnv1.Cdnv1
sdkClient *ksyuncdnsdk.Client
}
var _ Provider = (*Deployer)(nil)
@@ -93,8 +92,13 @@ func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*Dep
}
func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM string) error {
_, err := d.getAllDomains(ctx)
if err != nil {
return err
}
// 获取待部署的域名列表
var domains []string
var domainIds []string
switch d.config.DomainMatchPattern {
case "", DOMAIN_MATCH_PATTERN_EXACT:
{
@@ -102,7 +106,20 @@ func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM strin
return fmt.Errorf("config `domain` is required")
}
domains = []string{d.config.Domain}
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return err
}
domains := lo.Filter(domainCandidates, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) bool {
return d.config.Domain == domainItem.DomainName
})
if len(domains) == 0 {
return fmt.Errorf("could not find domain")
}
domainIds = lo.Map(domains, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) string {
return domainItem.DomainId
})
}
case DOMAIN_MATCH_PATTERN_WILDCARD:
@@ -111,21 +128,21 @@ func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM strin
return fmt.Errorf("config `domain` is required")
}
if strings.HasPrefix(d.config.Domain, "*.") {
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatch(d.config.Domain, domain)
})
if len(domains) == 0 {
return fmt.Errorf("could not find any domains matched by wildcard")
}
} else {
domains = []string{d.config.Domain}
domainCandidates, err := d.getAllDomains(ctx)
if err != nil {
return err
}
domains := lo.Filter(domainCandidates, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) bool {
return xcerthostname.IsMatch(d.config.Domain, domainItem.DomainName)
})
if len(domains) == 0 {
return fmt.Errorf("could not find any domains matched by wildcard")
}
domainIds = lo.Map(domains, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) string {
return domainItem.DomainId
})
}
case DOMAIN_MATCH_PATTERN_CERTSAN:
@@ -135,12 +152,16 @@ func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM strin
return err
}
domains = lo.Filter(domainCandidates, func(domain string, _ int) bool {
return xcerthostname.IsMatchByCertificatePEM(certPEM, domain)
domains := lo.Filter(domainCandidates, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) bool {
return xcerthostname.IsMatchByCertificatePEM(certPEM, domainItem.DomainName)
})
if len(domains) == 0 {
return fmt.Errorf("could not find any domains matched by certificate")
}
domainIds = lo.Map(domains, func(domainItem *ksyuncdnsdk.CDNDomain, _ int) string {
return domainItem.DomainId
})
}
default:
@@ -148,18 +169,18 @@ func (d *Deployer) deployToDomain(ctx context.Context, certPEM, privkeyPEM strin
}
// 遍历更新域名证书
if len(domains) == 0 {
if len(domainIds) == 0 {
d.logger.Info("no cdn domains to deploy")
} else {
d.logger.Info("found cdn domains to deploy", slog.Any("domains", domains))
d.logger.Info("found cdn domains to deploy", slog.Any("domainIds", domainIds))
var errs []error
for _, domain := range domains {
for _, domainId := range domainIds {
select {
case <-ctx.Done():
return ctx.Err()
default:
if err := d.updateDomainCertificate(ctx, domain, certPEM, privkeyPEM); err != nil {
if err := d.updateDomainCertificate(ctx, domainId, certPEM, privkeyPEM); err != nil {
errs = append(errs, err)
}
}
@@ -180,14 +201,14 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
// 更新证书
// REF: https://docs.ksyun.com/documents/259
setCertificateInput := map[string]any{
"CertificateId": d.config.CertificateId,
"CertificateName": fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
"ServerCertificate": certPEM,
"PrivateKey": privkeyPEM,
setCertificateReq := &ksyuncdnsdk.SetCertificateRequest{
CertificateId: lo.ToPtr(d.config.CertificateId),
CertificateName: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
ServerCertificate: lo.ToPtr(certPEM),
PrivateKey: lo.ToPtr(privkeyPEM),
}
setCertificateOutput, err := d.sdkClient.SetCertificatePostWithContext(ctx, &setCertificateInput)
d.logger.Debug("sdk request 'cdn.SetCertificate'", slog.Any("request", setCertificateInput), slog.Any("response", setCertificateOutput))
setCertificateResp, err := d.sdkClient.SetCertificateWithContext(ctx, setCertificateReq)
d.logger.Debug("sdk request 'cdn.SetCertificate'", slog.Any("request", setCertificateReq), slog.Any("response", setCertificateResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdn.SetCertificate': %w", err)
}
@@ -195,8 +216,8 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
return nil
}
func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {
domains := make([]string, 0)
func (d *Deployer) getAllDomains(ctx context.Context) ([]*ksyuncdnsdk.CDNDomain, error) {
domains := make([]*ksyuncdnsdk.CDNDomain, 0)
// 查询域名列表
// REF: https://docs.ksyun.com/documents/198
@@ -209,38 +230,28 @@ func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {
default:
}
getCdnDomainsInput := map[string]any{
"PageNumber": getCdnDomainsPageNumber,
"PageSize": getCdnDomainsPageSize,
getCdnDomainsReq := &ksyuncdnsdk.GetCDNDomainsRequest{
ProjectId: lo.IfF(d.config.ProjectId != 0, func() *int64 { return lo.ToPtr(d.config.ProjectId) }).Else(nil),
PageNumber: lo.ToPtr(int32(getCdnDomainsPageNumber)),
PageSize: lo.ToPtr(int32(getCdnDomainsPageSize)),
}
getCdnDomainsOutput, err := d.sdkClient.GetCdnDomainsPostWithContext(ctx, &getCdnDomainsInput)
d.logger.Debug("sdk request 'cdn.GetCdnDomains'", slog.Any("request", getCdnDomainsInput), slog.Any("response", getCdnDomainsOutput))
getCdnDomainsResp, err := d.sdkClient.GetCDNDomainsWithContext(ctx, getCdnDomainsReq)
d.logger.Debug("sdk request 'cdn.GetCdnDomains'", slog.Any("request", getCdnDomainsReq), slog.Any("response", getCdnDomainsResp))
if err != nil {
return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetCdnDomains': %w", err)
}
type GetCdnDomainsResponse struct {
PageNumber int32 `json:"PageNumber"`
PageSize int32 `json:"PageSize"`
TotalCount int32 `json:"TotalCount"`
Domains []*struct {
DomainId string `json:"DomainId"`
DomainName string `json:"DomainName"`
Cname string `json:"Cname"`
CdnType string `json:"CdnType"`
CreatedTime string `json:"CreatedTime"`
ModifiedTime string `json:"ModifiedTime"`
Region string `json:"Region"`
} `json:"Domains"`
}
var getCdnDomainsResp *GetCdnDomainsResponse
mapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp)
if getCdnDomainsResp == nil {
if getCdnDomainsResp.Domains == nil {
break
}
ignoredStatuses := []string{"offline", "icp_checking", "icp_check_failed", "locking", "locked"}
for _, domainItem := range getCdnDomainsResp.Domains {
domains = append(domains, domainItem.DomainName)
if lo.Contains(ignoredStatuses, domainItem.DomainStatus) {
continue
}
domains = append(domains, domainItem)
}
if len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize {
@@ -253,84 +264,18 @@ func (d *Deployer) getAllDomains(ctx context.Context) ([]string, error) {
return domains, nil
}
func (d *Deployer) findDomainIdByDomain(ctx context.Context, domain string) (string, error) {
// 查询域名列表
// REF: https://docs.ksyun.com/documents/198
getCdnDomainsPageNumber := 1
getCdnDomainsPageSize := 100
for {
select {
case <-ctx.Done():
return "", ctx.Err()
default:
}
getCdnDomainsInput := map[string]any{
"PageNumber": getCdnDomainsPageNumber,
"PageSize": getCdnDomainsPageSize,
"DomainName": domain,
"FuzzyMatch": "off",
}
getCdnDomainsOutput, err := d.sdkClient.GetCdnDomainsPostWithContext(ctx, &getCdnDomainsInput)
d.logger.Debug("sdk request 'cdn.GetCdnDomains'", slog.Any("request", getCdnDomainsInput), slog.Any("response", getCdnDomainsOutput))
if err != nil {
return "", fmt.Errorf("failed to execute sdk request 'cdn.GetCdnDomains': %w", err)
}
type GetCdnDomainsResponse struct {
PageNumber int32 `json:"PageNumber"`
PageSize int32 `json:"PageSize"`
TotalCount int32 `json:"TotalCount"`
Domains []*struct {
DomainId string `json:"DomainId"`
DomainName string `json:"DomainName"`
Cname string `json:"Cname"`
CdnType string `json:"CdnType"`
CreatedTime string `json:"CreatedTime"`
ModifiedTime string `json:"ModifiedTime"`
Region string `json:"Region"`
} `json:"Domains"`
}
var getCdnDomainsResp *GetCdnDomainsResponse
mapstructure.Decode(getCdnDomainsOutput, &getCdnDomainsResp)
if getCdnDomainsResp == nil {
break
}
for _, domainItem := range getCdnDomainsResp.Domains {
if strings.EqualFold(domainItem.DomainName, domain) {
return domainItem.DomainId, nil
}
}
if len(getCdnDomainsResp.Domains) < getCdnDomainsPageSize {
break
}
getCdnDomainsPageNumber++
}
return "", fmt.Errorf("could not find domain '%s'", domain)
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, certPEM, privkeyPEM string) error {
// 获取域名 ID
domainId, err := d.findDomainIdByDomain(ctx, domain)
if err != nil {
return err
}
func (d *Deployer) updateDomainCertificate(ctx context.Context, cloudDomainId string, certPEM, privkeyPEM string) error {
// 为加速域名配置证书接口
// REF: https://docs.ksyun.com/documents/261
configCertificateInput := map[string]any{
"Enable": "on",
"DomainIds": domainId,
"CertificateName": fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
"ServerCertificate": certPEM,
"PrivateKey": privkeyPEM,
configCertificateReq := &ksyuncdnsdk.ConfigCertificateRequest{
Enable: lo.ToPtr("on"),
DomainIds: lo.ToPtr(cloudDomainId),
CertificateName: lo.ToPtr(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())),
ServerCertificate: lo.ToPtr(certPEM),
PrivateKey: lo.ToPtr(privkeyPEM),
}
configCertificateOutput, err := d.sdkClient.ConfigCertificatePostWithContext(ctx, &configCertificateInput)
d.logger.Debug("sdk request 'cdn.ConfigCertificate'", slog.Any("request", configCertificateInput), slog.Any("response", configCertificateOutput))
configCertificateResp, err := d.sdkClient.ConfigCertificateWithContext(ctx, configCertificateReq)
d.logger.Debug("sdk request 'cdn.ConfigCertificate'", slog.Any("request", configCertificateReq), slog.Any("response", configCertificateResp))
if err != nil {
return fmt.Errorf("failed to execute sdk request 'cdn.ConfigCertificate': %w", err)
}
@@ -338,8 +283,13 @@ func (d *Deployer) updateDomainCertificate(ctx context.Context, domain string, c
return nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ksccdnv1.Cdnv1, error) {
region := "cn-beijing-6"
client := ksccdnv1.SdkNew(ksc.NewClient(accessKeyId, secretAccessKey), &ksc.Config{Region: &region})
func createSDKClient(accessKeyId, secretAccessKey string) (*ksyuncdnsdk.Client, error) {
client, err := ksyuncdnsdk.NewClient(
ksyuncdnsdk.WithAkSk(accessKeyId, secretAccessKey),
)
if err != nil {
return nil, err
}
return client, nil
}
@@ -4,12 +4,9 @@ import (
"context"
"fmt"
"log/slog"
"time"
"github.com/KscSDK/ksc-sdk-go/ksc"
ksckcm "github.com/KscSDK/ksc-sdk-go/service/kcm"
"github.com/certimate-go/certimate/pkg/core"
cmgrimpl "github.com/certimate-go/certimate/pkg/core/certmgr/providers/ksyun-slb"
)
type (
@@ -22,6 +19,10 @@ type DeployerConfig struct {
AccessKeyId string `json:"accessKeyId"`
// 金山云 SecretAccessKey。
SecretAccessKey string `json:"secretAccessKey"`
// 金山云项目 ID。
ProjectId int64 `json:"projectId,omitempty"`
// 金山云地域。
Region string `json:"region"`
// 部署目标。
DeployTarget string `json:"deployTarget"`
// 证书 ID。
@@ -30,9 +31,9 @@ type DeployerConfig struct {
}
type Deployer struct {
config *DeployerConfig
logger *slog.Logger
sdkClient *ksckcm.Kcm
config *DeployerConfig
logger *slog.Logger
sdkCertmgr core.Certmgr
}
var _ Provider = (*Deployer)(nil)
@@ -42,15 +43,20 @@ func NewDeployer(config *DeployerConfig) (*Deployer, error) {
return nil, fmt.Errorf("the configuration of the deployer provider is nil")
}
client, err := createSDKClient(config.AccessKeyId, config.SecretAccessKey)
pcertmgr, err := cmgrimpl.NewCertmgr(&cmgrimpl.CertmgrConfig{
AccessKeyId: config.AccessKeyId,
SecretAccessKey: config.SecretAccessKey,
ProjectId: config.ProjectId,
Region: config.Region,
})
if err != nil {
return nil, fmt.Errorf("could not create client: %w", err)
return nil, fmt.Errorf("could not create certmgr: %w", err)
}
return &Deployer{
config: config,
logger: slog.Default(),
sdkClient: client,
config: config,
logger: slog.Default(),
sdkCertmgr: pcertmgr,
}, nil
}
@@ -60,6 +66,8 @@ func (d *Deployer) SetLogger(logger *slog.Logger) {
} else {
d.logger = logger
}
d.sdkCertmgr.SetLogger(logger)
}
func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*DeployResult, error) {
@@ -82,25 +90,13 @@ func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM
return fmt.Errorf("config `certificateId` is required")
}
// 更新证书信息
// REF: https://docs.ksyun.com/documents/2121
modifyCertificateInput := map[string]any{
"CertificateId": d.config.CertificateId,
"CertificateName": fmt.Sprintf("certimate_%d", time.Now().UnixMilli()),
"PublicKey": certPEM,
"PrivateKey": privkeyPEM,
}
modifyCertificateOutput, err := d.sdkClient.ModifyCertificateWithContext(ctx, &modifyCertificateInput)
d.logger.Debug("sdk request 'kcm.ModifyCertificate'", slog.Any("request", modifyCertificateInput), slog.Any("response", modifyCertificateOutput))
// 替换证书
rplres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM)
if err != nil {
return fmt.Errorf("failed to execute sdk request 'kcm.ModifyCertificate': %w", err)
return fmt.Errorf("failed to replace certificate file: %w", err)
} else {
d.logger.Info("ssl certificate replaced", slog.Any("result", rplres))
}
return nil
}
func createSDKClient(accessKeyId, secretAccessKey string) (*ksckcm.Kcm, error) {
region := "cn-beijing-6"
client := ksckcm.SdkNew(ksc.NewClient(accessKeyId, secretAccessKey), &ksc.Config{Region: &region})
return client, nil
}
@@ -13,6 +13,7 @@ var (
fTestKeyPath string
fAccessKeyId string
fSecretAccessKey string
fRegion string
fCertificateId string
)
@@ -21,6 +22,7 @@ func init() {
fp.DefineString(&fTestKeyPath, "TESTKEYPATH")
fp.DefineString(&fAccessKeyId, "ACCESSKEYID")
fp.DefineString(&fSecretAccessKey, "SECRETACCESSKEY")
fp.DefineString(&fRegion, "REGION")
fp.DefineString(&fCertificateId, "CERTIFICATEID")
}
@@ -32,6 +34,7 @@ Shell command to run this test:
--KSYUNSLB_TESTKEYPATH="/path/to/your-test-key.pem" \
--KSYUNSLB_ACCESSKEYID="your-access-key-id" \
--KSYUNSLB_SECRETACCESSKEY="your-secret-access-key" \
--KSYUNSLB_REGION="cn-beijing-6" \
--KSYUNSLB_CERTIFICATEID="your-certificate-id"
*/
func TestProvider(t *testing.T) {
@@ -41,6 +44,7 @@ func TestProvider(t *testing.T) {
provider, err := impl.NewDeployer(&impl.DeployerConfig{
AccessKeyId: fAccessKeyId,
SecretAccessKey: fSecretAccessKey,
Region: fRegion,
DeployTarget: impl.DEPLOY_TARGET_CERTIFICATE,
CertificateId: fCertificateId,
})
@@ -0,0 +1,51 @@
package cdn
import (
"context"
"net/http"
)
type ConfigCertificateRequest struct {
Enable *string `json:"Enable,omitempty"`
DomainIds *string `json:"DomainIds,omitempty"`
CertificateId *string `json:"CertificateId,omitempty"`
CertificateName *string `json:"CertificateName,omitempty"`
ServerCertificate *string `json:"ServerCertificate,omitempty"`
PrivateKey *string `json:"PrivateKey,omitempty"`
}
type ConfigCertificateResponse struct {
sdkResponseBase
CertificateId string `json:"CertificateId,omitempty"`
}
func (c *Client) ConfigCertificate(req *ConfigCertificateRequest) (*ConfigCertificateResponse, error) {
return c.ConfigCertificateWithContext(context.Background(), req)
}
func (c *Client) ConfigCertificateWithContext(ctx context.Context, req *ConfigCertificateRequest) (*ConfigCertificateResponse, error) {
params := &struct {
ConfigCertificateRequest `json:",inline"`
Action string
Version string
}{
ConfigCertificateRequest: *req,
Action: "ConfigCertificate",
Version: "2016-09-01",
}
httpreq, err := c.newRequest(http.MethodPost, "/2016-09-01/cert/ConfigCertificate", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &ConfigCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,55 @@
package cdn
import (
"context"
"net/http"
)
type GetCDNDomainsRequest struct {
ProjectId *int64 `json:"ProjectId,omitempty"`
DomainName *string `json:"DomainName,omitempty"`
DomainStatus *string `json:"DomainStatus,omitempty"`
FuzzyMatch *bool `json:"FuzzyMatch,omitempty"`
CdnType *bool `json:"CdnType,omitempty"`
PageNumber *int32 `json:"PageNumber,omitempty"`
PageSize *int32 `json:"PageSize,omitempty"`
}
type GetCdnDomainsResponse struct {
sdkResponseBase
Domains []*CDNDomain `json:"Domains"`
PageNumber int32 `json:"PageNumber,omitempty"`
PageSize int32 `json:"PageSize,omitempty"`
TotalCount int32 `json:"TotalCount,omitempty"`
}
func (c *Client) GetCDNDomains(req *GetCDNDomainsRequest) (*GetCdnDomainsResponse, error) {
return c.GetCDNDomainsWithContext(context.Background(), req)
}
func (c *Client) GetCDNDomainsWithContext(ctx context.Context, req *GetCDNDomainsRequest) (*GetCdnDomainsResponse, error) {
params := &struct {
GetCDNDomainsRequest `json:",inline"`
Action string
Version string
}{
GetCDNDomainsRequest: *req,
Action: "GetCdnDomains",
Version: "2019-06-01",
}
httpreq, err := c.newRequest(http.MethodGet, "/2019-06-01/GetCdnDomains", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &GetCdnDomainsResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,49 @@
package cdn
import (
"context"
"net/http"
)
type SetCertificateRequest struct {
CertificateId *string `json:"CertificateId,omitempty"`
CertificateName *string `json:"CertificateName,omitempty"`
ServerCertificate *string `json:"ServerCertificate,omitempty"`
PrivateKey *string `json:"PrivateKey,omitempty"`
}
type SetCertificateResponse struct {
sdkResponseBase
CertificateId string `json:"CertificateId,omitempty"`
}
func (c *Client) SetCertificate(req *SetCertificateRequest) (*SetCertificateResponse, error) {
return c.SetCertificateWithContext(context.Background(), req)
}
func (c *Client) SetCertificateWithContext(ctx context.Context, req *SetCertificateRequest) (*SetCertificateResponse, error) {
params := &struct {
SetCertificateRequest `json:",inline"`
Action string
Version string
}{
SetCertificateRequest: *req,
Action: "SetCertificate",
Version: "2016-09-01",
}
httpreq, err := c.newRequest(http.MethodPost, "/2016-09-01/cert/SetCertificate", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &SetCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+54
View File
@@ -0,0 +1,54 @@
// A simple SDK client for KingsoftCloud CDN.
// API documentation: https://apiexplorer.ksyun.com/#/api/home
package cdn
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
common "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/zz-shared-common"
)
const (
service = "cdn"
endpoint = "https://" + service + ".api.ksyun.com"
)
type Client struct {
client *common.Client
}
func NewClient(optFns ...common.OptionsFunc) (*Client, error) {
client, err := common.NewClient(endpoint, service, optFns...)
if err != nil {
return nil, err
}
return &Client{client: client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {
return c.client.NewRequest(method, path, params)
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
return c.client.DoRequest(req)
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
resp, err := c.client.DoRequestWithResult(req, res)
if err == nil {
if err := res.GetAPIError(); err != nil {
return resp, fmt.Errorf("sdkerr: api error: %s", err.Error())
}
}
return resp, err
}
+14
View File
@@ -0,0 +1,14 @@
package cdn
type CDNDomain struct {
Region string `json:"Region"`
DomainId string `json:"DomainId"`
DomainName string `json:"DomainName"`
Description string `json:"Description"`
Cname string `json:"Cname"`
CdnType string `json:"CdnType"`
CdnSubType string `json:"CdnSubType"`
DomainStatus string `json:"DomainStatus"`
CreatedTime string `json:"CreatedTime"`
ModifiedTime string `json:"ModifiedTime"`
}
+9
View File
@@ -0,0 +1,9 @@
package cdn
import (
common "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/zz-shared-common"
)
func WithAkSk(ak, sk string) common.OptionsFunc {
return common.WithAkSk(ak, sk)
}
+34
View File
@@ -0,0 +1,34 @@
package cdn
import "fmt"
type sdkResponse interface {
GetAPIError() error
}
type sdkResponseBase struct {
RequestId *string `json:"RequestId,omitempty"`
Error *sdkAPIError `json:"Error,omitempty"`
}
type sdkAPIError struct {
Type string `json:"Type,omitempty"`
Code string `json:"Code"`
Message string `json:"Message"`
}
func (e sdkAPIError) Error() string {
if e.Type == "" {
return fmt.Sprintf("[%s] %s ", e.Code, e.Message)
}
return fmt.Sprintf("[%s_%s] %s ", e.Type, e.Code, e.Message)
}
func (r *sdkResponseBase) GetAPIError() error {
if r.Error != nil {
return r.Error
}
return nil
}
var _ sdkResponse = (*sdkResponseBase)(nil)
@@ -0,0 +1,61 @@
package kcm
import (
"context"
"net/http"
qs "github.com/google/go-querystring/query"
)
type CreateCertificateRequest struct {
Region *string `json:"Region,omitempty" url:"Region,omitempty"`
CertificateName *string `json:"CertificateName,omitempty" url:"CertificateName,omitempty"`
Description *string `json:"Description,omitempty" url:"Description,omitempty"`
PublicKey *string `json:"PublicKey,omitempty" url:"-"`
PrivateKey *string `json:"PrivateKey,omitempty" url:"-"`
CertificateType *string `json:"CertificateType,omitempty" url:"CertificateType,omitempty"`
Source *string `json:"Source,omitempty" url:"Source,omitempty"`
SSLCertificateId *string `json:"SslCertificateId,omitempty" url:"SslCertificateId,omitempty"`
}
type CreateCertificateResponse struct {
sdkResponseBase
Certificate *LBCertificate `json:"Certificate,omitempty"`
}
func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
return c.CreateCertificateWithContext(context.Background(), req)
}
func (c *Client) CreateCertificateWithContext(ctx context.Context, req *CreateCertificateRequest) (*CreateCertificateResponse, error) {
params := &struct {
CreateCertificateRequest `json:",inline"`
Action string
Version string
}{
CreateCertificateRequest: *req,
Action: "CreateCertificate",
Version: "2016-03-04",
}
httpreq, err := c.newRequest(http.MethodPost, "/", params)
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &CreateCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,48 @@
package kcm
import (
"context"
"net/http"
)
type DescribeCertificatesRequest struct {
Region *string `json:"Region,omitempty"`
Page *int32 `json:"Page,omitempty"`
PageSize *int32 `json:"PageSize,omitempty"`
}
type DescribeCertificatesResponse struct {
sdkResponseBase
CertificateSet []*LBCertificate `json:"CertificateSet"`
}
func (c *Client) DescribeCertificates(req *DescribeCertificatesRequest) (*DescribeCertificatesResponse, error) {
return c.DescribeCertificatesWithContext(context.Background(), req)
}
func (c *Client) DescribeCertificatesWithContext(ctx context.Context, req *DescribeCertificatesRequest) (*DescribeCertificatesResponse, error) {
params := &struct {
DescribeCertificatesRequest `json:",inline"`
Action string
Version string
}{
DescribeCertificatesRequest: *req,
Action: "DescribeCertificates",
Version: "2016-03-04",
}
httpreq, err := c.newRequest(http.MethodGet, "/", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &DescribeCertificatesResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,50 @@
package kcm
import (
"context"
"net/http"
)
type ListUserCertificatesRequest struct {
Page *int32 `json:"Page,omitempty"`
PageSize *int32 `json:"PageSize,omitempty"`
}
type ListUserCertificatesResponse struct {
sdkResponseBase
Success bool `json:"Success"`
Ret *struct {
Certs []*UserCertificate `json:"Certs"`
} `json:"Ret,omitempty"`
}
func (c *Client) ListUserCertificates(req *ListUserCertificatesRequest) (*ListUserCertificatesResponse, error) {
return c.ListUserCertificatesWithContext(context.Background(), req)
}
func (c *Client) ListUserCertificatesWithContext(ctx context.Context, req *ListUserCertificatesRequest) (*ListUserCertificatesResponse, error) {
params := &struct {
ListUserCertificatesRequest `json:",inline"`
Action string
Version string
}{
ListUserCertificatesRequest: *req,
Action: "ListUserCertificates",
Version: "2016-03-04",
}
httpreq, err := c.newRequest(http.MethodGet, "/", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &ListUserCertificatesResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,60 @@
package kcm
import (
"context"
"net/http"
qs "github.com/google/go-querystring/query"
)
type ModifyCertificateRequest struct {
Region *string `json:"Region,omitempty" url:"Region,omitempty"`
CertificateId *string `json:"CertificateId,omitempty" url:"CertificateId,omitempty"`
CertificateName *string `json:"CertificateName,omitempty" url:"CertificateName,omitempty"`
Description *string `json:"Description,omitempty" url:"Description,omitempty"`
PublicKey *string `json:"PublicKey,omitempty" url:"-"`
PrivateKey *string `json:"PrivateKey,omitempty" url:"-"`
SSLCertificateId *string `json:"SslCertificateId,omitempty" url:"SslCertificateId,omitempty"`
}
type ModifyCertificateResponse struct {
sdkResponseBase
Certificate *LBCertificate `json:"Certificate,omitempty"`
}
func (c *Client) ModifyCertificate(req *ModifyCertificateRequest) (*ModifyCertificateResponse, error) {
return c.ModifyCertificateWithContext(context.Background(), req)
}
func (c *Client) ModifyCertificateWithContext(ctx context.Context, req *ModifyCertificateRequest) (*ModifyCertificateResponse, error) {
params := &struct {
ModifyCertificateRequest `json:",inline"`
Action string
Version string
}{
ModifyCertificateRequest: *req,
Action: "ModifyCertificate",
Version: "2016-03-04",
}
httpreq, err := c.newRequest(http.MethodPost, "/", params)
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &ModifyCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,50 @@
package kcm
import (
"context"
"net/http"
)
type UploadCertificateRequest struct {
ProjectId *int64 `json:"ProjectId,omitempty"`
CertName *string `json:"CertName,omitempty"`
CertFile *string `json:"CertFile,omitempty"`
CertKey *string `json:"CertKey,omitempty"`
}
type UploadCertificateResponse struct {
sdkResponseBase
Success bool `json:"Success"`
Ret *UserCertificate `json:"Ret,omitempty"`
}
func (c *Client) UploadCertificate(req *UploadCertificateRequest) (*UploadCertificateResponse, error) {
return c.UploadCertificateWithContext(context.Background(), req)
}
func (c *Client) UploadCertificateWithContext(ctx context.Context, req *UploadCertificateRequest) (*UploadCertificateResponse, error) {
params := &struct {
UploadCertificateRequest `json:",inline"`
Action string
Version string
}{
UploadCertificateRequest: *req,
Action: "UploadCertificate",
Version: "2016-03-04",
}
httpreq, err := c.newRequest(http.MethodPost, "/", params)
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &UploadCertificateResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+54
View File
@@ -0,0 +1,54 @@
// A simple SDK client for KingsoftCloud KCM.
// API documentation: https://apiexplorer.ksyun.com/#/api/home
package kcm
import (
"fmt"
"time"
"github.com/go-resty/resty/v2"
common "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/zz-shared-common"
)
const (
service = "kcm"
endpoint = "https://" + service + ".api.ksyun.com"
)
type Client struct {
client *common.Client
}
func NewClient(optFns ...common.OptionsFunc) (*Client, error) {
client, err := common.NewClient(endpoint, service, optFns...)
if err != nil {
return nil, err
}
return &Client{client: client}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) newRequest(method string, path string, params any) (*resty.Request, error) {
return c.client.NewRequest(method, path, params)
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
return c.client.DoRequest(req)
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
resp, err := c.client.DoRequestWithResult(req, res)
if err == nil {
if err := res.GetAPIError(); err != nil {
return resp, fmt.Errorf("sdkerr: api error: %s", err.Error())
}
}
return resp, err
}
+32
View File
@@ -0,0 +1,32 @@
package kcm
type UserCertificate struct {
CertId string `json:"CertID"`
CertName string `json:"CertName"`
MainDomain string `json:"MainDomain"`
Domains []string `json:"Domains"`
AdditionalDomains []string `json:"AdditionalDomains"`
Brand string `json:"Brand"`
CA string `json:"CA"`
Level string `json:"Level"`
FingerPrint string `json:"FingerPrint"`
DomainCount int32 `json:"DomainCount"`
WildcardCount int32 `json:"WildcardCount"`
IssueTime string `json:"IssueTime"`
ExpireTime string `json:"ExpireTime"`
Source string `json:"Source"`
}
type LBCertificate struct {
CertificateId string `json:"CertificateId"`
CertificateName string `json:"CertificateName"`
CommonName string `json:"CommonName"`
CertAuthority string `json:"CertAuthority"`
CertType string `json:"CertType"`
CertificateType string `json:"CertificateType"`
PublicKey string `json:"PublicKey"`
ExpireTime string `json:"ExpireTime"`
CreateTime string `json:"CreateTime"`
Source string `json:"Source"`
SSLCertificateId string `json:"SslCertificateId,omitempty"`
}
+9
View File
@@ -0,0 +1,9 @@
package kcm
import (
common "github.com/certimate-go/certimate/pkg/sdk3rd/ksyun/zz-shared-common"
)
func WithAkSk(ak, sk string) common.OptionsFunc {
return common.WithAkSk(ak, sk)
}
+30
View File
@@ -0,0 +1,30 @@
package kcm
import "fmt"
type sdkResponse interface {
GetAPIError() error
}
type sdkResponseBase struct {
RequestId *string `json:"RequestId,omitempty"`
Error *sdkAPIError `json:"Error,omitempty"`
}
type sdkAPIError struct {
Code string `json:"Code"`
Message string `json:"Message"`
}
func (e sdkAPIError) Error() string {
return fmt.Sprintf("[%s] %s ", e.Code, e.Message)
}
func (r *sdkResponseBase) GetAPIError() error {
if r.Error != nil {
return r.Error
}
return nil
}
var _ sdkResponse = (*sdkResponseBase)(nil)
+152
View File
@@ -0,0 +1,152 @@
package openapi
import (
"encoding/json"
"fmt"
"net/http"
"net/url"
"reflect"
"strings"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
rc *resty.Client
}
func NewClient(baseUrl string, service string, optFns ...OptionsFunc) (*Client, error) {
options := &Options{}
for _, fn := range optFns {
fn(options)
}
if baseUrl == "" {
return nil, fmt.Errorf("sdkerr: unset baseUrl")
}
if _, err := url.Parse(baseUrl); err != nil {
return nil, fmt.Errorf("sdkerr: invalid baseUrl: %w", err)
}
if service == "" {
return nil, fmt.Errorf("sdkerr: unset service")
}
if options.AccessKeyId == "" {
return nil, fmt.Errorf("sdkerr: unset accessKeyId")
}
if options.SecretAccessKey == "" {
return nil, fmt.Errorf("sdkerr: unset secretAccessKey")
}
signer := &signer{
accessKeyId: options.AccessKeyId,
secretAccessKey: options.SecretAccessKey,
service: service,
}
httper := resty.New().
SetBaseURL(baseUrl).
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(_ *resty.Client, req *http.Request) error {
if err := signer.Sign(req); err != nil {
return fmt.Errorf("sdkerr: sign error: %w", err)
}
return nil
})
return &Client{rc: httper}, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.rc.SetTimeout(timeout)
return c
}
func (c *Client) NewRequest(method string, path string, params any) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
paramsMap := make(map[string]string)
if params != nil {
temp := make(map[string]any)
jsonb, _ := json.Marshal(params)
json.Unmarshal(jsonb, &temp)
for k, v := range temp {
if v == nil {
continue
}
switch reflect.Indirect(reflect.ValueOf(v)).Kind() {
case reflect.String:
paramsMap[k] = v.(string)
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Float32, reflect.Float64:
paramsMap[k] = fmt.Sprintf("%v", v)
default:
jsonb, _ := json.Marshal(v)
paramsMap[k] = string(jsonb)
}
}
}
req := c.rc.R()
req.Method = method
req.URL = path
if strings.ToUpper(method) == http.MethodGet {
req.SetQueryParams(paramsMap)
} else {
req.SetBody(paramsMap)
}
return req, nil
}
func (c *Client) DoRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
// WARN:
// PLEASE DO NOT USE `req.SetBody` or `req.SetFormData` HERE! USE `newRequest` INSTEAD.
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) DoRequestWithResult(req *resty.Request, res any) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.DoRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
}
}
return resp, nil
}
@@ -0,0 +1,15 @@
package openapi
type Options struct {
AccessKeyId string
SecretAccessKey string
}
type OptionsFunc func(*Options)
func WithAkSk(ak, sk string) OptionsFunc {
return func(o *Options) {
o.AccessKeyId = ak
o.SecretAccessKey = sk
}
}
+126
View File
@@ -0,0 +1,126 @@
package openapi
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"io"
"net/http"
"net/url"
"sort"
"strings"
"time"
)
type signer struct {
accessKeyId string
secretAccessKey string
service string
}
func (s *signer) Sign(req *http.Request) error {
// API 签名机制:
// https://docs.ksyun.com/documents/40298
method := strings.ToUpper(req.Method)
query := url.Values{}
if req.URL != nil {
query = req.URL.Query()
}
payload := make(map[string]string)
if method != http.MethodGet && req.Body != nil {
payloadb, err := io.ReadAll(req.Body)
if err != nil {
return err
}
if err := json.Unmarshal(payloadb, &payload); err != nil {
return err
}
req.Body = io.NopCloser(bytes.NewReader(payloadb))
}
params := make(map[string]string)
for k := range query {
params[k] = query.Get(k)
}
for k, v := range payload {
params[k] = v
}
nowUtc := time.Now().UTC()
timestamp := nowUtc.Format("2006-01-02T15:04:05Z")
params["Accesskey"] = s.accessKeyId
params["Service"] = s.service
params["Timestamp"] = timestamp
params["SignatureVersion"] = "1.0"
params["SignatureMethod"] = "HMAC-SHA256"
paramsKeys := make([]string, 0, len(params))
for k := range params {
paramsKeys = append(paramsKeys, k)
}
sort.Strings(paramsKeys)
stringToSign := ""
for i, k := range paramsKeys {
if i > 0 {
stringToSign += "&"
}
stringToSign += escapeQuery(k) + "=" + escapeQuery(params[k])
}
h := hmac.New(sha256.New, []byte(s.secretAccessKey))
h.Write([]byte(stringToSign))
signature := strings.ToLower(hex.EncodeToString(h.Sum(nil)))
if method == http.MethodGet {
query.Set("Accesskey", params["Accesskey"])
query.Set("Service", params["Service"])
query.Set("Timestamp", params["Timestamp"])
query.Set("SignatureVersion", params["SignatureVersion"])
query.Set("SignatureMethod", params["SignatureMethod"])
req.URL.RawQuery = query.Encode() + "&Signature=" + signature
} else {
if _, ok := payload["Action"]; ok {
query.Set("Action", payload["Action"])
delete(payload, "Action")
}
if _, ok := payload["Version"]; ok {
query.Set("Version", payload["Version"])
delete(payload, "Version")
}
payload["Accesskey"] = params["Accesskey"]
payload["Service"] = params["Service"]
payload["Timestamp"] = params["Timestamp"]
payload["SignatureVersion"] = params["SignatureVersion"]
payload["SignatureMethod"] = params["SignatureMethod"]
payload["Signature"] = signature
jsonb, err := json.Marshal(payload)
if err != nil {
return err
}
req.URL.RawQuery = query.Encode()
req.Body = io.NopCloser(bytes.NewReader(jsonb))
req.ContentLength = int64(len(jsonb))
}
return nil
}
func escapeQuery(str string) string {
res := url.QueryEscape(str)
res = strings.ReplaceAll(res, "+", "%20")
return res
}
@@ -24,6 +24,16 @@ const BizDeployNodeConfigFieldsProviderKsyunSLB = () => {
return (
<>
<Form.Item
name={[parentNamePath, "region"]}
initialValue={initialValues.region}
label={t("workflow_node.deploy.form.ksyun_slb_region.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow_node.deploy.form.ksyun_slb_region.tooltip") }}></span>}
>
<Input placeholder={t("workflow_node.deploy.form.ksyun_slb_region.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "deployTarget"]}
initialValue={initialValues.deployTarget}
@@ -56,6 +66,7 @@ const BizDeployNodeConfigFieldsProviderKsyunSLB = () => {
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
region: "",
deployTarget: DEPLOY_TARGET_CERTIFICATE,
certificateId: "",
};
@@ -66,6 +77,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType<typeof getI18n> })
return z
.object({
region: z.string().nonempty(),
deployTarget: z.enum([DEPLOY_TARGET_CERTIFICATE]),
certificateId: z.string().nullish(),
})
@@ -1836,6 +1836,11 @@
"placeholder": "Please enter Kingsoft Cloud CDN certificate ID",
"tooltip": "For more information, see <a href=\"https://cdn.console.ksyun.com/\" target=\"_blank\">https://cdn.console.ksyun.com/</a>"
},
"ksyun_slb_region": {
"label": "Kingsoft Cloud region",
"placeholder": "Please enter Kingsoft Cloud SLB region (e.g. cn-beijing-6)",
"tooltip": "For more information, see <a href=\"https://endocs.ksyun.com/directories/1164\" target=\"_blank\">https://endocs.ksyun.com/directories/1164</a>"
},
"ksyun_slb_deploy_target": {
"option": {
"certificate": {
@@ -1835,6 +1835,11 @@
"placeholder": "请输入金山云 CDN 证书 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://cdn.console.ksyun.com/\" target=\"_blank\">https://cdn.console.ksyun.com/</a>"
},
"ksyun_slb_region": {
"label": "金山云服务地域",
"placeholder": "请输入金山云 SLB 服务地域(例如:cn-beijing-6)",
"tooltip": "这是什么?请参阅 <a href=\"https://docs.ksyun.com/documents/1164\" target=\"_blank\">https://docs.ksyun.com/documents/1164</a>"
},
"ksyun_slb_deploy_target": {
"option": {
"certificate": {