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",