diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index f3d93a2c..c7bf5f77 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -44,6 +44,7 @@ import ( pCacheFly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cachefly" pCdnfly "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/cdnfly" 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" pEdgioApplications "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/edgio-applications" pFlexCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/flexcdn" @@ -622,7 +623,7 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer return deployer, err } - case domain.DeploymentProviderTypeCTCCCloudCDN: + case domain.DeploymentProviderTypeCTCCCloudCDN, domain.DeploymentProviderTypeCTCCCloudICDN: { access := domain.AccessConfigForCTCCCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { @@ -638,6 +639,14 @@ func createDeployerProvider(options *deployerProviderOptions) (deployer.Deployer }) return deployer, err + case domain.DeploymentProviderTypeCTCCCloudICDN: + deployer, err := pCTCCCloudICDN.NewDeployer(&pCTCCCloudICDN.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 f399c21b..9032d0cf 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -217,9 +217,9 @@ const ( DeploymentProviderTypeCdnfly = DeploymentProviderType(AccessProviderTypeCdnfly) DeploymentProviderTypeCTCCCloudAO = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-ao") // (预留) DeploymentProviderTypeCTCCCloudCDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cdn") - DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms") // (预留) - DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb") // (预留) - DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn") // (预留) + DeploymentProviderTypeCTCCCloudCMS = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-cms") // (预留) + DeploymentProviderTypeCTCCCloudELB = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-elb") // (预留) + DeploymentProviderTypeCTCCCloudICDN = DeploymentProviderType(AccessProviderTypeCTCCCloud + "-icdn") DeploymentProviderTypeDogeCloudLVDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-ldvn") // (预留) DeploymentProviderTypeDogeCloudCDN = DeploymentProviderType(AccessProviderTypeDogeCloud + "-cdn") DeploymentProviderTypeEdgioApplications = DeploymentProviderType(AccessProviderTypeEdgio + "-applications") diff --git a/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go new file mode 100644 index 00000000..621bd698 --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn.go @@ -0,0 +1,111 @@ +package ctcccloudicdn + +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-icdn" + ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn" + 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 *ctyunicdn.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") + } + + // 上传证书到 ICDN + 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=112&api=10849&data=173&isNormal=1&vid=166 + queryDomainDetailReq := &ctyunicdn.QueryDomainDetailRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + } + queryDomainDetailResp, err := d.sdkClient.QueryDomainDetail(queryDomainDetailReq) + d.logger.Debug("sdk request 'icdn.QueryDomainDetail'", slog.Any("request", queryDomainDetailReq), slog.Any("response", queryDomainDetailResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'icdn.QueryDomainDetail': %w", err) + } + + // 修改域名配置 + // REF: https://eop.ctyun.cn/ebp/ctapiDocument/search?sid=112&api=10853&data=173&isNormal=1&vid=166 + updateDomainReq := &ctyunicdn.UpdateDomainRequest{ + Domain: typeutil.ToPtr(d.config.Domain), + HttpsStatus: typeutil.ToPtr("on"), + CertName: typeutil.ToPtr(upres.CertName), + } + updateDomainResp, err := d.sdkClient.UpdateDomain(updateDomainReq) + d.logger.Debug("sdk request 'icdn.UpdateDomain'", slog.Any("request", updateDomainReq), slog.Any("response", updateDomainResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'icdn.UpdateDomain': %w", err) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) { + return ctyunicdn.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go new file mode 100644 index 00000000..df514ea6 --- /dev/null +++ b/internal/pkg/core/deployer/providers/ctcccloud-icdn/ctcccloud_icdn_test.go @@ -0,0 +1,75 @@ +package ctcccloudicdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ctcccloud-icdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_CTCCCLOUDCDN_" + + 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_cdn_test.go -args \ + --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_SECRETACCESSKEY="your-secret-access-key" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDCDN_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-cdn/ctcccloud_cdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go index 4582132b..c4eef38a 100644 --- a/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go +++ b/internal/pkg/core/uploader/providers/ctcccloud-cdn/ctcccloud_cdn_test.go @@ -1,4 +1,4 @@ -package ctcccloudcdn +package ctcccloudcdn_test import ( "context" diff --git a/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go new file mode 100644 index 00000000..95f497e1 --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn.go @@ -0,0 +1,171 @@ +package ctcccloudicdn + +import ( + "context" + "fmt" + "log/slog" + "slices" + "strings" + "time" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + ctyunicdn "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/icdn" + 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 *ctyunicdn.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=112&api=10838&data=173&isNormal=1&vid=166 + queryCertListPage := int32(1) + queryCertListPerPage := int32(1000) + for { + select { + case <-ctx.Done(): + return nil, ctx.Err() + default: + } + + queryCertListReq := &ctyunicdn.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 'icdn.QueryCertList'", slog.Any("request", queryCertListReq), slog.Any("response", queryCertListResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'icdn.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=112&api=10837&data=173&isNormal=1&vid=166 + queryCertDetailReq := &ctyunicdn.QueryCertDetailRequest{ + Id: typeutil.ToPtr(certRecord.Id), + } + queryCertDetailResp, err := u.sdkClient.QueryCertDetail(queryCertDetailReq) + u.logger.Debug("sdk request 'icdn.QueryCertDetail'", slog.Any("request", queryCertDetailReq), slog.Any("response", queryCertDetailResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'icdn.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=112&api=10835&data=173&isNormal=1&vid=166 + createCertReq := &ctyunicdn.CreateCertRequest{ + Name: typeutil.ToPtr(certName), + Certs: typeutil.ToPtr(certPEM), + Key: typeutil.ToPtr(privkeyPEM), + } + createCertResp, err := u.sdkClient.CreateCert(createCertReq) + u.logger.Debug("sdk request 'icdn.CreateCert'", slog.Any("request", createCertReq), slog.Any("response", createCertResp)) + if err != nil { + return nil, fmt.Errorf("failed to execute sdk request 'icdn.CreateCert': %w", err) + } + + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", createCertResp.ReturnObj.Id), + CertName: certName, + }, nil +} + +func createSdkClient(accessKeyId, secretAccessKey string) (*ctyunicdn.Client, error) { + return ctyunicdn.NewClient(accessKeyId, secretAccessKey) +} diff --git a/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go new file mode 100644 index 00000000..c7d781f6 --- /dev/null +++ b/internal/pkg/core/uploader/providers/ctcccloud-icdn/ctcccloud_icdn_test.go @@ -0,0 +1,72 @@ +package ctcccloudicdn_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/ctcccloud-icdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fSecretAccessKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_CTCCCLOUDICDN_" + + 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_icdn_test.go -args \ + --CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_CTCCCLOUDICDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDICDN_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_CTCCCLOUDICDN_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/cdn/client.go b/internal/pkg/sdk3rd/ctyun/cdn/client.go index 7a7896a6..3699c721 100644 --- a/internal/pkg/sdk3rd/ctyun/cdn/client.go +++ b/internal/pkg/sdk3rd/ctyun/cdn/client.go @@ -35,6 +35,6 @@ 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) { +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/cdn/types.go b/internal/pkg/sdk3rd/ctyun/cdn/types.go index c6366c56..a4206792 100644 --- a/internal/pkg/sdk3rd/ctyun/cdn/types.go +++ b/internal/pkg/sdk3rd/ctyun/cdn/types.go @@ -1,6 +1,17 @@ package cdn -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 CertRecord struct { Id int32 `json:"id"` Name string `json:"name"` diff --git a/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go b/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go new file mode 100644 index 00000000..ce96bd51 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/api_create_cert.go @@ -0,0 +1,41 @@ +package icdn + +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, "/v1/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/icdn/api_query_cert_detail.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go new file mode 100644 index 00000000..405ecd50 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_detail.go @@ -0,0 +1,51 @@ +package icdn + +import ( + "context" + "net/http" + "strconv" +) + +type QueryCertDetailRequest struct { + Id *int32 `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, "/v1/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/icdn/api_query_cert_list.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go new file mode 100644 index 00000000..a96ab7b1 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_cert_list.go @@ -0,0 +1,55 @@ +package icdn + +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, "/v1/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/icdn/api_query_domain_detail.go b/internal/pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go new file mode 100644 index 00000000..2ecff27e --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/api_query_domain_detail.go @@ -0,0 +1,64 @@ +package icdn + +import ( + "context" + "net/http" +) + +type QueryDomainDetailRequest struct { + Domain *string `json:"domain,omitempty"` + ProductCode *string `json:"product_code,omitempty"` + FunctionNames *string `json:"function_names,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"` + HttpsStatus string `json:"https_status"` + HttpsBasic *struct { + HttpsForce string `json:"https_force"` + HttpForce string `json:"http_force"` + ForceStatus string `json:"force_status"` + OriginProtocol string `json:"origin_protocol"` + } `json:"https_basic,omitempty"` + CertName string `json:"cert_name"` + Ssl string `json:"ssl"` + SslStapling string `json:"ssl_stapling"` + } `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, "/v1/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) + } + if req.FunctionNames != nil { + httpreq.SetQueryParam("function_names", *req.FunctionNames) + } + + 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/icdn/api_update_domain.go b/internal/pkg/sdk3rd/ctyun/icdn/api_update_domain.go new file mode 100644 index 00000000..70d3b73a --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/api_update_domain.go @@ -0,0 +1,37 @@ +package icdn + +import ( + "context" + "net/http" +) + +type UpdateDomainRequest struct { + Domain *string `json:"domain,omitempty"` + HttpsStatus *string `json:"https_status,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, "/v1/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/icdn/client.go b/internal/pkg/sdk3rd/ctyun/icdn/client.go new file mode 100644 index 00000000..0c2248e6 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/client.go @@ -0,0 +1,40 @@ +package icdn + +import ( + "time" + + "github.com/go-resty/resty/v2" + "github.com/usual2970/certimate/internal/pkg/sdk3rd/ctyun/openapi" +) + +const endpoint = "https://icdn-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/icdn/types.go b/internal/pkg/sdk3rd/ctyun/icdn/types.go new file mode 100644 index 00000000..095109e0 --- /dev/null +++ b/internal/pkg/sdk3rd/ctyun/icdn/types.go @@ -0,0 +1,90 @@ +package icdn + +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"` +} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index ba8f04b9..49eb910b 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -49,6 +49,7 @@ import DeployNodeConfigFormBunnyCDNConfig from "./DeployNodeConfigFormBunnyCDNCo import DeployNodeConfigFormBytePlusCDNConfig from "./DeployNodeConfigFormBytePlusCDNConfig"; import DeployNodeConfigFormCdnflyConfig from "./DeployNodeConfigFormCdnflyConfig"; import DeployNodeConfigFormCTCCCloudCDNConfig from "./DeployNodeConfigFormCTCCCloudCDNConfig"; +import DeployNodeConfigFormCTCCCloudICDNConfig from "./DeployNodeConfigFormCTCCCloudICDNConfig"; import DeployNodeConfigFormDogeCloudCDNConfig from "./DeployNodeConfigFormDogeCloudCDNConfig"; import DeployNodeConfigFormEdgioApplicationsConfig from "./DeployNodeConfigFormEdgioApplicationsConfig"; import DeployNodeConfigFormFlexCDNConfig from "./DeployNodeConfigFormFlexCDNConfig"; @@ -268,6 +269,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOYMENT_PROVIDERS.CTCCCLOUD_CDN: return ; + case DEPLOYMENT_PROVIDERS.CTCCCLOUD_ICDN: + return ; case DEPLOYMENT_PROVIDERS.DOGECLOUD_CDN: return ; case DEPLOYMENT_PROVIDERS.EDGIO_APPLICATIONS: diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.tsx new file mode 100644 index 00000000..0e94650b --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormCTCCCloudICDNConfig.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 DeployNodeConfigFormCTCCCloudICDNConfigFieldValues = Nullish<{ + domain: string; +}>; + +export type DeployNodeConfigFormCTCCCloudICDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormCTCCCloudICDNConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormCTCCCloudICDNConfigFieldValues => { + return {}; +}; + +const DeployNodeConfigFormCTCCCloudICDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormCTCCCloudICDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + domain: z + .string({ message: t("workflow_node.deploy.form.ctcccloud_icdn_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 DeployNodeConfigFormCTCCCloudICDNConfig; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 29676de9..84591d7e 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -414,6 +414,7 @@ export const DEPLOYMENT_PROVIDERS = Object.freeze({ CACHEFLY: `${ACCESS_PROVIDERS.CACHEFLY}`, CDNFLY: `${ACCESS_PROVIDERS.CDNFLY}`, CTCCCLOUD_CDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-cdn`, + CTCCCLOUD_ICDN: `${ACCESS_PROVIDERS.CTCCCLOUD}-icdn`, DOGECLOUD_CDN: `${ACCESS_PROVIDERS.DOGECLOUD}-cdn`, EDGIO_APPLICATIONS: `${ACCESS_PROVIDERS.EDGIO}-applications`, FLEXCDN: `${ACCESS_PROVIDERS.FLEXCDN}`, @@ -572,6 +573,7 @@ export const deploymentProvidersMap: Maphttps://cdn-console.ctyun.cn", + "workflow_node.deploy.form.ctcccloud_icdn_domain.label": "CTCC StateCloud ICDN domain", + "workflow_node.deploy.form.ctcccloud_icdn_domain.placeholder": "Please enter CTCC StateCloud ICDN domain name", + "workflow_node.deploy.form.ctcccloud_icdn_domain.tooltip": "For more information, see https://cdn-console.ctyun.cn", "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 b326b3e7..882bd603 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -400,6 +400,9 @@ "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", + "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.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",