From 5d6bc03f215ced49451d900de91dd601f16fe316 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sat, 14 Jun 2025 10:27:49 +0800 Subject: [PATCH] feat: new deployment provider: ctcccloud lvdn --- internal/deployer/providers.go | 11 +- internal/domain/provider.go | 2 +- .../ctcccloud-lvdn/ctcccloud_lvdn.go | 113 ++++++++++++ .../ctcccloud-lvdn/ctcccloud_lvdn_test.go | 75 ++++++++ .../ctcccloud-lvdn/ctcccloud_lvdn.go | 171 ++++++++++++++++++ .../ctcccloud-lvdn/ctcccloud_lvdn_test.go | 72 ++++++++ internal/pkg/sdk3rd/ctyun/cms/client.go | 3 +- internal/pkg/sdk3rd/ctyun/dns/client.go | 14 +- internal/pkg/sdk3rd/ctyun/dns/types.go | 62 ++++++- .../pkg/sdk3rd/ctyun/lvdn/api_create_cert.go | 41 +++++ .../ctyun/lvdn/api_query_cert_detail.go | 51 ++++++ .../sdk3rd/ctyun/lvdn/api_query_cert_list.go | 55 ++++++ .../ctyun/lvdn/api_query_domain_detail.go | 52 ++++++ .../sdk3rd/ctyun/lvdn/api_update_domain.go | 38 ++++ internal/pkg/sdk3rd/ctyun/lvdn/client.go | 49 +++++ internal/pkg/sdk3rd/ctyun/lvdn/types.go | 90 +++++++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + ...eployNodeConfigFormCTCCCloudLVDNConfig.tsx | 65 +++++++ ui/src/domain/provider.ts | 2 + .../i18n/locales/en/nls.workflow.nodes.json | 3 + .../i18n/locales/zh/nls.workflow.nodes.json | 3 + 21 files changed, 969 insertions(+), 6 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go create mode 100644 internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go create mode 100644 internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go create mode 100644 internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/client.go create mode 100644 internal/pkg/sdk3rd/ctyun/lvdn/types.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index fd668ace..f905285d 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -47,6 +47,7 @@ import ( pCTCCCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn" pCTCCCloudCMS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cms" pCTCCCloudICDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn" + pCTCCCloudLVDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn" pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" pFlexCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn" @@ -625,7 +626,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer return deployer, err } - case domain.DeploymentProviderTypeCTCCCloudAO, domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudCMS, domain.DeploymentProviderTypeCTCCCloudICDN: + case domain.DeploymentProviderTypeCTCCCloudAO, domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudCMS, domain.DeploymentProviderTypeCTCCCloudICDN, domain.DeploymentProviderTypeCTCCCloudLVDN: { access := domain.AccessConfigForCTCCCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -664,6 +665,14 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer }) return deployer, err + case domain.DeploymentProviderTypeCTCCCloudLVDN: + deployer, err := pCTCCCloudLVDN.NewDeployer(&pCTCCCloudLVDN.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + }) + return deployer, err + default: break } diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 302b8e11..924f58bd 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -220,7 +220,7 @@ const ( DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms") DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb") // (预留) DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn") - DeploymentProviderTypeDogeCloudLVDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-ldvn") // (预留) + DeploymentProviderTypeCTCCCloudLVDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ldvn") DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications") DeploymentProviderTypeFlexCDN = DeploymentProviderType(AccessProviderTypeFlexCDN) diff --git a/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go new file mode 100644 index 00000000..b655c697 --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn.go @@ -0,0 +1,113 @@ +package ctcccloudlvdn + +import ( + "context" + "errors" + "fmt" + "log/slog" + + "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/ctcccloud-lvdn" + ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn" + typeutil "github.com/usual2970/certimate/internal/pkg/utils/type" +) + +type DeployerConfig struct { + // 天翼云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 天翼云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` + // 加速域名(不支持泛域名)。 + Domain string `json:"domain"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *ctyunlvdn.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.SecretAccessKey) + if err != nil { + return nil, fmt.Errorf("failed to create sdk client: %w", err) + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + AccessKeyId: config.AccessKeyId, + SecretAccessKey: config.SecretAccessKey, + }) + 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.New(slog.DiscardHandler) + } else { + d.logger = logger + } + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*deployer.DeployResult, error) { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + // 上传证书到 CDN + 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)) + } + + // 查询域名配置信息 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11473&data=183&isNormal=1&vid=261 + queryDomainDetailReq := &ctyunlvdn.QueryDomainDetailRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + ProductCode: typeutil.ToPtr("005"), + } + queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq) + d.logger.Debug("sdk request 'lvdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryDomainDetail': %w", err) + } + + // 修改域名配置 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=108&api=11308&data=161&isNormal=1&vid=154 + updateDomainReq := &ctyunlvdn.UpdateDomainRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + ProductCode: typeutil.ToPtr("005"), + HttpsSwitch: typeutil.ToPtr(int32(1)), + CertName: typeutil.ToPtr(upres.CertName), + } + updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq) + d.logger.Debug("sdk request 'lvdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'lvdn.UpdateDomain': %w", err) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) { + return ctyunlvdn.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go new file mode 100644 index 00000000..84257a0f --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go @@ -0,0 +1,75 @@ +package ctcccloudlvdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-lvdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./ctcccloud_lvdn_test.go -args \ + --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDLVDN_DOMAIN="example.com" +*/ +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("SECRETACCESSKEY: %v", fSecretAccessKey), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + Domain: fDomain, + }) + 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/ctcccloud-lvdn/ctcccloud_lvdn.go b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go new file mode 100644 index 00000000..53453b1c --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn.go @@ -0,0 +1,171 @@ +package ctcccloudlvdn + +import ( + "context" + "fmt" + "log/slog" + "slices" + "strings" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + ctyunlvdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/lvdn" + 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"` + // 天翼云 SecretAccessKey。 + SecretAccessKey string `json:"secretAccessKey"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *ctyunlvdn.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.SecretAccessKey) + 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.New(slog.DiscardHandler) + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPEM string, privkeyPEM string) (*uploader.UploadResult, error) { + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPEM) + if err != nil { + return nil, err + } + + // 查询证书列表,避免重复上传 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11452&data=183&isNormal=1&vid=261 + queryCertListPage := int32(1) + queryCertListPerPage := int32(1000) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + queryCertListReq := &ctyunlvdn.QueryCertListRequest{ + Page: typeutil.ToPtr(queryCertListPage), + PerPage: typeutil.ToPtr(queryCertListPerPage), + UsageMode: typeutil.ToPtr(int32(0)), + } + queryCertListResp, err := u.sdkClient.QueryCertList(queryCertListReq) + u.logger.Debug("sdk request 'lvdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertList': %w", err) + } + + if queryCertListResp.ReturnObj != nil { + for _, certRecord := range queryCertListResp.ReturnObj.Results { + // 对比证书通用名称 + if !strings.EqualFold(certX509.Subject.CommonName, certRecord.CN) { + continue + } + + // 对比证书扩展名称 + if !slices.Equal(certX509.DNSNames, certRecord.SANs) { + continue + } + + // 对比证书有效期 + if !certX509.NotBefore.Equal(time.Unix(certRecord.IssueTime, 0).UTC()) { + continue + } else if !certX509.NotAfter.Equal(time.Unix(certRecord.ExpiresTime, 0).UTC()) { + continue + } + + // 查询证书详情 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11449&data=183&isNormal=1&vid=261 + queryCertDetailReq := &ctyunlvdn.QueryCertDetailRequest{ + Id: typeutil.ToPtr(certRecord.Id), + } + queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq) + u.logger.Debug("sdk request 'lvdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'lvdn.QueryCertDetail': %w", err) + } else if queryCertDetailResp.ReturnObj != nil && queryCertDetailResp.ReturnObj.Result != nil { + var isSameCert bool + if queryCertDetailResp.ReturnObj.Result.Certs == certPEM { + isSameCert = true + } else { + oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertDetailResp.ReturnObj.Result.Certs) + if err != nil { + continue + } + + isSameCert = certutil.EqualCertificate(certX509, oldCertX509) + } + + // 如果已存在相同证书,直接返回 + if isSameCert { + u.logger.Info("ssl certificate already exists") + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", queryCertDetailResp.ReturnObj.Result.Id), + CertName: queryCertDetailResp.ReturnObj.Result.Name, + }, nil + } + } + } + } + + if queryCertListResp.ReturnObj == nil || len(queryCertListResp.ReturnObj.Results) < int(queryCertListPerPage) { + break + } else { + queryCertListPage++ + } + } + + // 生成新证书名(需符合天翼云命名规则) + certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 创建证书 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=125&api=11436&data=183&isNormal=1&vid=261 + createCertReq := &ctyunlvdn.CreateCertRequest{ + Name: typeutil.ToPtr(certName), + Certs: typeutil.ToPtr(certPEM), + Key: typeutil.ToPtr(privkeyPEM), + } + createCertResp, err := u.sdkClient.CreateCert(createCertReq) + u.logger.Debug("sdk request 'lvdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'lvdn.CreateCert': %w", err) + } + + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), + CertName: certName, + }, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunlvdn.Client, error) { + return ctyunlvdn.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go new file mode 100644 index 00000000..3bcedfdd --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-lvdn/ctcccloud_lvdn_test.go @@ -0,0 +1,72 @@ +package ctcccloudlvdn_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-lvdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDLVDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fSecretAccessKey, argsPrefix+"SECRETACCESSKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./ctcccloud_lvdn_test.go -args \ + --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_UPLOADER_CTCCCLOUDLVDN_SECRETACCESSKEY="your-secret-access-key" +*/ +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("SECRETACCESSKEY: %v", fSecretAccessKey), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + AccessKeyId: fAccessKeyId, + SecretAccessKey: fSecretAccessKey, + }) + 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/ctyun/cms/client.go b/internal/pkg/sdk3rd/ctyun/cms/client.go index 015829d5..ac94d1b6 100644 --- a/internal/pkg/sdk3rd/ctyun/cms/client.go +++ b/internal/pkg/sdk3rd/ctyun/cms/client.go @@ -40,7 +40,8 @@ func (c *Client) doRequestWithResult(request *resty.Request, result baseResultIn response, err := c.client.DoRequestWithResult(request, result) if err == nil { statusCode := result.GetStatusCode() - if statusCode != "" && statusCode != "200" { + errorCode := result.GetError() + if (statusCode != "" && statusCode != "200") || errorCode != "" { return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage()) } } diff --git a/internal/pkg/sdk3rd/ctyun/dns/client.go b/internal/pkg/sdk3rd/ctyun/dns/client.go index c2f2b594..4e684bd8 100644 --- a/internal/pkg/sdk3rd/ctyun/dns/client.go +++ b/internal/pkg/sdk3rd/ctyun/dns/client.go @@ -1,6 +1,7 @@ package dns import ( + "fmt" "time" "github.com/go-resty/resty/v2" @@ -35,6 +36,15 @@ func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) { return c.client.DoRequest(request) } -func (c *Client) doRequestWithResult(request *resty.Request, result any) (*resty.Response, error) { - return c.client.DoRequestWithResult(request, result) +func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) { + response, err := c.client.DoRequestWithResult(request, result) + if err == nil { + statusCode := result.GetStatusCode() + errorCode := result.GetError() + if (statusCode != "" && statusCode != "200") || errorCode != "" { + return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage()) + } + } + + return response, err } diff --git a/internal/pkg/sdk3rd/ctyun/dns/types.go b/internal/pkg/sdk3rd/ctyun/dns/types.go index bcd21d76..225f60d7 100644 --- a/internal/pkg/sdk3rd/ctyun/dns/types.go +++ b/internal/pkg/sdk3rd/ctyun/dns/types.go @@ -1,6 +1,17 @@ package dns -import "encoding/json" +import ( + "bytes" + "encoding/json" + "strconv" +) + +type baseResultInterface interface { + GetStatusCode() string + GetMessage() string + GetError() string + GetErrorMessage() string +} type baseResult struct { StatusCode json.RawMessage `json:"statusCode,omitempty"` @@ -10,6 +21,55 @@ type baseResult struct { RequestId *string `json:"requestId,omitempty"` } +func (r *baseResult) GetStatusCode() string { + if r.StatusCode == nil { + return "" + } + + decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) + token, err := decoder.Token() + if err != nil { + return "" + } + + switch t := token.(type) { + case string: + return t + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + case json.Number: + return t.String() + default: + return "" + } +} + +func (r *baseResult) GetMessage() string { + if r.Message == nil { + return "" + } + + return *r.Message +} + +func (r *baseResult) GetError() string { + if r.Error == nil { + return "" + } + + return *r.Error +} + +func (r *baseResult) GetErrorMessage() string { + if r.ErrorMessage == nil { + return "" + } + + return *r.ErrorMessage +} + +var _ baseResultInterface = (*baseResult)(nil) + type DnsRecord struct { RecordId int32 `json:"recordId"` Host string `json:"host"` diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go new file mode 100644 index 00000000..c0188d3d --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_create_cert.go @@ -0,0 +1,41 @@ +package lvdn + +import ( + "context" + "net/http" +) + +type CreateCertRequest struct { + Name *string `json:"name,omitempty"` + Certs *string `json:"certs,omitempty"` + Key *string `json:"key,omitempty"` +} + +type CreateCertResponse struct { + baseResult + + ReturnObj *struct { + Id int64 `json:"id"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) CreateCert(req *CreateCertRequest) (*CreateCertResponse, error) { + return c.CreateCertWithContext(context.Background(), req) +} + +func (c *Client) CreateCertWithContext(ctx context.Context, req *CreateCertRequest) (*CreateCertResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/cert/creat-cert") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &CreateCertResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go new file mode 100644 index 00000000..cadcc6dc --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_detail.go @@ -0,0 +1,51 @@ +package lvdn + +import ( + "context" + "net/http" + "strconv" +) + +type QueryCertDetailRequest struct { + Id *int64 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + UsageMode *int32 `json:"usage_mode,omitempty"` +} + +type QueryCertDetailResponse struct { + baseResult + + ReturnObj *struct { + Result *CertDetail `json:"result,omitempty"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) QueryCertDetail(req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { + return c.QueryCertDetailWithContext(context.Background(), req) +} + +func (c *Client) QueryCertDetailWithContext(ctx context.Context, req *QueryCertDetailRequest) (*QueryCertDetailResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-detail") + if err != nil { + return nil, err + } else { + if req.Id != nil { + httpreq.SetQueryParam("id", strconv.Itoa(int(*req.Id))) + } + if req.Name != nil { + httpreq.SetQueryParam("name", *req.Name) + } + if req.UsageMode != nil { + httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode))) + } + + httpreq.SetContext(ctx) + } + + result := &QueryCertDetailResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go new file mode 100644 index 00000000..d1a7b974 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_cert_list.go @@ -0,0 +1,55 @@ +package lvdn + +import ( + "context" + "net/http" + "strconv" +) + +type QueryCertListRequest struct { + Page *int32 `json:"page,omitempty"` + PerPage *int32 `json:"per_page,omitempty"` + UsageMode *int32 `json:"usage_mode,omitempty"` +} + +type QueryCertListResponse struct { + baseResult + + ReturnObj *struct { + Results []*CertRecord `json:"result,omitempty"` + Page int32 `json:"page,omitempty"` + PerPage int32 `json:"per_page,omitempty"` + TotalPage int32 `json:"total_page,omitempty"` + TotalRecords int32 `json:"total_records,omitempty"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) QueryCertList(req *QueryCertListRequest) (*QueryCertListResponse, error) { + return c.QueryCertListWithContext(context.Background(), req) +} + +func (c *Client) QueryCertListWithContext(ctx context.Context, req *QueryCertListRequest) (*QueryCertListResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/cert/query-cert-list") + if err != nil { + return nil, err + } else { + if req.Page != nil { + httpreq.SetQueryParam("page", strconv.Itoa(int(*req.Page))) + } + if req.PerPage != nil { + httpreq.SetQueryParam("per_page", strconv.Itoa(int(*req.PerPage))) + } + if req.UsageMode != nil { + httpreq.SetQueryParam("usage_mode", strconv.Itoa(int(*req.UsageMode))) + } + + httpreq.SetContext(ctx) + } + + result := &QueryCertListResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go new file mode 100644 index 00000000..29e5f08f --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_query_domain_detail.go @@ -0,0 +1,52 @@ +package lvdn + +import ( + "context" + "net/http" +) + +type QueryDomainDetailRequest struct { + Domain *string `json:"domain,omitempty"` + ProductCode *string `json:"product_code,omitempty"` +} + +type QueryDomainDetailResponse struct { + baseResult + + ReturnObj *struct { + Domain string `json:"domain"` + ProductCode string `json:"product_code"` + Status int32 `json:"status"` + AreaScope int32 `json:"area_scope"` + Cname string `json:"cname"` + HttpsSwitch int32 `json:"https_switch"` + CertName string `json:"cert_name"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) QueryDomainDetail(req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { + return c.QueryDomainDetailWithContext(context.Background(), req) +} + +func (c *Client) QueryDomainDetailWithContext(ctx context.Context, req *QueryDomainDetailRequest) (*QueryDomainDetailResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/live/domain/query-domain-detail") + if err != nil { + return nil, err + } else { + if req.Domain != nil { + httpreq.SetQueryParam("domain", *req.Domain) + } + if req.ProductCode != nil { + httpreq.SetQueryParam("product_code", *req.ProductCode) + } + + httpreq.SetContext(ctx) + } + + result := &QueryDomainDetailResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go b/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go new file mode 100644 index 00000000..d5f90306 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/api_update_domain.go @@ -0,0 +1,38 @@ +package lvdn + +import ( + "context" + "net/http" +) + +type UpdateDomainRequest struct { + Domain *string `json:"domain,omitempty"` + ProductCode *string `json:"product_code,omitempty"` + HttpsSwitch *int32 `json:"https_switch,omitempty"` + CertName *string `json:"cert_name,omitempty"` +} + +type UpdateDomainResponse struct { + baseResult +} + +func (c *Client) UpdateDomain(req *UpdateDomainRequest) (*UpdateDomainResponse, error) { + return c.UpdateDomainWithContext(context.Background(), req) +} + +func (c *Client) UpdateDomainWithContext(ctx context.Context, req *UpdateDomainRequest) (*UpdateDomainResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/live/domain/update-domain") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &UpdateDomainResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/client.go b/internal/pkg/sdk3rd/ctyun/lvdn/client.go new file mode 100644 index 00000000..5542bad9 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/client.go @@ -0,0 +1,49 @@ +package lvdn + +import ( + "fmt" + "time" + + "github.com/go-resty/resty/v2" + "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi" +) + +const endpoint = "https://ctlvdn-global.ctapi.ctyun.cn" + +type Client struct { + client *openapi.Client +} + +func NewClient(accessKeyId, secretAccessKey string) (*Client, error) { + client, err := openapi.NewClient(endpoint, accessKeyId, secretAccessKey) + 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) (*resty.Request, error) { + return c.client.NewRequest(method, path) +} + +func (c *Client) doRequest(request *resty.Request) (*resty.Response, error) { + return c.client.DoRequest(request) +} + +func (c *Client) doRequestWithResult(request *resty.Request, result baseResultInterface) (*resty.Response, error) { + response, err := c.client.DoRequestWithResult(request, result) + if err == nil { + statusCode := result.GetStatusCode() + if statusCode != "" && statusCode != "100000" { + return response, fmt.Errorf("sdkerr: api error, code='%s', message='%s', errorCode='%s', errorMessage='%s'", statusCode, result.GetMessage(), result.GetMessage(), result.GetErrorMessage()) + } + } + + return response, err +} diff --git a/internal/pkg/sdk3rd/ctyun/lvdn/types.go b/internal/pkg/sdk3rd/ctyun/lvdn/types.go new file mode 100644 index 00000000..2ddc5369 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/lvdn/types.go @@ -0,0 +1,90 @@ +package lvdn + +import ( + "bytes" + "encoding/json" + "strconv" +) + +type baseResultInterface interface { + GetStatusCode() string + GetMessage() string + GetError() string + GetErrorMessage() string +} + +type baseResult struct { + StatusCode json.RawMessage `json:"statusCode,omitempty"` + Message *string `json:"message,omitempty"` + Error *string `json:"error,omitempty"` + ErrorMessage *string `json:"errorMessage,omitempty"` + RequestId *string `json:"requestId,omitempty"` +} + +func (r *baseResult) GetStatusCode() string { + if r.StatusCode == nil { + return "" + } + + decoder := json.NewDecoder(bytes.NewReader(r.StatusCode)) + token, err := decoder.Token() + if err != nil { + return "" + } + + switch t := token.(type) { + case string: + return t + case float64: + return strconv.FormatFloat(t, 'f', -1, 64) + case json.Number: + return t.String() + default: + return "" + } +} + +func (r *baseResult) GetMessage() string { + if r.Message == nil { + return "" + } + + return *r.Message +} + +func (r *baseResult) GetError() string { + if r.Error == nil { + return "" + } + + return *r.Error +} + +func (r *baseResult) GetErrorMessage() string { + if r.ErrorMessage == nil { + return "" + } + + return *r.ErrorMessage +} + +var _ baseResultInterface = (*baseResult)(nil) + +type CertRecord struct { + Id int64 `json:"id"` + Name string `json:"name"` + CN string `json:"cn"` + SANs []string `json:"sans"` + UsageMode int32 `json:"usage_mode"` + State int32 `json:"state"` + ExpiresTime int64 `json:"expires"` + IssueTime int64 `json:"issue"` + Issuer string `json:"issuer"` + CreatedTime int64 `json:"created"` +} + +type CertDetail struct { + CertRecord + Certs string `json:"certs"` + Key string `json:"key"` +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index ae208c1c..e1bee0c9 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -51,6 +51,7 @@ import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig import DeployNodeConfigFormCTCCCloudAOConfig from "./DeployNodeConfigFormCTCCCloudAOConfig"; import DeployNodeConfigFormCTCCCloudCDNConfig from "./DeployNodeConfigFormCTCCCloudCDNConfig"; import DeployNodeConfigFormCTCCCloudICDNConfig from "./DeployNodeConfigFormCTCCCloudICDNConfig"; +import DeployNodeConfigFormCTCCCloudLVDNConfig from "./DeployNodeConfigFormCTCCCloudLVDNConfig"; import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; import DeployNodeConfigFormFlexCDNConfig from "./DeployNodeConfigFormFlexCDNConfig"; @@ -274,6 +275,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN: return ; + case DEPLOYMENT_PROVIDERS.CTCCCLOUD_LVDN: + return ; case DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN: return ; case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx new file mode 100644 index 00000000..54f22907 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudLVDNConfig.tsx @@ -0,0 +1,65 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +type DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues = Nullish<{ + domain: string; +}>; + +export type DeployNodeConfigFormCTCCCloudLVDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormCTCCCloudLVDNConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormCTCCCloudLVDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormCTCCCloudLVDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + domain: z + .string({ message: t("workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder") }) + .refine((v) => validDomainName(v), t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default DeployNodeConfigFormCTCCCloudLVDNConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 10a23571..d54494ad 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -417,6 +417,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`, CTCCCLOUD_CMS: `${ACCESS_PROVIDERS.CTCCCLOUD}-cms`, CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`, + CTCCCLOUD_LVDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-lvdn`, DOGECLOUD_CDN: `${ACCESS_PROVIDERS.DOGECLOUD}-cdn`, EDGIO_APPLICATIONS: `${ACCESS_PROVIDERS.EDGIO}-applications`, FLEXCDN: `${ACCESS_PROVIDERS.FLEXCDN}`, @@ -577,6 +578,7 @@ export const deploymentProvidersMap: Maphttps://cdn-console.ctyun.cn", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.label": "CTCC StateCloud LVDN domain", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder": "Please enter CTCC StateCloud LVDN domain name", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.tooltip": "For more information, see https://cdn.ctyun.cn/h5/live/index", "workflow_node.deploy.form.dogecloud_cdn_domain.label": "Doge Cloud CDN domain", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "Please enter Doge Cloud CDN domain name", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "For more information, see https://console.dogecloud.com", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 92a88f93..f22a23e7 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -406,6 +406,9 @@ "workflow_node.deploy.form.ctcccloud_icdn_domain.label": "天翼云 ICDN 加速域名", "workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder": "请输入天翼云 ICDN 加速域名(支持泛域名)", "workflow_node.deploy.form.ctcccloud_icdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.ctyun.cn", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.label": "天翼云 LVDN 加速域名", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.placeholder": "请输入天翼云 LVDN 加速域名", + "workflow_node.deploy.form.ctcccloud_lvdn_domain.tooltip": "这是什么?请参阅 https://cdn.ctyun.cn/h5/live/index", "workflow_node.deploy.form.dogecloud_cdn_domain.label": "多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.placeholder": "请输入多吉云 CDN 加速域名", "workflow_node.deploy.form.dogecloud_cdn_domain.tooltip": "这是什么?请参阅 https://console.dogecloud.com",