From e6fc92eccbb341e89d5495ebc9b193833c2a755b Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 19 May 2025 23:49:06 +0800 Subject: [PATCH] feat: new deployment provider: wangsu certificate management --- internal/deployer/providers.go | 11 +- internal/domain/provider.go | 2 +- .../providers/wangsu-cdnpro/wangsu_cdnpro.go | 2 +- .../wangsu-certificate/wangsu_certificate.go | 109 +++++++++++++ .../wangsu_certificate_test.go | 75 +++++++++ .../wangsu-certificate/wangsu_certificate.go | 143 ++++++++++++++++++ .../wangsu_certificate_test.go | 72 +++++++++ internal/pkg/sdk3rd/dogecloud/client.go | 4 +- internal/pkg/sdk3rd/lecdn/v3/client/api.go | 1 + internal/pkg/sdk3rd/lecdn/v3/client/models.go | 1 + .../pkg/sdk3rd/wangsu/{cdn => cdnpro}/api.go | 14 +- .../sdk3rd/wangsu/{cdn => cdnpro}/client.go | 2 +- .../sdk3rd/wangsu/{cdn => cdnpro}/models.go | 4 +- internal/pkg/sdk3rd/wangsu/certificate/api.go | 42 +++++ .../pkg/sdk3rd/wangsu/certificate/client.go | 20 +++ .../pkg/sdk3rd/wangsu/certificate/models.go | 52 +++++++ internal/pkg/sdk3rd/wangsu/openapi/client.go | 6 +- .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../node/DeployNodeConfigFormAWSACMConfig.tsx | 2 +- ...eployNodeConfigFormAzureKeyVaultConfig.tsx | 2 +- ...DeployNodeConfigFormWangsuCDNProConfig.tsx | 16 +- ...yNodeConfigFormWangsuCertificateConfig.tsx | 61 ++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 1 + .../i18n/locales/en/nls.workflow.nodes.json | 3 + ui/src/i18n/locales/zh/nls.provider.json | 1 + .../i18n/locales/zh/nls.workflow.nodes.json | 3 + 27 files changed, 627 insertions(+), 27 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go create mode 100644 internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go create mode 100644 internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go create mode 100644 internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go rename internal/pkg/sdk3rd/wangsu/{cdn => cdnpro}/api.go (76%) rename internal/pkg/sdk3rd/wangsu/{cdn => cdnpro}/client.go (96%) rename internal/pkg/sdk3rd/wangsu/{cdn => cdnpro}/models.go (98%) create mode 100644 internal/pkg/sdk3rd/wangsu/certificate/api.go create mode 100644 internal/pkg/sdk3rd/wangsu/certificate/client.go create mode 100644 internal/pkg/sdk3rd/wangsu/certificate/models.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index 53951300..09cda5fe 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -87,6 +87,7 @@ import ( pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live" pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos" pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro" + pWangsuCertificate "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-certificate" pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook" httputil "github.com/usual2970/certimate/internal/pkg/utils/http" maputil "github.com/usual2970/certimate/internal/pkg/utils/map" @@ -1205,7 +1206,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } } - case domain.DeploymentProviderTypeWangsuCDNPro: + case domain.DeploymentProviderTypeWangsuCDNPro, domain.DeploymentProviderTypeWangsuCertificate: { access := domain.AccessConfigForWangsu{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -1225,6 +1226,14 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer }) return deployer, err + case domain.DeploymentProviderTypeWangsuCertificate: + deployer, err := pWangsuCertificate.NewDeployer(&pWangsuCertificate.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + CertificateId: maputil.GetString(options.ProviderServiceConfig, "certificateId"), + }) + return deployer, err + default: break } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d8091eb8..c518c3b9 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -245,7 +245,7 @@ const ( DeploymentProviderTypeVolcEngineTOS = DeploymentProviderType(AccessProviderTypeVolcEngine + "-tos") DeploymentProviderTypeWangsuCDN = DeploymentProviderType(AccessProviderTypeWangsu + "-cdn") // 网宿 CDN(预留) DeploymentProviderTypeWangsuCDNPro = DeploymentProviderType(AccessProviderTypeWangsu + "-cdnpro") - DeploymentProviderTypeWangsuCert = DeploymentProviderType(AccessProviderTypeWangsu + "-cert") // 网宿证书管理(预留) + DeploymentProviderTypeWangsuCertificate = DeploymentProviderType(AccessProviderTypeWangsu + "-certificate") DeploymentProviderTypeWebhook = DeploymentProviderType(AccessProviderTypeWebhook) ) diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go index ee16b08a..436ea5a5 100644 --- a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -17,7 +17,7 @@ import ( "time" "github.com/usual2970/certimate/internal/pkg/core/deployer" - wangsucdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdn" + wangsucdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/cdnpro" certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" ) diff --git a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go new file mode 100644 index 00000000..3f691489 --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate.go @@ -0,0 +1,109 @@ +package wangsucertificate + +import ( + "context" + "errors" + "fmt" + "log/slog" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/core/uploader" + uploadersp "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate" + wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" +) + +type DeployerConfig struct { + // 网宿云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 网宿云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` + // 证书 ID。 + // 选填。零值时表示新建证书;否则表示更新证书。 + CertificateId string `json:"certificateId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *wangsusdk.Client + sslUploader uploader.Uploader +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + }) + if err != nil { + return nil, fmt.Errorf("failed to create ssl uploader: %w", err) + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + sslUploader: uploader, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + if d.config.CertificateId == "" { + // 上传证书到证书管理 + upres, err := d.sslUploader.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + } else { + // 修改证书 + // REF: https://www.wangsu.com/document/api-doc/25568?productCode=certificatemanagement + updateCertificateReq := &wangsusdk.UpdateCertificateRequest{ + Name: typeutil.ToPtr(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + Certificate: typeutil.ToPtr(certPEM), + PrivateKey: typeutil.ToPtr(privkeyPEM), + Comment: typeutil.ToPtr("upload from certimate"), + } + updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq) + d.logger.Debug("sdk request 'certificatemanagement.UpdateCertificate'", slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.CreateCertificate': %w", err) + } + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) { + if accessKeyId == "" { + return nil, errors.New("invalid wangsu access key id") + } + + if accessKeySecret == "" { + return nil, errors.New("invalid wangsu access key secret") + } + + return wangsusdk.NewClient(accessKeyId, accessKeySecret), nil +} diff --git a/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go new file mode 100644 index 00000000..a6805ec9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-certificate/wangsu_certificate_test.go @@ -0,0 +1,75 @@ +package wangsucertificate_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-certificate" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string + fCertificateId string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") + flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./wangsu_certificate_test.go -args \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_CERTIFICATEID="your-certificate-id" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", 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("ACCESSKEYSECRET: %v", fAccessKeySecret), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + CertificateId: fCertificateId, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := deployer.Deploy(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go new file mode 100644 index 00000000..b512be09 --- /dev/null +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate.go @@ -0,0 +1,143 @@ +package jdcloudssl + +import ( + "context" + "errors" + "fmt" + "log/slog" + "regexp" + "strings" + "time" + + wangsusdk "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/certificate" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + certutil "github.com/usual2970/certimate/internal/pkg/utils/cert" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" +) + +type UploaderConfig struct { + // 网宿云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 网宿云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *wangsusdk.Client +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + return &UploaderProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { + if logger == nil { + u.logger = slog.Default() + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPEM) + if err != nil { + return nil, err + } + + // 查询证书列表,避免重复上传 + // REF: https://www.wangsu.com/document/api-doc/26426 + listCertificatesResp, err := u.sdkClient.ListCertificates() + u.logger.Debug("sdk request 'certificatemanagement.ListCertificates'", slog.Any("response", listCertificatesResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.ListCertificates': %w", err) + } + + if listCertificatesResp.Certificates != nil { + for _, certificate := range listCertificatesResp.Certificates { + // 对比证书序列号 + if !strings.EqualFold(certX509.SerialNumber.Text(16), certificate.Serial) { + continue + } + + // 再对比证书有效期 + cstzone := time.FixedZone("CST", 8*60*60) + oldCertNotBefore, _ := time.ParseInLocation(time.DateTime, certificate.ValidityFrom, cstzone) + oldCertNotAfter, _ := time.ParseInLocation(time.DateTime, certificate.ValidityTo, cstzone) + if !certX509.NotBefore.Equal(oldCertNotBefore) || !certX509.NotAfter.Equal(oldCertNotAfter) { + continue + } + + // 如果以上信息都一致,则视为已存在相同证书,直接返回 + u.logger.Info("ssl certificate already exists") + return &uploader.UploadResult{ + CertId: certificate.CertificateId, + CertName: certificate.Name, + }, nil + } + } + + // 生成新证书名(需符合网宿云命名规则) + var certId string + certName := fmt.Sprintf("certimate_%d", time.Now().UnixMilli()) + + // 新增证书 + // REF: https://www.wangsu.com/document/api-doc/25199?productCode=certificatemanagement + createCertificateReq := &wangsusdk.CreateCertificateRequest{ + Name: typeutil.ToPtr(certName), + Certificate: typeutil.ToPtr(certPEM), + PrivateKey: typeutil.ToPtr(privkeyPEM), + Comment: typeutil.ToPtr("upload from certimate"), + } + createCertificateResp, err := u.sdkClient.CreateCertificate(createCertificateReq) + u.logger.Debug("sdk request 'certificatemanagement.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'certificatemanagement.CreateCertificate': %w", err) + } + + // 网宿云证书 URL 中包含证书 ID + // 格式: + // https://open.chinanetcenter.com/api/certificate/100001 + wangsuCertIdMatches := regexp.MustCompile(`/certificate/([0-9]+)`).FindStringSubmatch(createCertificateResp.CertificateUrl) + if len(wangsuCertIdMatches) > 1 { + certId = wangsuCertIdMatches[1] + } else { + return nil, fmt.Errorf("received empty certificate id") + } + + return &uploader.UploadResult{ + CertId: certId, + CertName: certName, + }, nil +} + +func createSdkClient(accessKeyId, accessKeySecret string) (*wangsusdk.Client, error) { + if accessKeyId == "" { + return nil, errors.New("invalid wangsu access key id") + } + + if accessKeySecret == "" { + return nil, errors.New("invalid wangsu access key secret") + } + + return wangsusdk.NewClient(accessKeyId, accessKeySecret), nil +} diff --git a/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go new file mode 100644 index 00000000..bdec8cfe --- /dev/null +++ b/internal/pkg/core/uploader/providers/wangsu-certificate/wangsu_certificate_test.go @@ -0,0 +1,72 @@ +package jdcloudssl_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/wangsu-certificate" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_JDCLOUDSSL_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") +} + +/* +Shell command to run this test: + + go test -v ./wangsu_certificate_test.go -args \ + --CERTIMATE_UPLOADER_WANGSUCERTIFICATE_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_WANGSUCERTIFICATE_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_WANGSUCERTIFICATE_ACCESSKEYSECRET="your-access-key-secret" +*/ +func TestDeploy(t *testing.T) { + flag.Parse() + + t.Run("Deploy", 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("ACCESSKEYSECRET: %v", fAccessKeySecret), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + fInputCertData, _ := os.ReadFile(fInputCertPath) + fInputKeyData, _ := os.ReadFile(fInputKeyPath) + res, err := uploader.Upload(context.Background(), string(fInputCertData), string(fInputKeyData)) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + sres, _ := json.Marshal(res) + t.Logf("ok: %s", string(sres)) + }) +} diff --git a/internal/pkg/sdk3rd/dogecloud/client.go b/internal/pkg/sdk3rd/dogecloud/client.go index 46f3513d..806b0ea9 100644 --- a/internal/pkg/sdk3rd/dogecloud/client.go +++ b/internal/pkg/sdk3rd/dogecloud/client.go @@ -174,10 +174,10 @@ func (c *Client) sendReq(method string, path string, data map[string]interface{} } defer resp.Body.Close() - r, err := io.ReadAll(resp.Body) + bytes, err := io.ReadAll(resp.Body) if err != nil { return nil, err } - return r, nil + return bytes, nil } diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/api.go b/internal/pkg/sdk3rd/lecdn/v3/client/api.go index ffdc70e3..89f9cdc0 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/client/api.go +++ b/internal/pkg/sdk3rd/lecdn/v3/client/api.go @@ -14,6 +14,7 @@ func (c *Client) ensureAccessTokenExists() error { } req := &loginRequest{ + Email: c.username, Username: c.username, Password: c.password, } diff --git a/internal/pkg/sdk3rd/lecdn/v3/client/models.go b/internal/pkg/sdk3rd/lecdn/v3/client/models.go index a4fecf1c..6d63ea79 100644 --- a/internal/pkg/sdk3rd/lecdn/v3/client/models.go +++ b/internal/pkg/sdk3rd/lecdn/v3/client/models.go @@ -19,6 +19,7 @@ func (r *baseResponse) GetMessage() string { } type loginRequest struct { + Email string `json:"email"` Username string `json:"username"` Password string `json:"password"` } diff --git a/internal/pkg/sdk3rd/wangsu/cdn/api.go b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go similarity index 76% rename from internal/pkg/sdk3rd/wangsu/cdn/api.go rename to internal/pkg/sdk3rd/wangsu/cdnpro/api.go index 0da647c8..c6f8da04 100644 --- a/internal/pkg/sdk3rd/wangsu/cdn/api.go +++ b/internal/pkg/sdk3rd/wangsu/cdnpro/api.go @@ -1,4 +1,4 @@ -package cdn +package cdnpro import ( "fmt" @@ -10,14 +10,14 @@ import ( func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { resp := &CreateCertificateResponse{} - r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { + rres, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err } - resp.CertificateUrl = r.Header().Get("Location") + resp.CertificateUrl = rres.Header().Get("Location") return resp, err } @@ -27,14 +27,14 @@ func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateR } resp := &UpdateCertificateResponse{} - r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { + rres, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) }) if err != nil { return resp, err } - resp.CertificateUrl = r.Header().Get("Location") + resp.CertificateUrl = rres.Header().Get("Location") return resp, err } @@ -50,12 +50,12 @@ func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) { resp := &CreateDeploymentTaskResponse{} - r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp) + rres, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp) if err != nil { return resp, err } - resp.DeploymentTaskUrl = r.Header().Get("Location") + resp.DeploymentTaskUrl = rres.Header().Get("Location") return resp, err } diff --git a/internal/pkg/sdk3rd/wangsu/cdn/client.go b/internal/pkg/sdk3rd/wangsu/cdnpro/client.go similarity index 96% rename from internal/pkg/sdk3rd/wangsu/cdn/client.go rename to internal/pkg/sdk3rd/wangsu/cdnpro/client.go index ac53e171..b5c0f530 100644 --- a/internal/pkg/sdk3rd/wangsu/cdn/client.go +++ b/internal/pkg/sdk3rd/wangsu/cdnpro/client.go @@ -1,4 +1,4 @@ -package cdn +package cdnpro import ( "time" diff --git a/internal/pkg/sdk3rd/wangsu/cdn/models.go b/internal/pkg/sdk3rd/wangsu/cdnpro/models.go similarity index 98% rename from internal/pkg/sdk3rd/wangsu/cdn/models.go rename to internal/pkg/sdk3rd/wangsu/cdnpro/models.go index a9a9ec74..9cb1e648 100644 --- a/internal/pkg/sdk3rd/wangsu/cdn/models.go +++ b/internal/pkg/sdk3rd/wangsu/cdnpro/models.go @@ -1,11 +1,11 @@ -package cdn +package cdnpro import ( "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/openapi" ) type baseResponse struct { - RequestId *string `json:"-"` + RequestId *string `json:"requestId,omitempty"` Code *string `json:"code,omitempty"` Message *string `json:"message,omitempty"` } diff --git a/internal/pkg/sdk3rd/wangsu/certificate/api.go b/internal/pkg/sdk3rd/wangsu/certificate/api.go new file mode 100644 index 00000000..037fb6e7 --- /dev/null +++ b/internal/pkg/sdk3rd/wangsu/certificate/api.go @@ -0,0 +1,42 @@ +package certificate + +import ( + "fmt" + "net/http" + "net/url" +) + +func (c *Client) ListCertificates() (*ListCertificatesResponse, error) { + resp := &ListCertificatesResponse{} + _, err := c.client.SendRequestWithResult(http.MethodGet, "/api/certificate", nil, resp) + if err != nil { + return resp, err + } + + return resp, err +} + +func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { + resp := &CreateCertificateResponse{} + rres, err := c.client.SendRequestWithResult(http.MethodPost, "/api/certificate", req, resp) + if err != nil { + return resp, err + } + + resp.CertificateUrl = rres.Header().Get("Location") + return resp, err +} + +func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + if certificateId == "" { + return nil, fmt.Errorf("wangsu api error: invalid parameter: certificateId") + } + + resp := &UpdateCertificateResponse{} + _, err := c.client.SendRequestWithResult(http.MethodPut, fmt.Sprintf("/api/certificate/%s", url.PathEscape(certificateId)), req, resp) + if err != nil { + return resp, err + } + + return resp, err +} diff --git a/internal/pkg/sdk3rd/wangsu/certificate/client.go b/internal/pkg/sdk3rd/wangsu/certificate/client.go new file mode 100644 index 00000000..19f4cfaa --- /dev/null +++ b/internal/pkg/sdk3rd/wangsu/certificate/client.go @@ -0,0 +1,20 @@ +package certificate + +import ( + "time" + + "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/openapi" +) + +type Client struct { + client *openapi.Client +} + +func NewClient(accessKey, secretKey string) *Client { + return &Client{client: openapi.NewClient(accessKey, secretKey)} +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.WithTimeout(timeout) + return c +} diff --git a/internal/pkg/sdk3rd/wangsu/certificate/models.go b/internal/pkg/sdk3rd/wangsu/certificate/models.go new file mode 100644 index 00000000..4e882e7c --- /dev/null +++ b/internal/pkg/sdk3rd/wangsu/certificate/models.go @@ -0,0 +1,52 @@ +package certificate + +import ( + "github.com/usual2970/certimate/internal/pkg/sdk3rd/wangsu/openapi" +) + +type baseResponse struct { + RequestId *string `json:"requestId,omitempty"` + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +var _ openapi.Result = (*baseResponse)(nil) + +func (r *baseResponse) SetRequestId(requestId string) { + r.RequestId = &requestId +} + +type CreateCertificateRequest struct { + Name *string `json:"name,omitempty" required:"true"` + Certificate *string `json:"certificate,omitempty" required:"true"` + PrivateKey *string `json:"privateKey,omitempty"` + Comment *string `json:"comment,omitempty" ` +} + +type CreateCertificateResponse struct { + baseResponse + CertificateUrl string `json:"location,omitempty"` +} + +type UpdateCertificateRequest struct { + Name *string `json:"name,omitempty" required:"true"` + Certificate *string `json:"certificate,omitempty"` + PrivateKey *string `json:"privateKey,omitempty"` + Comment *string `json:"comment,omitempty" ` +} + +type UpdateCertificateResponse struct { + baseResponse +} + +type ListCertificatesResponse struct { + baseResponse + Certificates []*struct { + CertificateId string `json:"certificate-id"` + Name string `json:"name"` + Comment string `json:"comment"` + ValidityFrom string `json:"certificate-validity-from"` + ValidityTo string `json:"certificate-validity-to"` + Serial string `json:"certificate-serial"` + } `json:"ssl-certificates,omitempty"` +} diff --git a/internal/pkg/sdk3rd/wangsu/openapi/client.go b/internal/pkg/sdk3rd/wangsu/openapi/client.go index 95d17bb0..0bb141d8 100644 --- a/internal/pkg/sdk3rd/wangsu/openapi/client.go +++ b/internal/pkg/sdk3rd/wangsu/openapi/client.go @@ -154,8 +154,10 @@ func (c *Client) sendRequest(method string, path string, params interface{}, con req = req.SetBody(params) } - for _, fn := range configureReq { - fn(req) + if configureReq != nil { + for _, fn := range configureReq { + fn(req) + } } resp, err := req.Send() diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 3bc1774d..a46ae1be 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -92,6 +92,7 @@ import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVo import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx"; import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx"; import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx"; +import DeployNodeConfigFormWangsuCertificateConfig from "./DeployNodeConfigFormWangsuCertificateConfig.tsx"; import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx"; type DeployNodeConfigFormFieldValues = Partial; @@ -335,6 +336,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.WANGSU_CDNPRO: return ; + case DEPLOYMENT_PROVIDERS.WANGSU_CERTIFICATE: + return ; case DEPLOYMENT_PROVIDERS.WEBHOOK: return ; } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx index f007bf87..2e539453 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAWSACMConfig.tsx @@ -28,7 +28,7 @@ const DeployNodeConfigFormAWSACMConfig = ({ form: formInst, formName, disabled, .string({ message: t("workflow_node.deploy.form.aws_acm_region.placeholder") }) .nonempty(t("workflow_node.deploy.form.aws_acm_region.placeholder")) .trim(), - certificateArn: z.string({ message: t("workflow_node.deploy.form.aws_acm_certificate_arn.placeholder") }).nullish(), + certificateArn: z.string().nullish(), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx index 2a54bb99..bd2347df 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAzureKeyVaultConfig.tsx @@ -35,7 +35,7 @@ const DeployNodeConfigFormAzureKeyVaultConfig = ({ .nonempty(t("workflow_node.deploy.form.azure_keyvault_name.placeholder")) .trim(), certificateName: z - .string({ message: t("workflow_node.deploy.form.azure_keyvault_certificate_name.placeholder") }) + .string() .nullish() .refine((v) => { if (!v) return true; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx index a86b34c0..e89e1e8d 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx @@ -5,37 +5,37 @@ import { z } from "zod"; import { validDomainName } from "@/utils/validators"; -type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{ +type DeployNodeConfigFormWangsuCDNProConfigFieldValues = Nullish<{ environment: string; domain: string; certificateId?: string; webhookId?: string; }>; -export type DeployNodeConfigFormBaishanCDNConfigProps = { +export type DeployNodeConfigFormWangsuCDNProConfigProps = { form: FormInstance; formName: string; disabled?: boolean; - initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues; - onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void; + initialValues?: DeployNodeConfigFormWangsuCDNProConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormWangsuCDNProConfigFieldValues) => void; }; const ENVIRONMENT_PRODUCTION = "production" as const; const ENVIRONMENT_STAGING = "stating" as const; -const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => { +const initFormModel = (): DeployNodeConfigFormWangsuCDNProConfigFieldValues => { return { environment: ENVIRONMENT_PRODUCTION, }; }; -const DeployNodeConfigFormBaishanCDNConfig = ({ +const DeployNodeConfigFormWangsuCDNProConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange, -}: DeployNodeConfigFormBaishanCDNConfigProps) => { +}: DeployNodeConfigFormWangsuCDNProConfigProps) => { const { t } = useTranslation(); const formSchema = z.object({ @@ -104,4 +104,4 @@ const DeployNodeConfigFormBaishanCDNConfig = ({ ); }; -export default DeployNodeConfigFormBaishanCDNConfig; +export default DeployNodeConfigFormWangsuCDNProConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx new file mode 100644 index 00000000..739e4b28 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCertificateConfig.tsx @@ -0,0 +1,61 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +type DeployNodeConfigFormWangsuCertificateConfigFieldValues = Nullish<{ + certificateId?: string; +}>; + +export type DeployNodeConfigFormWangsuCertificateConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormWangsuCertificateConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormWangsuCertificateConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormWangsuCertificateConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormWangsuCertificateConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormWangsuCertificateConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + certificateId: z.string().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default DeployNodeConfigFormWangsuCertificateConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 4111f054..e679d13b 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -425,6 +425,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ VOLCENGINE_LIVE: `${ACCESS_PROVIDERS.VOLCENGINE}-live`, VOLCENGINE_TOS: `${ACCESS_PROVIDERS.VOLCENGINE}-tos`, WANGSU_CDNPRO: `${ACCESS_PROVIDERS.WANGSU}-cdnpro`, + WANGSU_CERTIFICATE: `${ACCESS_PROVIDERS.WANGSU}-certificate`, WEBHOOK: `${ACCESS_PROVIDERS.WEBHOOK}`, } as const); @@ -520,6 +521,7 @@ export const deploymentProvidersMap: Maphttps://cdnpro.console.wangsu.com/v2/index/#/certificate", + "workflow_node.deploy.form.wangsu_certificate_id.label": "Wangsu Cloud certificate ID (Optional)", + "workflow_node.deploy.form.wangsu_certificate_id.placeholder": "Please enter Wangsu Cloud certificate ID", + "workflow_node.deploy.form.wangsu_certificate_id.tooltip": "For more information, see https://cdn.console.wangsu.com/v2/index#/certificate/list?code=cert_mylist&parentCode=cert_ssl&productCode=certificatemanagement", "workflow_node.deploy.form.webhook_data.label": "Webhook data (Optional)", "workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data to override the default value", "workflow_node.deploy.form.webhook_data.tooltip": "Leave it blank to use the default Webhook data provided by the authorization.", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 9d997ca0..8c1be41a 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -146,6 +146,7 @@ "provider.volcengine.tos": "火山引擎 - 对象存储 TOS", "provider.wangsu": "网宿云", "provider.wangsu.cdnpro": "网宿云 - CDN Pro", + "provider.wangsu.certificate_upload": "网宿云 - 上传到证书管理", "provider.webhook": "Webhook", "provider.wecombot": "企业微信群机器人", "provider.westcn": "西部数码", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 356c3d64..ada4ec22 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -767,6 +767,9 @@ "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "网宿云 CDN Pro 部署任务 Webhook ID(可选)", "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID", "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate", + "workflow_node.deploy.form.wangsu_certificate_id.label": "网宿云证书 ID(可选)", + "workflow_node.deploy.form.wangsu_certificate_id.placeholder": "请输入网宿云证书 ID", + "workflow_node.deploy.form.wangsu_certificate_id.tooltip": "这是什么?请参阅 https://cdn.console.wangsu.com/v2/index#/certificate/list?code=cert_mylist&parentCode=cert_ssl&productCode=certificatemanagement

不填写时,将上传新证书;否则,将替换原证书。", "workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(可选)", "workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据以覆盖默认值", "workflow_node.deploy.form.webhook_data.tooltip": "不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。",