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