From 66710f6ca2dba928b26ff3cce7dd183a674a7adb Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 15 May 2026 13:11:22 +0800 Subject: [PATCH] feat(provider): new deployment provider: zenlayer zga --- go.mod | 1 + go.sum | 2 + internal/certmgmt/deployers/sp_zenlayer_ga.go | 29 +++ internal/domain/access.go | 6 + internal/domain/provider.go | 2 + .../providers/zenlayer-ga/internal/client.go | 46 ++++ .../providers/zenlayer-ga/zenlayer_ga.go | 202 ++++++++++++++++++ .../deployer/providers/zenlayer-ga/consts.go | 8 + .../providers/zenlayer-ga/internal/client.go | 40 ++++ .../providers/zenlayer-ga/zenlayer_ga.go | 200 +++++++++++++++++ .../providers/zenlayer-ga/zenlayer_ga_test.go | 111 ++++++++++ ui/public/imgs/providers/zenlayer.svg | 1 + .../forms/AccessConfigFieldsProvider.tsx | 2 + .../AccessConfigFieldsProviderZenlayer.tsx | 75 +++++++ .../BizDeployNodeConfigFieldsProvider.tsx | 2 + ...ployNodeConfigFieldsProviderZenlayerGA.tsx | 120 +++++++++++ ui/src/domain/provider.ts | 4 + ui/src/i18n/locales/en/nls.access.json | 9 + ui/src/i18n/locales/en/nls.provider.json | 2 + .../i18n/locales/en/nls.workflow.nodes.json | 8 + ui/src/i18n/locales/zh/nls.access.json | 9 + ui/src/i18n/locales/zh/nls.provider.json | 2 + .../i18n/locales/zh/nls.workflow.nodes.json | 8 + 23 files changed, 889 insertions(+) create mode 100644 internal/certmgmt/deployers/sp_zenlayer_ga.go create mode 100644 pkg/core/certmgr/providers/zenlayer-ga/internal/client.go create mode 100644 pkg/core/certmgr/providers/zenlayer-ga/zenlayer_ga.go create mode 100644 pkg/core/deployer/providers/zenlayer-ga/consts.go create mode 100644 pkg/core/deployer/providers/zenlayer-ga/internal/client.go create mode 100644 pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga.go create mode 100644 pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga_test.go create mode 100644 ui/public/imgs/providers/zenlayer.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderZenlayer.tsx create mode 100644 ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderZenlayerGA.tsx diff --git a/go.mod b/go.mod index fbe17a35a..4d7e2d8bc 100644 --- a/go.mod +++ b/go.mod @@ -256,6 +256,7 @@ require ( github.com/tinylib/msgp v1.6.4 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/zeebo/xxh3 v1.1.0 // indirect + github.com/zenlayer/zenlayercloud-sdk-go v0.2.33 // indirect golang.org/x/image v0.40.0 // indirect golang.org/x/mod v0.35.0 // indirect golang.org/x/net v0.54.0 // indirect diff --git a/go.sum b/go.sum index aa77b9de6..325ec1c7c 100644 --- a/go.sum +++ b/go.sum @@ -982,6 +982,8 @@ github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs= github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s= +github.com/zenlayer/zenlayercloud-sdk-go v0.2.33 h1:lfzoPBtKyCFktOk69IfvpMI3n3o6YHSV/NVp3qQQ1mU= +github.com/zenlayer/zenlayercloud-sdk-go v0.2.33/go.mod h1:iOR3Pzb2t3Z0+UvXhdUqi7nhwZM5Ux2oH4ixKH+qe88= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ= diff --git a/internal/certmgmt/deployers/sp_zenlayer_ga.go b/internal/certmgmt/deployers/sp_zenlayer_ga.go new file mode 100644 index 000000000..95fb9aa66 --- /dev/null +++ b/internal/certmgmt/deployers/sp_zenlayer_ga.go @@ -0,0 +1,29 @@ +package deployers + +import ( + "fmt" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/deployer" + zenlayerga "github.com/certimate-go/certimate/pkg/core/deployer/providers/zenlayer-ga" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + Registries.MustRegister(domain.DeploymentProviderTypeZenlayerGA, func(options *ProviderFactoryOptions) (deployer.Provider, error) { + credentials := domain.AccessConfigForZenlayer{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := zenlayerga.NewDeployer(&zenlayerga.DeployerConfig{ + AccessKeyId: credentials.AccessKeyId, + AccessKeyPassword: credentials.AccessKeyPassword, + ResourceGroupId: credentials.ResourceGroupId, + ResourceType: xmaps.GetString(options.ProviderExtendedConfig, "resourceType"), + AcceleratorId: xmaps.GetString(options.ProviderExtendedConfig, "acceleratorId"), + CertificateId: xmaps.GetString(options.ProviderExtendedConfig, "certificateId"), + }) + return provider, err + }) +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 73cf44aea..2ed71e329 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -621,6 +621,12 @@ type AccessConfigForXinnet struct { ApiPassword string `json:"apiPassword"` } +type AccessConfigForZenlayer struct { + AccessKeyId string `json:"accessKeyId"` + AccessKeyPassword string `json:"accessKeyPassword"` + ResourceGroupId string `json:"resourceGroupId,omitempty"` +} + type AccessConfigForZeroSSL struct { AccessConfigForACMEExternalAccountBinding } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 832313a4f..08cb4b92d 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -123,6 +123,7 @@ const ( AccessProviderTypeWeComBot = AccessProviderType("wecombot") AccessProviderTypeWestcn = AccessProviderType("westcn") AccessProviderTypeXinnet = AccessProviderType("xinnet") + AccessProviderTypeZenlayer = AccessProviderType("zenlayer") AccessProviderTypeZeroSSL = AccessProviderType("zerossl") ) @@ -391,6 +392,7 @@ const ( DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro") DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate") DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook) + DeploymentProviderTypeZenlayerGA = DeploymentProviderType(AccessProviderTypeZenlayer + "-ga") ) type NotificationProviderType string diff --git a/pkg/core/certmgr/providers/zenlayer-ga/internal/client.go b/pkg/core/certmgr/providers/zenlayer-ga/internal/client.go new file mode 100644 index 000000000..834071bde --- /dev/null +++ b/pkg/core/certmgr/providers/zenlayer-ga/internal/client.go @@ -0,0 +1,46 @@ +package internal + +import ( + "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/common" + zga "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/zga20230706" +) + +// This is a partial copy of https://github.com/zenlayer/zenlayercloud-sdk-go/blob/main/zenlayercloud/zga20230706/client.go +// to lightweight the vendor packages in the built binary. +type ZgaClient struct { + *common.Client +} + +func NewZgaClient(config *common.Config, secretKeyId, secretKeyPassword string) (client *ZgaClient, err error) { + client = &ZgaClient{} + + err = client.InitWithCredential(common.NewCredential(secretKeyId, secretKeyPassword)) + if err != nil { + return nil, err + } + + err = client.WithConfig(config) + if err != nil { + return nil, err + } + + return client, nil +} + +func (c *ZgaClient) CreateCertificate(request *zga.CreateCertificateRequest) (response *zga.CreateCertificateResponse, err error) { + response = zga.NewCreateCertificateResponse() + err = c.ApiCall(request, response) + return +} + +func (c *ZgaClient) DescribeCertificates(request *zga.DescribeCertificatesRequest) (response *zga.DescribeCertificatesResponse, err error) { + response = zga.NewDescribeCertificatesResponse() + err = c.ApiCall(request, response) + return +} + +func (c *ZgaClient) ModifyCertificate(request *zga.ModifyCertificateRequest) (response *zga.ModifyCertificateResponse, err error) { + response = zga.NewModifyCertificateResponse() + err = c.ApiCall(request, response) + return +} diff --git a/pkg/core/certmgr/providers/zenlayer-ga/zenlayer_ga.go b/pkg/core/certmgr/providers/zenlayer-ga/zenlayer_ga.go new file mode 100644 index 000000000..ca83713f4 --- /dev/null +++ b/pkg/core/certmgr/providers/zenlayer-ga/zenlayer_ga.go @@ -0,0 +1,202 @@ +package zenlayerga + +import ( + "context" + "crypto/md5" + "crypto/sha1" + "crypto/x509" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "slices" + "strings" + "time" + + zcommon "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/common" + zga "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/zga20230706" + + "github.com/certimate-go/certimate/pkg/core/certmgr" + "github.com/certimate-go/certimate/pkg/core/certmgr/providers/zenlayer-ga/internal" + xcert "github.com/certimate-go/certimate/pkg/utils/cert" +) + +type CertmgrConfig struct { + // Zenlayer AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // Zenlayer AccessKeyPassword。 + AccessKeyPassword string `json:"accessKeyPassword"` + // Zenlayer 资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` +} + +type Certmgr struct { + config *CertmgrConfig + logger *slog.Logger + sdkClient *internal.ZgaClient +} + +var _ certmgr.Provider = (*Certmgr)(nil) + +func NewCertmgr(config *CertmgrConfig) (*Certmgr, error) { + if config == nil { + return nil, errors.New("the configuration of the certmgr provider is nil") + } + + client, err := createSDKClient(config.AccessKeyId, config.AccessKeyPassword) + 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) (*certmgr.UploadResult, error) { + // 解析证书内容 + certX509, err := xcert.ParseCertificateFromPEM(certPEM) + if err != nil { + return nil, err + } + + // 查询证书列表,避免重复上传 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/certificate/describecertificates + describeCertificatesPageNum := 1 + describeCertificatesPageSize := 100 + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + describeCertificatesReq := zga.NewDescribeCertificatesRequest() + describeCertificatesReq.ResourceGroupId = c.config.ResourceGroupId + describeCertificatesReq.PageNum = describeCertificatesPageNum + describeCertificatesReq.PageSize = describeCertificatesPageSize + describeCertificatesResp, err := c.sdkClient.DescribeCertificates(describeCertificatesReq) + c.logger.Debug("sdk request 'zga.DescribeCertificates'", slog.Any("request", describeCertificatesReq), slog.Any("response", describeCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'zga.DescribeCertificates': %w", err) + } + + for _, certItem := range describeCertificatesResp.Response.DataSet { + // 对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, certItem.Common) { + continue + } + + // 对比证书扩展名称 + if !slices.Equal(certX509.DNSNames, certItem.Sans) { + continue + } + + // 对比证书有效期 + oldCertNotBefore, _ := time.Parse("2006-01-02T15:04:05Z", certItem.StartTime) + oldCertNotAfter, _ := time.Parse("2006-01-02T15:04:05Z", certItem.EndTime) + if !certX509.NotBefore.Equal(oldCertNotBefore) { + continue + } else if !certX509.NotAfter.Equal(oldCertNotAfter) { + continue + } + + // 对比证书指纹 + // + // 注意,虽然文档中描述为 MD5 摘要,但示例给出的是 SHA-1 摘要,因此这里都尝试对比一下 + fingerprintMd5 := md5.Sum(certX509.Raw) + fingerprintMd5Hex := hex.EncodeToString(fingerprintMd5[:]) + fingerprintSha1 := sha1.Sum(certX509.Raw) + fingerprintSha1Hex := hex.EncodeToString(fingerprintSha1[:]) + if !strings.EqualFold(fingerprintMd5Hex, certItem.Fingerprint) && !strings.EqualFold(fingerprintSha1Hex, certItem.Fingerprint) { + continue + } + + // 对比证书公钥算法 + switch certX509.PublicKeyAlgorithm { + case x509.RSA: + if !strings.EqualFold(certItem.Algorithm, "RSA") { + continue + } + case x509.ECDSA: + if !strings.EqualFold(certItem.Algorithm, "EC") && !strings.EqualFold(certItem.Algorithm, "ECDSA") { + continue + } + default: + // 未知算法,跳过 + continue + } + + // 如果以上信息都一致,则视为已存在相同证书,直接返回 + c.logger.Info("ssl certificate already exists") + return &certmgr.UploadResult{ + CertId: certItem.CertificateId, + CertName: certItem.CertificateLabel, + }, nil + } + + if len(describeCertificatesResp.Response.DataSet) < describeCertificatesPageSize { + break + } + + describeCertificatesPageNum++ + } + + // 生成新证书名(需符合 Zenlayer 命名规则) + certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) + + // 创建证书 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/certificate/createcertificate + createCertificateReq := zga.NewCreateCertificateRequest() + createCertificateReq.ResourceGroupId = c.config.ResourceGroupId + createCertificateReq.CertificateLabel = certName + createCertificateReq.CertificateContent = certPEM + createCertificateReq.CertificateKey = privkeyPEM + createCertificateResp, err := c.sdkClient.CreateCertificate(createCertificateReq) + c.logger.Debug("sdk request 'zga.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'zga.CreateCertificate': %w", err) + } + + return &certmgr.UploadResult{ + CertId: createCertificateResp.Response.CertificateId, + CertName: certName, + }, nil +} + +func (c *Certmgr) Replace(ctx context.Context, certIdOrName string, certPEM, privkeyPEM string) (*certmgr.OperateResult, error) { + // 更新证书 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/certificate/modifycertificate + modifyCertificateReq := zga.NewModifyCertificateRequest() + modifyCertificateReq.CertificateId = certIdOrName + modifyCertificateReq.CertificateContent = certPEM + modifyCertificateReq.CertificateKey = privkeyPEM + modifyCertificateResp, err := c.sdkClient.ModifyCertificate(modifyCertificateReq) + c.logger.Debug("sdk request 'zga.ModifyCertificate'", slog.Any("request", modifyCertificateReq), slog.Any("response", modifyCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'zga.ModifyCertificate': %w", err) + } + + return &certmgr.OperateResult{}, nil +} + +func createSDKClient(accessKeyId, accessKeyPassword string) (*internal.ZgaClient, error) { + config := zcommon.NewConfig() + + client, err := internal.NewZgaClient(config, accessKeyId, accessKeyPassword) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/deployer/providers/zenlayer-ga/consts.go b/pkg/core/deployer/providers/zenlayer-ga/consts.go new file mode 100644 index 000000000..f6208113b --- /dev/null +++ b/pkg/core/deployer/providers/zenlayer-ga/consts.go @@ -0,0 +1,8 @@ +package zenlayerga + +const ( + // 资源类型:部署到指定全球网络加速器。 + RESOURCE_TYPE_ACCELERATOR = "accelerator" + // 资源类型:替换指定证书。 + RESOURCE_TYPE_CERTIFICATE = "certificate" +) diff --git a/pkg/core/deployer/providers/zenlayer-ga/internal/client.go b/pkg/core/deployer/providers/zenlayer-ga/internal/client.go new file mode 100644 index 000000000..8f29bde7b --- /dev/null +++ b/pkg/core/deployer/providers/zenlayer-ga/internal/client.go @@ -0,0 +1,40 @@ +package internal + +import ( + "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/common" + zga "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/zga20230706" +) + +// This is a partial copy of https://github.com/zenlayer/zenlayercloud-sdk-go/blob/main/zenlayercloud/zga20230706/client.go +// to lightweight the vendor packages in the built binary. +type ZgaClient struct { + *common.Client +} + +func NewZgaClient(config *common.Config, secretKeyId, secretKeyPassword string) (client *ZgaClient, err error) { + client = &ZgaClient{} + + err = client.InitWithCredential(common.NewCredential(secretKeyId, secretKeyPassword)) + if err != nil { + return nil, err + } + + err = client.WithConfig(config) + if err != nil { + return nil, err + } + + return client, nil +} + +func (c *ZgaClient) DescribeAccelerators(request *zga.DescribeAcceleratorsRequest) (response *zga.DescribeAcceleratorsResponse, err error) { + response = zga.NewDescribeAcceleratorsResponse() + err = c.ApiCall(request, response) + return +} + +func (c *ZgaClient) ModifyAcceleratorCertificate(request *zga.ModifyAcceleratorCertificateRequest) (response *zga.ModifyAcceleratorCertificateResponse, err error) { + response = zga.NewModifyAcceleratorCertificateResponse() + err = c.ApiCall(request, response) + return +} diff --git a/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga.go b/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga.go new file mode 100644 index 000000000..d70fac326 --- /dev/null +++ b/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga.go @@ -0,0 +1,200 @@ +package zenlayerga + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + zcommon "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/common" + zga "github.com/zenlayer/zenlayercloud-sdk-go/zenlayercloud/zga20230706" + + "github.com/certimate-go/certimate/pkg/core/certmgr" + mcertmgr "github.com/certimate-go/certimate/pkg/core/certmgr/providers/zenlayer-ga" + "github.com/certimate-go/certimate/pkg/core/deployer" + "github.com/certimate-go/certimate/pkg/core/deployer/providers/zenlayer-ga/internal" + xwait "github.com/certimate-go/certimate/pkg/utils/wait" +) + +type DeployerConfig struct { + // Zenlayer AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // Zenlayer AccessKeyPassword。 + AccessKeyPassword string `json:"accessKeyPassword"` + // Zenlayer 资源组 ID。 + ResourceGroupId string `json:"resourceGroupId,omitempty"` + // 部署资源类型。 + ResourceType string `json:"resourceType"` + // 加速器 ID。 + // 部署资源类型为 [RESOURCE_TYPE_ACCELERATOR] 时必填。 + AcceleratorId string `json:"acceleratorId,omitempty"` + // 证书 ID。 + // 部署资源类型为 [RESOURCE_TYPE_CERTIFICATE] 时必填。 + CertificateId string `json:"certificateId,omitempty"` +} + +type Deployer struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *internal.ZgaClient + sdkCertmgr certmgr.Provider +} + +var _ deployer.Provider = (*Deployer)(nil) + +func NewDeployer(config *DeployerConfig) (*Deployer, error) { + if config == nil { + return nil, errors.New("the configuration of the deployer provider is nil") + } + + client, err := createSDKClient(config.AccessKeyId, config.AccessKeyPassword) + if err != nil { + return nil, fmt.Errorf("could not create client: %w", err) + } + + pcertmgr, err := mcertmgr.NewCertmgr(&mcertmgr.CertmgrConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeyPassword: config.AccessKeyPassword, + ResourceGroupId: config.ResourceGroupId, + }) + if err != nil { + return nil, fmt.Errorf("could not create certmgr: %w", err) + } + + return &Deployer{ + config: config, + logger: slog.Default(), + sdkClient: client, + sdkCertmgr: pcertmgr, + }, nil +} + +func (d *Deployer) SetLogger(logger *slog.Logger) { + if logger == nil { + d.logger = slog.New(slog.DiscardHandler) + } else { + d.logger = logger + } + + d.sdkCertmgr.SetLogger(logger) +} + +func (d *Deployer) Deploy(ctx context.Context, certPEM, privkeyPEM string) (*deployer.DeployResult, error) { + // 根据部署资源类型决定部署方式 + switch d.config.ResourceType { + case RESOURCE_TYPE_ACCELERATOR: + if err := d.deployToAccelerator(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + case RESOURCE_TYPE_CERTIFICATE: + if err := d.deployToCertificate(ctx, certPEM, privkeyPEM); err != nil { + return nil, err + } + + default: + return nil, fmt.Errorf("unsupported resource type '%s'", d.config.ResourceType) + } + + return &deployer.DeployResult{}, nil +} + +func (d *Deployer) deployToAccelerator(ctx context.Context, certPEM, privkeyPEM string) error { + if d.config.AcceleratorId == "" { + return errors.New("config `acceleratorId` is required") + } + + // 上传证书 + upres, err := d.sdkCertmgr.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // 查询加速器信息 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/accelerator/describeaccelerators + describeAcceleratorsReq := zga.NewDescribeAcceleratorsRequest() + describeAcceleratorsReq.AcceleratorIds = []string{d.config.AcceleratorId} + describeAcceleratorsReq.PageNum = 1 + describeAcceleratorsReq.PageSize = 1 + describeAcceleratorsResp, err := d.sdkClient.DescribeAccelerators(describeAcceleratorsReq) + d.logger.Debug("sdk request 'zga.DescribeAccelerators'", slog.Any("request", describeAcceleratorsReq), slog.Any("response", describeAcceleratorsResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'zga.DescribeAccelerators': %w", err) + } else if len(describeAcceleratorsResp.Response.DataSet) == 0 { + return fmt.Errorf("could not found accelerator '%s'", d.config.AcceleratorId) + } + + // 修改加速器证书 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/accelerator/modifyacceleratorcertificate + acceleratorInfo := describeAcceleratorsResp.Response.DataSet[0] + if acceleratorInfo.Certificate == nil || acceleratorInfo.Certificate.CertificateId != upres.CertId { + modifyAcceleratorCertificateReq := zga.NewModifyAcceleratorCertificateRequest() + modifyAcceleratorCertificateReq.AcceleratorId = acceleratorInfo.AcceleratorId + modifyAcceleratorCertificateReq.CertificateId = upres.CertId + modifyAcceleratorCertificateResp, err := d.sdkClient.ModifyAcceleratorCertificate(modifyAcceleratorCertificateReq) + d.logger.Debug("sdk request 'zga.ModifyAcceleratorCertificate'", slog.Any("request", modifyAcceleratorCertificateReq), slog.Any("response", modifyAcceleratorCertificateResp)) + if err != nil { + return fmt.Errorf("failed to execute sdk request 'zga.ModifyAcceleratorCertificate': %w", err) + } + } + + // 查询加速器状态,等待部署状态变更 + // REF: https://docs.console.zenlayer.com/api-reference/cn/networking/zga/accelerator/describeaccelerators + if _, err := xwait.UntilWithContext(ctx, func(_ context.Context, _ int) (bool, error) { + describeAcceleratorsReq := zga.NewDescribeAcceleratorsRequest() + describeAcceleratorsReq.AcceleratorIds = []string{acceleratorInfo.AcceleratorId} + describeAcceleratorsReq.PageNum = 1 + describeAcceleratorsReq.PageSize = 1 + describeAcceleratorsResp, err := d.sdkClient.DescribeAccelerators(describeAcceleratorsReq) + d.logger.Debug("sdk request 'zga.DescribeAccelerators'", slog.Any("request", describeAcceleratorsReq), slog.Any("response", describeAcceleratorsResp)) + if err != nil { + return false, fmt.Errorf("failed to execute sdk request 'zga.DescribeAccelerators': %w", err) + } else if len(describeAcceleratorsResp.Response.DataSet) == 0 { + return false, fmt.Errorf("could not found accelerator '%s'", d.config.AcceleratorId) + } + + switch describeAcceleratorsResp.Response.DataSet[0].AcceleratorStatus { + case "Accelerating": + return true, nil + case "NotAccelerate", "StopAccelerate", "AccelerateFailure": + return false, fmt.Errorf("unexpected zenlayer accelerator status") + } + + d.logger.Info("waiting for zenlayer accelerator deploying completion ...") + return false, nil + }, time.Second*5); err != nil { + return err + } + + return nil +} + +func (d *Deployer) deployToCertificate(ctx context.Context, certPEM, privkeyPEM string) error { + if d.config.CertificateId == "" { + return errors.New("config `certificateId` is required") + } + + // 替换证书 + upres, err := d.sdkCertmgr.Replace(ctx, d.config.CertificateId, certPEM, privkeyPEM) + if err != nil { + return fmt.Errorf("failed to replace certificate file: %w", err) + } else { + d.logger.Info("ssl certificate replaced", slog.Any("result", upres)) + } + + return nil +} + +func createSDKClient(accessKeyId, accessKeyPassword string) (*internal.ZgaClient, error) { + config := zcommon.NewConfig() + + client, err := internal.NewZgaClient(config, accessKeyId, accessKeyPassword) + if err != nil { + return nil, err + } + + return client, nil +} diff --git a/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga_test.go b/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga_test.go new file mode 100644 index 000000000..8f3e6c2ef --- /dev/null +++ b/pkg/core/deployer/providers/zenlayer-ga/zenlayer_ga_test.go @@ -0,0 +1,111 @@ +package zenlayerga_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/certimate-go/certimate/pkg/core/deployer/providers/zenlayer-ga" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeyPassword string + fAcceleratorId string + fCertificateId string +) + +func init() { + argsPrefix := "ZENLAYERGA_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeyPassword, argsPrefix+"ACCESSKEYPASSWORD", "", "") + flag.StringVar(&fAcceleratorId, argsPrefix+"ACCELERATORID", "", "") + flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./zenlayer_ga_test.go -args \ + --ZENLAYERGA_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --ZENLAYERGA_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --ZENLAYERGA_ACCESSKEYID="your-access-key-id" \ + --ZENLAYERGA_ACCESSKEYPASSWORD="your-access-key-password" \ + --ZENLAYERGA_ACCELERATORID="your-ga-accelerator-id" \ + --ZENLAYERGA_CERTIFICATEID="your-ga-certificate-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy_ToAccelerator", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYPASSWORD: %v", fAccessKeyPassword), + fmt.Sprintf("ACCELERATORID: %v", fAcceleratorId), + }, "\n")) + + provider, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeyPassword: fAccessKeyPassword, + ResourceType: provider.RESOURCE_TYPE_ACCELERATOR, + AcceleratorId: fAcceleratorId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) + + t.Run("Deploy_ToCertificate", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("INPUTCERTPATH: %v", fInputCertPath), + fmt.Sprintf("INPUTKEYPATH: %v", fInputKeyPath), + fmt.Sprintf("ACCESSKEYID: %v", fAccessKeyId), + fmt.Sprintf("ACCESSKEYPASSWORD: %v", fAccessKeyPassword), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + provider, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeyPassword: fAccessKeyPassword, + ResourceType: provider.RESOURCE_TYPE_CERTIFICATE, + CertificateId: fCertificateId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := provider.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/ui/public/imgs/providers/zenlayer.svg b/ui/public/imgs/providers/zenlayer.svg new file mode 100644 index 000000000..2f1d841fa --- /dev/null +++ b/ui/public/imgs/providers/zenlayer.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index b45043a99..002c1294c 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -111,6 +111,7 @@ import AccessConfigFieldsProviderWebhook from "./AccessConfigFieldsProviderWebho import AccessConfigFieldsProviderWeComBot from "./AccessConfigFieldsProviderWeComBot"; import AccessConfigFieldsProviderWestcn from "./AccessConfigFieldsProviderWestcn"; import AccessConfigFieldsProviderXinnet from "./AccessConfigFieldsProviderXinnet"; +import AccessConfigFieldsProviderZenlayer from "./AccessConfigFieldsProviderZenlayer"; import AccessConfigFieldsProviderZeroSSL from "./AccessConfigFieldsProviderZeroSSL"; const providerComponentMap: Partial>> = { @@ -227,6 +228,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 { + accessKeyId: "", + accessKeyPassword: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + accessKeyId: z.string().nonempty(t("access.form.zenlayer_access_key_id.placeholder")), + accessKeyPassword: z.string().nonempty(t("access.form.zenlayer_access_key_password.placeholder")), + resourceGroupId: z.string().nullish(), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderZenlayer, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx index f4d8111e8..3591f40b9 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProvider.tsx @@ -122,6 +122,7 @@ import BizDeployNodeConfigFieldsProviderWangsuCDN from "./BizDeployNodeConfigFie import BizDeployNodeConfigFieldsProviderWangsuCDNPro from "./BizDeployNodeConfigFieldsProviderWangsuCDNPro"; import BizDeployNodeConfigFieldsProviderWangsuCertificate from "./BizDeployNodeConfigFieldsProviderWangsuCertificate"; import BizDeployNodeConfigFieldsProviderWebhook from "./BizDeployNodeConfigFieldsProviderWebhook"; +import BizDeployNodeConfigFieldsProviderZenlayerGA from "./BizDeployNodeConfigFieldsProviderZenlayerGA"; const providerComponentMap: Partial>> = { /* @@ -248,6 +249,7 @@ const providerComponentMap: Partial any; deps?: unknown[] }) => { diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderZenlayerGA.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderZenlayerGA.tsx new file mode 100644 index 000000000..234d0ac25 --- /dev/null +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderZenlayerGA.tsx @@ -0,0 +1,120 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import Show from "@/components/Show"; + +import { useFormNestedFieldsContext } from "./_context"; + +const RESOURCE_TYPE_ACCELERATOR = "accelerator" as const; +const RESOURCE_TYPE_CERTIFICATE = "certificate" as const; + +const BizDeployNodeConfigFieldsProviderZenlayerGA = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); + const initialValues = getInitialValues(); + + const fieldResourceType = Form.useWatch([parentNamePath, "resourceType"], formInst); + + return ( + <> + + + + + + + } + > + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + resourceType: RESOURCE_TYPE_ACCELERATOR, + acceleratorId: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) => { + const { t } = i18n; + + return z + .object({ + resourceType: z.literal([RESOURCE_TYPE_ACCELERATOR, RESOURCE_TYPE_CERTIFICATE], t("workflow_node.deploy.form.shared_resource_type.placeholder")), + acceleratorId: z.string().nullish(), + certificateId: z.string().nullish(), + }) + .superRefine((values, ctx) => { + switch (values.resourceType) { + case RESOURCE_TYPE_ACCELERATOR: + { + if (!values.acceleratorId?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.zenlayer_ga_accelerator_id.placeholder"), + path: ["acceleratorId"], + }); + } + } + break; + + case RESOURCE_TYPE_CERTIFICATE: + { + if (!values.certificateId?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("workflow_node.deploy.form.zenlayer_ga_certificate_id.placeholder"), + path: ["certificateId"], + }); + } + } + break; + } + }); +}; + +const _default = Object.assign(BizDeployNodeConfigFieldsProviderZenlayerGA, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index a72fee84e..26cbf164e 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -127,6 +127,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ WECOMBOT: "wecombot", WESTCN: "westcn", XINNET: "xinnet", + ZENLAYER: "zenlayer", ZEROSSL: "zerossl", } as const); @@ -186,6 +187,7 @@ export const accessProvidersMap: Maphttps://apidoc.xin.cn/doc-7283837", + "access.form.zenlayer_access_key_id.label": "Zenlayer AccessKeyID", + "access.form.zenlayer_access_key_id.placeholder": "Please enter Zenlayer AccessKeyID", + "access.form.zenlayer_access_key_id.tooltip": "For more information, see https://docs.console.zenlayer.com/welcome/platform/team-management/generate-an-api-access-key", + "access.form.zenlayer_access_key_password.label": "Zenlayer AccessKeyPassword", + "access.form.zenlayer_access_key_password.placeholder": "Please enter Zenlayer AccessKeyPassword", + "access.form.zenlayer_access_key_password.tooltip": "For more information, see https://docs.console.zenlayer.com/welcome/platform/team-management/generate-an-api-access-key", + "access.form.zenlayer_resource_group_id.label": "Zenlayer resource group ID (Optional)", + "access.form.zenlayer_resource_group_id.placeholder": "Please enter Zenlayer resource group ID", + "access.form.zenlayer_resource_group_id.tooltip": "For more information, see https://docs.console.zenlayer.com/welcome/platform/team-management/create-a-resource-group", "access.form.zerossl_eab.guide": "Learn more about using EAB key in ZeroSSL:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 210ebb7a1..92e63ba06 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -235,6 +235,8 @@ "provider.westcn": "West.cn", "provider.xinnet": "Xinnet.com", "provider.zerossl": "ZeroSSL", + "provider.zenlayer": "Zenlayer", + "provider.zenlayer_ga": "Zenlayer - ZGA (Global Accelerator)", "provider.category.all": "All", "provider.category.cdn": "CDN", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 4b9c19ac3..d7282395c 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -1265,6 +1265,14 @@ "workflow_node.deploy.form.webhook_timeout.label": "Webhook timeout (Optional)", "workflow_node.deploy.form.webhook_timeout.placeholder": "Please enter Webhook timeout", "workflow_node.deploy.form.webhook_timeout.unit": "seconds", + "workflow_node.deploy.form.zenlayer_ga_resource_type.option.accelerator.label": "Accelerator", + "workflow_node.deploy.form.zenlayer_ga_resource_type.option.certificate.label": "Certificate", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.label": "Zenlayer ZGA accelerator ID", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.placeholder": "Please enter Zenlayer ZGA accelerator ID", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.tooltip": "For more information, https://console.zenlayer.com/gia/", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.label": "Zenlayer ZGA certificate ID", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.placeholder": "Please enter Zenlayer ZGA certificate ID", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.tooltip": "For more information, https://console.zenlayer.com/gia/", "workflow_node.deploy.form.skip_on_last_succeeded.label": "Repeated deployment", "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "If the last deployment was successful, ", "workflow_node.deploy.form.skip_on_last_succeeded.suffix": " re-deployment.", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index f0ec5844e..d395ee915 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -747,5 +747,14 @@ "access.form.xinnet_api_password.label": "新网数码代理商 API 密码", "access.form.xinnet_api_password.placeholder": "请输入新网数码代理商 API 密码", "access.form.xinnet_agent.guide": "新网数码 API 仅支持代理商调用。点击下方链接了解更多:
https://apidoc.xin.cn/doc-7283837", + "access.form.zenlayer_access_key_id.label": "Zenlayer AccessKeyID", + "access.form.zenlayer_access_key_id.placeholder": "请输入 Zenlayer AccessKeyID", + "access.form.zenlayer_access_key_id.tooltip": "这是什么?请参阅 https://docs.console.zenlayer.com/welcome/cn/platform/team-management/generate-an-api-access-key", + "access.form.zenlayer_access_key_password.label": "Zenlayer AccessKeyPassword", + "access.form.zenlayer_access_key_password.placeholder": "请输入 Zenlayer AccessKeyPassword", + "access.form.zenlayer_access_key_password.tooltip": "这是什么?请参阅 https://docs.console.zenlayer.com/welcome/cn/platform/team-management/generate-an-api-access-key", + "access.form.zenlayer_resource_group_id.label": "Zenlayer 资源组 ID(可选)", + "access.form.zenlayer_resource_group_id.placeholder": "请输入 Zenlayer 资源组 ID", + "access.form.zenlayer_resource_group_id.tooltip": "这是什么?请参阅 https://docs.console.zenlayer.com/welcome/cn/platform/team-management/create-a-resource-group", "access.form.zerossl_eab.guide": "点击下方链接了解如何获取 ZeroSSL EAB:
https://zerossl.com/documentation/acme/" } diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 6f4851ab3..241215385 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -235,6 +235,8 @@ "provider.westcn": "西部数码", "provider.xinnet": "新网数码", "provider.zerossl": "ZeroSSL", + "provider.zenlayer": "Zenlayer", + "provider.zenlayer_ga": "Zenlayer - 全球网络加速器 ZGA", "provider.category.all": "全部", "provider.category.cdn": "CDN", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 2bb8b3d92..63f8054f2 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -1263,6 +1263,14 @@ "workflow_node.deploy.form.webhook_timeout.label": "Webhook 超时时间(可选)", "workflow_node.deploy.form.webhook_timeout.placeholder": "请输入 Webhook 超时时间", "workflow_node.deploy.form.webhook_timeout.unit": "秒", + "workflow_node.deploy.form.zenlayer_ga_resource_type.option.accelerator.label": "部署到指定全球网络加速器", + "workflow_node.deploy.form.zenlayer_ga_resource_type.option.certificate.label": "替换指定证书", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.label": "Zenlayer ZGA 加速器 ID", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.placeholder": "请输入 Zenlayer ZGA 加速器 ID", + "workflow_node.deploy.form.zenlayer_ga_accelerator_id.tooltip": "这是什么?请参阅 https://console.zenlayer.com/gia/", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.label": "Zenlayer ZGA 证书 ID", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.placeholder": "请输入 Zenlayer ZGA 证书 ID", + "workflow_node.deploy.form.zenlayer_ga_certificate_id.tooltip": "这是什么?请参阅 https://console.zenlayer.com/gia/", "workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署", "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功后,再次运行工作流时", "workflow_node.deploy.form.skip_on_last_succeeded.suffix": "此部署节点。",