From 18e72380671067b841391cc87be0f6b84e711060 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Fri, 13 Jun 2025 20:33:46 +0800 Subject: [PATCH] feat: new deployment provider: ctcccloud ao --- internal/deployer/providers.go | 11 +- internal/domain/provider.go | 2 +- .../providers/ctcccloud-ao/ctcccloud_ao.go | 113 ++++++++++++ .../ctcccloud-ao/ctcccloud_ao_test.go | 75 ++++++++ .../providers/ctcccloud-ao/ctcccloud_ao.go | 171 ++++++++++++++++++ .../ctcccloud-ao/ctcccloud_ao_test.go | 72 ++++++++ .../pkg/sdk3rd/ctyun/ao/api_create_cert.go | 41 +++++ .../sdk3rd/ctyun/ao/api_get_domain_config.go | 54 ++++++ internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go | 55 ++++++ .../ctyun/ao/api_modify_domain_config.go | 40 ++++ .../pkg/sdk3rd/ctyun/ao/api_query_cert.go | 51 ++++++ internal/pkg/sdk3rd/ctyun/ao/client.go | 40 ++++ internal/pkg/sdk3rd/ctyun/ao/types.go | 101 +++++++++++ .../workflow/node/DeployNodeConfigForm.tsx | 3 + .../DeployNodeConfigFormCTCCCloudAOConfig.tsx | 65 +++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.provider.json | 2 +- .../i18n/locales/en/nls.workflow.nodes.json | 3 + ui/src/i18n/locales/zh/nls.provider.json | 2 +- .../i18n/locales/zh/nls.workflow.nodes.json | 3 + 20 files changed, 902 insertions(+), 4 deletions(-) create mode 100644 internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go create mode 100644 internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go create mode 100644 internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go create mode 100644 internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/client.go create mode 100644 internal/pkg/sdk3rd/ctyun/ao/types.go create mode 100644 ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index c7bf5f77..2a7f368d 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -43,6 +43,7 @@ import ( pBytePlusCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/byteplus-cdn" pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly" pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly" + pCTCCCloudAO "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao" pCTCCCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-cdn" pCTCCCloudICDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn" pDogeCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/dogecloud-cdn" @@ -623,7 +624,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer return deployer, err } - case domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudICDN: + case domain.DeploymentProviderTypeCTCCCloudAO, domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudICDN: { access := domain.AccessConfigForCTCCCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -631,6 +632,14 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer } switch options.Provider { + case domain.DeploymentProviderTypeCTCCCloudAO: + deployer, err := pCTCCCloudAO.NewDeployer(&pCTCCCloudAO.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + SecretAccessKey: access.SecretAccessKey, + Domain: maputil.GetString(options.ProviderServiceConfig, "domain"), + }) + return deployer, err + case domain.DeploymentProviderTypeCTCCCloudCDN: deployer, err := pCTCCCloudCDN.NewDeployer(&pCTCCCloudCDN.DeployerConfig{ AccessKeyId: access.AccessKeyId, diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 9032d0cf..1e71f22a 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -215,7 +215,7 @@ const ( DeploymentProviderTypeBytePlusCDN = DeploymentProviderType(AccessProviderTypeBytePlus + "-cdn") DeploymentProviderTypeCacheFly = DeploymentProviderType(AccessProviderTypeCacheFly) DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) - DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao") // (预留) + DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao") DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn") DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms") // (预留) DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb") // (预留) diff --git a/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go new file mode 100644 index 00000000..027bcd69 --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao.go @@ -0,0 +1,113 @@ +package ctcccloudao + +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-ao" + ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao" + 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 *ctyunao.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") + } + + // 上传证书到 AccessOne + 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=113&api=13412&data=174&isNormal=1&vid=167 + getDomainConfigReq := &ctyunao.GetDomainConfigRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + } + getDomainConfigResp, err := d.sdkClient.GetDomainConfig(getDomainConfigReq) + d.logger.Debug("sdk request 'cdn.GetDomainConfig'", slog.Any("request", getDomainConfigReq), slog.Any("response", getDomainConfigResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.GetDomainConfig': %w", err) + } + + // 域名基础及加速配置修改 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13413&data=174&isNormal=1&vid=167 + modifyDomainConfigReq := &ctyunao.ModifyDomainConfigRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + ProductCode: typeutil.ToPtr(getDomainConfigResp.ReturnObj.ProductCode), + Origin: getDomainConfigResp.ReturnObj.Origin, + HttpsStatus: typeutil.ToPtr("on"), + CertName: typeutil.ToPtr(upres.CertName), + } + modifyDomainConfigResp, err := d.sdkClient.ModifyDomainConfig(modifyDomainConfigReq) + d.logger.Debug("sdk request 'cdn.ModifyDomainConfig'", slog.Any("request", modifyDomainConfigReq), slog.Any("response", modifyDomainConfigResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'cdn.ModifyDomainConfig': %w", err) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) { + return ctyunao.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go new file mode 100644 index 00000000..3cc42cb3 --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-ao/ctcccloud_ao_test.go @@ -0,0 +1,75 @@ +package ctcccloudao_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-ao" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDAO_" + + 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_ao_test.go -args \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_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-ao/ctcccloud_ao.go b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go new file mode 100644 index 00000000..fa09e3e5 --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao.go @@ -0,0 +1,171 @@ +package ctcccloudao + +import ( + "context" + "fmt" + "log/slog" + "slices" + "strings" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + ctyunao "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/ao" + 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 *ctyunao.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=113&api=13175&data=174&isNormal=1&vid=167 + listCertPage := int32(1) + listCertPerPage := int32(1000) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + listCertReq := &ctyunao.ListCertRequest{ + Page: typeutil.ToPtr(listCertPage), + PerPage: typeutil.ToPtr(listCertPerPage), + UsageMode: typeutil.ToPtr(int32(0)), + } + listCertResp, err := u.sdkClient.ListCert(listCertReq) + u.logger.Debug("sdk request 'ao.ListCert'", slog.Any("request", listCertReq), slog.Any("response", listCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ao.ListCert': %w", err) + } + + if listCertResp.ReturnObj != nil { + for _, certRecord := range listCertResp.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=113&api=13015&data=174&isNormal=1&vid=167 + queryCertReq := &ctyunao.QueryCertRequest{ + Id: typeutil.ToPtr(certRecord.Id), + } + queryCertResp, err := u.sdkClient.QueryCert(queryCertReq) + u.logger.Debug("sdk request 'ao.QueryCert'", slog.Any("request", queryCertReq), slog.Any("response", queryCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ao.QueryCert': %w", err) + } else if queryCertResp.ReturnObj != nil && queryCertResp.ReturnObj.Result != nil { + var isSameCert bool + if queryCertResp.ReturnObj.Result.Certs == certPEM { + isSameCert = true + } else { + oldCertX509, err := certutil.ParseCertificateFromPEM(queryCertResp.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", queryCertResp.ReturnObj.Result.Id), + CertName: queryCertResp.ReturnObj.Result.Name, + }, nil + } + } + } + } + + if listCertResp.ReturnObj == nil || len(listCertResp.ReturnObj.Results) < int(listCertPerPage) { + break + } else { + listCertPage++ + } + } + + // 生成新证书名(需符合天翼云命名规则) + certName := fmt.Sprintf("certimate-%d", time.Now().UnixMilli()) + + // 创建证书 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=113&api=13014&data=174&isNormal=1&vid=167 + createCertReq := &ctyunao.CreateCertRequest{ + Name: typeutil.ToPtr(certName), + Certs: typeutil.ToPtr(certPEM), + Key: typeutil.ToPtr(privkeyPEM), + } + createCertResp, err := u.sdkClient.CreateCert(createCertReq) + u.logger.Debug("sdk request 'ao.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'ao.CreateCert': %w", err) + } + + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), + CertName: certName, + }, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunao.Client, error) { + return ctyunao.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go new file mode 100644 index 00000000..bbf8a0a9 --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-ao/ctcccloud_ao_test.go @@ -0,0 +1,72 @@ +package ctcccloudao_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-ao" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDAO_" + + 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_ao_test.go -args \ + --CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_CTCCCLOUDAO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDAO_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/ao/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go new file mode 100644 index 00000000..b45543f0 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/api_create_cert.go @@ -0,0 +1,41 @@ +package ao + +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 int32 `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, "/ctapi/v1/accessone/cert/create") + 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/ao/api_get_domain_config.go b/internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go new file mode 100644 index 00000000..6085adf6 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/api_get_domain_config.go @@ -0,0 +1,54 @@ +package ao + +import ( + "context" + "net/http" +) + +type GetDomainConfigRequest struct { + Domain *string `json:"domain,omitempty"` + ProductCode *string `json:"product_code,omitempty"` +} + +type GetDomainConfigResponse 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"` + Origin []*DomainOriginConfig `json:"origin,omitempty"` + HttpsStatus string `json:"https_status"` + HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` + CertName string `json:"cert_name"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) GetDomainConfig(req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { + return c.GetDomainConfigWithContext(context.Background(), req) +} + +func (c *Client) GetDomainConfigWithContext(ctx context.Context, req *GetDomainConfigRequest) (*GetDomainConfigResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/domain/config") + 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 := &GetDomainConfigResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go new file mode 100644 index 00000000..b34c199a --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/api_list_cert.go @@ -0,0 +1,55 @@ +package ao + +import ( + "context" + "net/http" + "strconv" +) + +type ListCertRequest struct { + Page *int32 `json:"page,omitempty"` + PerPage *int32 `json:"per_page,omitempty"` + UsageMode *int32 `json:"usage_mode,omitempty"` +} + +type ListCertResponse 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) ListCert(req *ListCertRequest) (*ListCertResponse, error) { + return c.ListCertWithContext(context.Background(), req) +} + +func (c *Client) ListCertWithContext(ctx context.Context, req *ListCertRequest) (*ListCertResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/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 := &ListCertResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go b/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go new file mode 100644 index 00000000..f28b3dc9 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/api_modify_domain_config.go @@ -0,0 +1,40 @@ +package ao + +import ( + "context" + "net/http" +) + +type ModifyDomainConfigRequest struct { + Domain *string `json:"domain,omitempty"` + ProductCode *string `json:"product_code,omitempty"` + Origin []*DomainOriginConfig `json:"origin,omitempty"` + HttpsStatus *string `json:"https_status,omitempty"` + HttpsBasic *DomainHttpsBasicConfig `json:"https_basic,omitempty"` + CertName *string `json:"cert_name,omitempty"` +} + +type ModifyDomainConfigResponse struct { + baseResult +} + +func (c *Client) ModifyDomainConfig(req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) { + return c.ModifyDomainConfigWithContext(context.Background(), req) +} + +func (c *Client) ModifyDomainConfigWithContext(ctx context.Context, req *ModifyDomainConfigRequest) (*ModifyDomainConfigResponse, error) { + httpreq, err := c.newRequest(http.MethodPost, "/ctapi/v1/accessone/domain/modify_config") + if err != nil { + return nil, err + } else { + httpreq.SetBody(req) + httpreq.SetContext(ctx) + } + + result := &ModifyDomainConfigResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go b/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go new file mode 100644 index 00000000..f8b203d0 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/api_query_cert.go @@ -0,0 +1,51 @@ +package ao + +import ( + "context" + "net/http" + "strconv" +) + +type QueryCertRequest struct { + Id *int32 `json:"id,omitempty"` + Name *string `json:"name,omitempty"` + UsageMode *int32 `json:"usage_mode,omitempty"` +} + +type QueryCertResponse struct { + baseResult + + ReturnObj *struct { + Result *CertDetail `json:"result,omitempty"` + } `json:"returnObj,omitempty"` +} + +func (c *Client) QueryCert(req *QueryCertRequest) (*QueryCertResponse, error) { + return c.QueryCertWithContext(context.Background(), req) +} + +func (c *Client) QueryCertWithContext(ctx context.Context, req *QueryCertRequest) (*QueryCertResponse, error) { + httpreq, err := c.newRequest(http.MethodGet, "/ctapi/v1/accessone/cert/query") + 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 := &QueryCertResponse{} + if _, err := c.doRequestWithResult(httpreq, result); err != nil { + return result, err + } + + return result, nil +} diff --git a/internal/pkg/sdk3rd/ctyun/ao/client.go b/internal/pkg/sdk3rd/ctyun/ao/client.go new file mode 100644 index 00000000..822f1a4f --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/client.go @@ -0,0 +1,40 @@ +package ao + +import ( + "time" + + "github.com/go-resty/resty/v2" + "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi" +) + +const endpoint = "https://accessone-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) { + return c.client.DoRequestWithResult(request, result) +} diff --git a/internal/pkg/sdk3rd/ctyun/ao/types.go b/internal/pkg/sdk3rd/ctyun/ao/types.go new file mode 100644 index 00000000..98b62097 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/ao/types.go @@ -0,0 +1,101 @@ +package ao + +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 int32 `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"` +} + +type DomainOriginConfig struct { + Origin string `json:"origin"` + Role string `json:"role"` + Weight int32 `json:"weight"` +} + +type DomainHttpsBasicConfig struct { + HttpsForce string `json:"https_force"` + ForceStatus string `json:"force_status"` +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 49eb910b..ae208c1c 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -48,6 +48,7 @@ import DeployNodeConfigFormBaotaWAFSiteConfig from "./DeployNodeConfigFormBaotaW import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNConfig.tsx"; import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig"; import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig"; +import DeployNodeConfigFormCTCCCloudAOConfig from "./DeployNodeConfigFormCTCCCloudAOConfig"; import DeployNodeConfigFormCTCCCloudCDNConfig from "./DeployNodeConfigFormCTCCCloudCDNConfig"; import DeployNodeConfigFormCTCCCloudICDNConfig from "./DeployNodeConfigFormCTCCCloudICDNConfig"; import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; @@ -267,6 +268,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.CDNFLY: return ; + case DEPLOYMENT_PROVIDERS.CTCCCLOUD_AO: + return ; case DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN: return ; case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.tsx new file mode 100644 index 00000000..f46934cd --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudAOConfig.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 DeployNodeConfigFormCTCCCloudAOConfigFieldValues = Nullish<{ + domain: string; +}>; + +export type DeployNodeConfigFormCTCCCloudAOConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormCTCCCloudAOConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormCTCCCloudAOConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormCTCCCloudAOConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormCTCCCloudAOConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormCTCCCloudAOConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + domain: z + .string({ message: t("workflow_node.deploy.form.ctcccloud_ao_domain.placeholder") }) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default DeployNodeConfigFormCTCCCloudAOConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 84591d7e..5caf1f05 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -413,6 +413,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ BYTEPLUS_CDN: `${ACCESS_PROVIDERS.BYTEPLUS}-cdn`, CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`, CDNFLY: `${ACCESS_PROVIDERS.CDNFLY}`, + CTCCCLOUD_AO: `${ACCESS_PROVIDERS.CTCCCLOUD}-ao`, CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`, CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`, DOGECLOUD_CDN: `${ACCESS_PROVIDERS.DOGECLOUD}-cdn`, @@ -574,6 +575,7 @@ export const deploymentProvidersMap: Maphttps://cdn.ctyun.cn/h5/ctaccessone/", "workflow_node.deploy.form.ctcccloud_cdn_domain.label": "CTCC StateCloud CDN domain", "workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder": "Please enter CTCC StateCloud CDN domain name", "workflow_node.deploy.form.ctcccloud_cdn_domain.tooltip": "For more information, see https://cdn-console.ctyun.cn", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 2134e402..865f5b48 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -60,7 +60,7 @@ "provider.cmcccloud.dns": "移动云 - 云解析 DNS", "provider.constellix": "Constellix", "provider.ctcccloud": "天翼云", - "provider.ctcccloud.accessone": "天翼云 - 边缘安全加速平台 AccessOne", + "provider.ctcccloud.ao": "天翼云 - 边缘安全加速平台 AccessOne", "provider.ctcccloud.cdn": "天翼云 - 内容分发网络 CDN", "provider.ctcccloud.cms_upload": "天翼云 - 上传到证书管理服务 CMS", "provider.ctcccloud.elb": "天翼云 - 弹性负载均衡 ELB", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 882bd603..92a88f93 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -397,6 +397,9 @@ "workflow_node.deploy.form.cdnfly_certificate_id.label": "Cdnfly 证书 ID", "workflow_node.deploy.form.cdnfly_certificate_id.placeholder": "请输入 Cdnfly 证书 ID", "workflow_node.deploy.form.cdnfly_certificate_id.tooltip": "请登录 Cdnfly 控制台查看。", + "workflow_node.deploy.form.ctcccloud_ao_domain.label": "天翼云 AccessOne 加速域名", + "workflow_node.deploy.form.ctcccloud_ao_domain.placeholder": "请输入天翼云 AccessOne 加速域名(支持泛域名)", + "workflow_node.deploy.form.ctcccloud_ao_domain.tooltip": "这是什么?请参阅 https://cdn.ctyun.cn/h5/ctaccessone/", "workflow_node.deploy.form.ctcccloud_cdn_domain.label": "天翼云 CDN 加速域名", "workflow_node.deploy.form.ctcccloud_cdn_domain.placeholder": "请输入天翼云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.ctcccloud_cdn_domain.tooltip": "这是什么?请参阅 https://cdn-console.ctyun.cn",