diff --git a/go.mod b/go.mod index 7c8723af..196b186b 100644 --- a/go.mod +++ b/go.mod @@ -71,6 +71,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/privatedns/armprivatedns v1.3.0 // indirect github.com/Azure/azure-sdk-for-go/sdk/resourcemanager/resourcegraph/armresourcegraph v0.9.0 // indirect github.com/AzureAD/microsoft-authentication-library-for-go v1.3.3 // indirect + github.com/Edgio/edgio-api v0.0.0-workspace // indirect github.com/alibabacloud-go/alibabacloud-gateway-fc-util v0.0.7 // indirect github.com/alibabacloud-go/openplatform-20191219/v2 v2.0.1 // indirect github.com/alibabacloud-go/tea-fileform v1.1.1 // indirect @@ -211,6 +212,8 @@ require ( modernc.org/sqlite v1.36.1 // indirect ) +replace github.com/Edgio/edgio-api v0.0.0-workspace => ./internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace + replace gitlab.ecloud.com/ecloud/ecloudsdkcore v1.0.0 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkcore@v1.0.0 replace gitlab.ecloud.com/ecloud/ecloudsdkclouddns v1.0.1 => ./internal/pkg/vendors/cmcc-sdk/ecloudsdkclouddns@v1.0.1 diff --git a/internal/deployer/providers.go b/internal/deployer/providers.go index fc3c7a37..c2136d20 100644 --- a/internal/deployer/providers.go +++ b/internal/deployer/providers.go @@ -49,6 +49,7 @@ import ( pLocal "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/local" pQiniuCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-cdn" pQiniuPili "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/qiniu-pili" + pRainYunRCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn" pSafeLine "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/safeline" pSSH "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/ssh" pTencentCloudCDN "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/tencentcloud-cdn" @@ -73,6 +74,7 @@ import ( pVolcEngineImageX "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-imagex" pVolcEngineLive "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-live" pVolcEngineTOS "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/volcengine-tos" + pWangsuCDNPro "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro" pWebhook "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/webhook" "github.com/usual2970/certimate/internal/pkg/utils/maputil" "github.com/usual2970/certimate/internal/pkg/utils/sliceutil" @@ -681,6 +683,27 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } + case domain.DeployProviderTypeRainYunRCDN: + { + access := domain.AccessConfigForRainYun{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + switch options.Provider { + case domain.DeployProviderTypeTencentCloudCDN: + deployer, err := pRainYunRCDN.NewDeployer(&pRainYunRCDN.DeployerConfig{ + ApiKey: access.ApiKey, + InstanceId: maputil.GetInt32(options.ProviderDeployConfig, "instanceId"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + }) + return deployer, err + + default: + break + } + } + case domain.DeployProviderTypeSafeLine: { access := domain.AccessConfigForSafeLine{} @@ -981,6 +1004,30 @@ func createDeployer(options *deployerOptions) (deployer.Deployer, error) { } } + case domain.DeployProviderTypeWangsuCDNPro: + { + access := domain.AccessConfigForWangsu{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + switch options.Provider { + case domain.DeployProviderTypeWangsuCDNPro: + deployer, err := pWangsuCDNPro.NewDeployer(&pWangsuCDNPro.DeployerConfig{ + AccessKeyId: access.AccessKeyId, + AccessKeySecret: access.AccessKeySecret, + Environment: maputil.GetOrDefaultString(options.ProviderDeployConfig, "environment", "production"), + Domain: maputil.GetString(options.ProviderDeployConfig, "domain"), + CertificateId: maputil.GetString(options.ProviderDeployConfig, "certificateId"), + WebhookId: maputil.GetString(options.ProviderDeployConfig, "webhookId"), + }) + return deployer, err + + default: + break + } + } + case domain.DeployProviderTypeWebhook: { access := domain.AccessConfigForWebhook{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 0d3528ab..9e419eaa 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -228,6 +228,11 @@ type AccessConfigForVolcEngine struct { SecretAccessKey string `json:"secretAccessKey"` } +type AccessConfigForWangsu struct { + AccessKeyId string `json:"accessKeyId"` + AccessKeySecret string `json:"accessKeySecret"` +} + type AccessConfigForWebhook struct { Url string `json:"url"` AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index d8726034..668612f7 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -61,6 +61,7 @@ const ( AccessProviderTypeUpyun = AccessProviderType("upyun") AccessProviderTypeVercel = AccessProviderType("vercel") AccessProviderTypeVolcEngine = AccessProviderType("volcengine") + AccessProviderTypeWangsu = AccessProviderType("wangsu") AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWestcn = AccessProviderType("westcn") AccessProviderTypeZeroSSL = AccessProviderType("zerossl") @@ -186,6 +187,7 @@ const ( DeployProviderTypeQiniuCDN = DeployProviderType("qiniu-cdn") DeployProviderTypeQiniuKodo = DeployProviderType("qiniu-kodo") DeployProviderTypeQiniuPili = DeployProviderType("qiniu-pili") + DeployProviderTypeRainYunRCDN = DeployProviderType("rainyun-rcdn") DeployProviderTypeSafeLine = DeployProviderType("safeline") DeployProviderTypeSSH = DeployProviderType("ssh") DeployProviderTypeTencentCloudCDN = DeployProviderType("tencentcloud-cdn") @@ -211,5 +213,6 @@ const ( DeployProviderTypeVolcEngineImageX = DeployProviderType("volcengine-imagex") DeployProviderTypeVolcEngineLive = DeployProviderType("volcengine-live") DeployProviderTypeVolcEngineTOS = DeployProviderType("volcengine-tos") + DeployProviderTypeWangsuCDNPro = DeployProviderType("wangsu-cdnpro") DeployProviderTypeWebhook = DeployProviderType("webhook") ) diff --git a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go index 8ce6c73d..8dea5555 100644 --- a/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go +++ b/internal/pkg/core/deployer/providers/edgio-applications/edgio_applications.go @@ -4,12 +4,12 @@ import ( "context" "log/slog" + edgio "github.com/Edgio/edgio-api/applications/v7" + edgiodtos "github.com/Edgio/edgio-api/applications/v7/dtos" xerrors "github.com/pkg/errors" "github.com/usual2970/certimate/internal/pkg/core/deployer" "github.com/usual2970/certimate/internal/pkg/utils/certutil" - edgsdk "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7" - edgsdkdtos "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) type DeployerConfig struct { @@ -24,7 +24,7 @@ type DeployerConfig struct { type DeployerProvider struct { config *DeployerConfig logger *slog.Logger - sdkClient *edgsdk.EdgioClient + sdkClient *edgio.EdgioClient } var _ deployer.Deployer = (*DeployerProvider)(nil) @@ -64,7 +64,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe // 上传 TLS 证书 // REF: https://docs.edg.io/rest_api/#tag/tls-certs/operation/postConfigV01TlsCerts - uploadTlsCertReq := edgsdkdtos.UploadTlsCertRequest{ + uploadTlsCertReq := edgiodtos.UploadTlsCertRequest{ EnvironmentID: d.config.EnvironmentId, PrimaryCert: privateCertPem, IntermediateCert: intermediateCertPem, @@ -79,7 +79,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe return &deployer.DeployResult{}, nil } -func createSdkClient(clientId, clientSecret string) (*edgsdk.EdgioClient, error) { - client := edgsdk.NewEdgioClient(clientId, clientSecret, "", "") +func createSdkClient(clientId, clientSecret string) (*edgio.EdgioClient, error) { + client := edgio.NewEdgioClient(clientId, clientSecret, "", "") return client, nil } diff --git a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go index a4d1c33e..c0a9e5ba 100644 --- a/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go +++ b/internal/pkg/core/deployer/providers/gcore-cdn/gcore_cdn.go @@ -100,9 +100,15 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe SSlEnabled: true, SSLData: int(updateResourceCertId), ProxySSLEnabled: getResourceResp.ProxySSLEnabled, - ProxySSLCA: &getResourceResp.ProxySSLCA, - ProxySSLData: &getResourceResp.ProxySSLData, - Options: getResourceResp.Options, + } + if getResourceResp.ProxySSLCA != 0 { + updateResourceReq.ProxySSLCA = &getResourceResp.ProxySSLCA + } + if getResourceResp.ProxySSLData != 0 { + updateResourceReq.ProxySSLData = &getResourceResp.ProxySSLData + } + if getResourceResp.Options != nil { + updateResourceReq.Options = getResourceResp.Options } updateResourceResp, err := d.sdkClient.Update(context.TODO(), d.config.ResourceId, updateResourceReq) d.logger.Debug("sdk request 'resources.Update'", slog.Int64("resourceId", d.config.ResourceId), slog.Any("request", updateResourceReq), slog.Any("response", updateResourceResp)) diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go new file mode 100644 index 00000000..d2b56e07 --- /dev/null +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn.go @@ -0,0 +1,102 @@ +package rainyunrcdn + +import ( + "context" + "errors" + "log/slog" + "strconv" + + xerrors "github.com/pkg/errors" + + "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/rainyun-sslcenter" + rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk" +) + +type DeployerConfig struct { + // 雨云 API 密钥。 + ApiKey string `json:"apiKey"` + // RCDN 实例 ID。 + InstanceId int32 `json:"instanceId"` + // 加速域名(支持泛域名)。 + Domain string `json:"domain"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *rainyunsdk.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.ApiKey) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + uploader, err := uploadersp.NewUploader(&uploadersp.UploaderConfig{ + ApiKey: config.ApiKey, + }) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create ssl uploader") + } + + 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.Default() + } else { + d.logger = logger + } + d.sslUploader.WithLogger(logger) + return d +} + +func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPem string) (*deployer.DeployResult, error) { + // 上传证书到 SSL 证书 + upres, err := d.sslUploader.Upload(ctx, certPem, privkeyPem) + if err != nil { + return nil, xerrors.Wrap(err, "failed to upload certificate file") + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + + // RCDN SSL 绑定域名 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120 + certId, _ := strconv.Atoi(upres.CertId) + rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{ + CertId: int32(certId), + Domains: []string{d.config.Domain}, + } + rcdnInstanceSslBindResp, err := d.sdkClient.RcdnInstanceSslBind(d.config.InstanceId, rcdnInstanceSslBindReq) + d.logger.Debug("sdk request 'rcdn.InstanceSslBind'", slog.Any("instanceId", d.config.InstanceId), slog.Any("request", rcdnInstanceSslBindReq), slog.Any("response", rcdnInstanceSslBindResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'rcdn.InstanceSslBind'") + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(apiKey string) (*rainyunsdk.Client, error) { + if apiKey == "" { + return nil, errors.New("invalid rainyun api key") + } + + client := rainyunsdk.NewClient(apiKey) + return client, nil +} diff --git a/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go new file mode 100644 index 00000000..7c3e90f7 --- /dev/null +++ b/internal/pkg/core/deployer/providers/rainyun-rcdn/rainyun_rcdn_test.go @@ -0,0 +1,75 @@ +package rainyunrcdn_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/rainyun-rcdn" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiKey string + fInstanceId int64 + fDomain string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_RAINYUNRCDN_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") + flag.Int64Var(&fInstanceId, argsPrefix+"INSTANCEID", 0, "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") +} + +/* +Shell command to run this test: + + go test -v ./ucloud_ucdn_test.go -args \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_APIKEY="your-api-key" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_INSTANCEID="your-rcdn-instance-id" \ + --CERTIMATE_DEPLOYER_RAINYUNRCDN_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("APIKEY: %v", fApiKey), + fmt.Sprintf("INSTANCEID: %v", fInstanceId), + fmt.Sprintf("DOMAIN: %v", fDomain), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + PrivateKey: fApiKey, + InstanceId: fInstanceId, + 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/deployer/providers/volcengine-alb/volcengine_alb.go b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go index 0c6ba1b4..ceba0952 100644 --- a/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go +++ b/internal/pkg/core/deployer/providers/volcengine-alb/volcengine_alb.go @@ -182,7 +182,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str return errors.New("config `listenerId` is required") } - if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { + if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } diff --git a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go index 37481a3f..752b0f85 100644 --- a/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go +++ b/internal/pkg/core/deployer/providers/volcengine-clb/volcengine_clb.go @@ -178,7 +178,7 @@ func (d *DeployerProvider) deployToListener(ctx context.Context, cloudCertId str return errors.New("config `listenerId` is required") } - if err := d.updateListenerCertificate(ctx, d.config.LoadbalancerId, cloudCertId); err != nil { + if err := d.updateListenerCertificate(ctx, d.config.ListenerId, cloudCertId); err != nil { return err } diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go new file mode 100644 index 00000000..c5ac15b9 --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro.go @@ -0,0 +1,276 @@ +package wangsucdnpro + +import ( + "bytes" + "context" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/sha256" + "encoding/base64" + "encoding/hex" + "errors" + "fmt" + "log/slog" + "regexp" + "time" + + "github.com/alibabacloud-go/tea/tea" + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/deployer" + "github.com/usual2970/certimate/internal/pkg/utils/certutil" + wangsucdn "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/cdn" +) + +type DeployerConfig struct { + // 网宿云 AccessKeyId。 + AccessKeyId string `json:"accessKeyId"` + // 网宿云 AccessKeySecret。 + AccessKeySecret string `json:"accessKeySecret"` + // 网宿云环境。 + Environment string `json:"environment"` + // 加速域名(支持泛域名)。 + Domain string `json:"domain"` + // 证书 ID。 + // 选填。 + CertificateId string `json:"certificateId,omitempty"` + // Webhook ID。 + // 选填。 + WebhookId string `json:"webhookId,omitempty"` +} + +type DeployerProvider struct { + config *DeployerConfig + logger *slog.Logger + sdkClient *wangsucdn.Client +} + +var _ deployer.Deployer = (*DeployerProvider)(nil) + +func NewDeployer(config *DeployerConfig) (*DeployerProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.AccessKeyId, config.AccessKeySecret) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + return &DeployerProvider{ + config: config, + logger: slog.Default(), + sdkClient: client, + }, nil +} + +func (d *DeployerProvider) WithLogger(logger *slog.Logger) deployer.Deployer { + if logger == nil { + d.logger = slog.Default() + } 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") + } + + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 查询已部署加速域名的详情 + getHostnameDetailResp, err := d.sdkClient.GetHostnameDetail(d.config.Domain) + d.logger.Debug("sdk request 'cdn.GetHostnameDetail'", slog.String("hostname", d.config.Domain), slog.Any("response", getHostnameDetailResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetHostnameDetail'") + } + + // 生成网宿云证书参数 + encryptedPrivateKey, err := encryptPrivateKey(privkeyPem, d.config.AccessKeySecret, time.Now().Unix()) + if err != nil { + return nil, xerrors.Wrap(err, "failed to encrypt private key") + } + certificateNewVersionInfo := &wangsucdn.CertificateVersion{ + PrivateKey: tea.String(encryptedPrivateKey), + Certificate: tea.String(certPem), + IdentificationInfo: &wangsucdn.CertificateVersionIdentificationInfo{ + CommonName: tea.String(certX509.Subject.CommonName), + SubjectAlternativeNames: &certX509.DNSNames, + }, + } + + // 网宿云证书 URL 中包含证书 ID 及版本号 + // 格式: + // http://open.chinanetcenter.com/cdn/certificates/5dca2205f9e9cc0001df7b33 + // http://open.chinanetcenter.com/cdn/certificates/329f12c1fe6708c23c31e91f/versions/5 + var wangsuCertUrl string + var wangsuCertId, wangsuCertVer string + + // 如果原证书 ID 为空,则创建证书;否则更新证书。 + timestamp := time.Now().Unix() + if d.config.CertificateId == "" { + // 创建证书 + createCertificateReq := &wangsucdn.CreateCertificateRequest{ + Timestamp: timestamp, + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + AutoRenew: tea.String("Off"), + NewVersion: certificateNewVersionInfo, + } + createCertificateResp, err := d.sdkClient.CreateCertificate(createCertificateReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createCertificateReq), slog.Any("response", createCertificateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateCertificate'") + } + + wangsuCertUrl = createCertificateResp.CertificateUrl + d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) + + wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertIdMatches) > 1 { + wangsuCertId = wangsuCertIdMatches[1] + } + + wangsuCertVer = "1" + } else { + // 更新证书 + updateCertificateReq := &wangsucdn.UpdateCertificateRequest{ + Timestamp: timestamp, + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + AutoRenew: tea.String("Off"), + NewVersion: certificateNewVersionInfo, + } + updateCertificateResp, err := d.sdkClient.UpdateCertificate(d.config.CertificateId, updateCertificateReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("certificateId", d.config.CertificateId), slog.Any("request", updateCertificateReq), slog.Any("response", updateCertificateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.UpdateCertificate'") + } + + wangsuCertUrl = updateCertificateResp.CertificateUrl + d.logger.Info("ssl certificate uploaded", slog.Any("certUrl", wangsuCertUrl)) + + wangsuCertIdMatches := regexp.MustCompile(`/certificates/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertIdMatches) > 1 { + wangsuCertId = wangsuCertIdMatches[1] + } + + wangsuCertVerMatches := regexp.MustCompile(`/versions/(\d+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuCertVerMatches) > 1 { + wangsuCertVer = wangsuCertVerMatches[1] + } + } + + // 创建部署任务 + // REF: https://www.wangsu.com/document/api-doc/27034 + createDeploymentTaskReq := &wangsucdn.CreateDeploymentTaskRequest{ + Name: tea.String(fmt.Sprintf("certimate_%d", time.Now().UnixMilli())), + Target: tea.String(d.config.Environment), + Actions: &[]wangsucdn.DeploymentTaskAction{ + { + Action: tea.String("deploy_cert"), + CertificateId: tea.String(wangsuCertId), + Version: tea.String(wangsuCertVer), + }, + }, + } + if d.config.WebhookId != "" { + createDeploymentTaskReq.Webhook = tea.String(d.config.WebhookId) + } + createDeploymentTaskResp, err := d.sdkClient.CreateDeploymentTask(createDeploymentTaskReq) + d.logger.Debug("sdk request 'cdn.CreateCertificate'", slog.Any("request", createDeploymentTaskReq), slog.Any("response", createDeploymentTaskResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.CreateDeploymentTask'") + } + + // 循环获取部署任务详细信息,等待任务状态变更 + // REF: https://www.wangsu.com/document/api-doc/27038 + var wangsuTaskId string + wangsuTaskMatches := regexp.MustCompile(`/deploymentTasks/([a-zA-Z0-9-]+)`).FindStringSubmatch(wangsuCertUrl) + if len(wangsuTaskMatches) > 1 { + wangsuTaskId = wangsuTaskMatches[1] + } + for { + if ctx.Err() != nil { + return nil, ctx.Err() + } + + getDeploymentTaskDetailResp, err := d.sdkClient.GetDeploymentTaskDetail(wangsuTaskId) + d.logger.Debug("sdk request 'cdn.GetDeploymentTaskDetail'", slog.Any("taskId", wangsuTaskId), slog.Any("response", getDeploymentTaskDetailResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'cdn.GetDeploymentTaskDetail'") + } + + if getDeploymentTaskDetailResp.Status == "failed" { + return nil, errors.New("unexpected deployment task status") + } else if getDeploymentTaskDetailResp.Status == "succeeded" { + break + } + + d.logger.Info("waiting for deployment task completion ...") + time.Sleep(time.Second * 15) + } + + return &deployer.DeployResult{}, nil +} + +func createSdkClient(accessKeyId, accessKeySecret string) (*wangsucdn.Client, error) { + if accessKeyId == "" { + return nil, errors.New("invalid wangsu access key id") + } + + if accessKeySecret == "" { + return nil, errors.New("invalid wangsu access key secret") + } + + return wangsucdn.NewClient(accessKeyId, accessKeySecret), nil +} + +func encryptPrivateKey(privkeyPem string, secretKey string, timestamp int64) (string, error) { + date := time.Unix(timestamp, 0).UTC() + dateStr := date.Format("Mon, 02 Jan 2006 15:04:05 GMT") + + mac := hmac.New(sha256.New, []byte(secretKey)) + mac.Write([]byte(dateStr)) + aesivkey := mac.Sum(nil) + aesivkeyHex := hex.EncodeToString(aesivkey) + + if len(aesivkeyHex) != 64 { + return "", fmt.Errorf("invalid hmac length: %d", len(aesivkeyHex)) + } + ivHex := aesivkeyHex[:32] + keyHex := aesivkeyHex[32:64] + + iv, err := hex.DecodeString(ivHex) + if err != nil { + return "", fmt.Errorf("failed to decode iv: %w", err) + } + + key, err := hex.DecodeString(keyHex) + if err != nil { + return "", fmt.Errorf("failed to decode key: %w", err) + } + + block, err := aes.NewCipher(key) + if err != nil { + return "", err + } + + plainBytes := []byte(privkeyPem) + padlen := aes.BlockSize - len(plainBytes)%aes.BlockSize + if padlen > 0 { + paddata := bytes.Repeat([]byte{byte(padlen)}, padlen) + plainBytes = append(plainBytes, paddata...) + } + + encBytes := make([]byte, len(plainBytes)) + mode := cipher.NewCBCEncrypter(block, iv) + mode.CryptBlocks(encBytes, plainBytes) + + return base64.StdEncoding.EncodeToString(encBytes), nil +} diff --git a/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go new file mode 100644 index 00000000..25dd7b1e --- /dev/null +++ b/internal/pkg/core/deployer/providers/wangsu-cdnpro/wangsu_cdnpro_test.go @@ -0,0 +1,90 @@ +package wangsucdnpro_test + +import ( + "context" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/deployer/providers/wangsu-cdnpro" +) + +var ( + fInputCertPath string + fInputKeyPath string + fAccessKeyId string + fAccessKeySecret string + fEnvironment string + fDomain string + fCertificateId string + fWebhookId string +) + +func init() { + argsPrefix := "CERTIMATE_DEPLOYER_WANGSUCDNPRO_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fAccessKeyId, argsPrefix+"ACCESSKEYID", "", "") + flag.StringVar(&fAccessKeySecret, argsPrefix+"ACCESSKEYSECRET", "", "") + flag.StringVar(&fEnvironment, argsPrefix+"ENVIRONMENT", "production", "") + flag.StringVar(&fDomain, argsPrefix+"DOMAIN", "", "") + flag.StringVar(&fCertificateId, argsPrefix+"CERTIFICATEID", "", "") + flag.StringVar(&fWebhookId, argsPrefix+"WEBHOOKID", "", "") +} + +/* +Shell command to run this test: + + go test -v ./wangsu_cdnpro_test.go -args \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYID="your-access-key-id" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ACCESSKEYSECRET="your-access-key-secret" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_ENVIRONMENT="production" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_DOMAIN="example.com" \ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_CERTIFICATEID="your-certificate-id"\ + --CERTIMATE_DEPLOYER_WANGSUCDNPRO_WEBHOOKID="your-webhook-id" +*/ +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("ACCESSKEYSECRET: %v", fAccessKeySecret), + fmt.Sprintf("ENVIRONMENT: %v", fEnvironment), + fmt.Sprintf("DOMAIN: %v", fDomain), + fmt.Sprintf("CERTIFICATEID: %v", fCertificateId), + fmt.Sprintf("WEBHOOKID: %v", fWebhookId), + }, "\n")) + + deployer, err := provider.NewDeployer(&provider.DeployerConfig{ + AccessKeyId: fAccessKeyId, + AccessKeySecret: fAccessKeySecret, + Environment: fEnvironment, + Domain: fDomain, + CertificateId: fCertificateId, + WebhookId: fWebhookId, + }) + 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/1panel-ssl/1panel_ssl.go b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go index ee00c06a..4eeec679 100644 --- a/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go +++ b/internal/pkg/core/uploader/providers/1panel-ssl/1panel_ssl.go @@ -58,7 +58,7 @@ func (u *UploaderProvider) WithLogger(logger *slog.Logger) uploader.Uploader { func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { // 遍历证书列表,避免重复上传 - if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil { return nil, err } else if res != nil { u.logger.Info("ssl certificate already exists") @@ -82,7 +82,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } // 遍历证书列表,获取刚刚上传证书 ID - if res, err := u.getExistCert(ctx, certPem, privkeyPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem, privkeyPem); err != nil { return nil, err } else if res == nil { return nil, fmt.Errorf("no ssl certificate found, may be upload failed (code: %d, message: %s)", uploadWebsiteSSLResp.GetCode(), uploadWebsiteSSLResp.GetMessage()) @@ -91,7 +91,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe } } -func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { searchWebsiteSSLPageNumber := int32(1) searchWebsiteSSLPageSize := int32(100) for { diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go new file mode 100644 index 00000000..f2ee4bde --- /dev/null +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter.go @@ -0,0 +1,169 @@ +package rainyunsslcenter + +import ( + "context" + "errors" + "fmt" + "log/slog" + "strings" + + xerrors "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/uploader" + "github.com/usual2970/certimate/internal/pkg/utils/certutil" + rainyunsdk "github.com/usual2970/certimate/internal/pkg/vendors/rainyun-sdk" +) + +type UploaderConfig struct { + // 雨云 API 密钥。 + ApiKey string `json:"ApiKey"` +} + +type UploaderProvider struct { + config *UploaderConfig + logger *slog.Logger + sdkClient *rainyunsdk.Client +} + +var _ uploader.Uploader = (*UploaderProvider)(nil) + +func NewUploader(config *UploaderConfig) (*UploaderProvider, error) { + if config == nil { + panic("config is nil") + } + + client, err := createSdkClient(config.ApiKey) + if err != nil { + return nil, xerrors.Wrap(err, "failed to create sdk client") + } + + 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.Default() + } else { + u.logger = logger + } + return u +} + +func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPem string) (res *uploader.UploadResult, err error) { + if res, err := u.getCertIfExists(ctx, certPem); err != nil { + return nil, err + } else if res != nil { + u.logger.Info("ssl certificate already exists") + return res, nil + } + + // SSL 证书上传 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 + sslCenterCreateReq := &rainyunsdk.SslCenterCreateRequest{ + Cert: certPem, + Key: privkeyPem, + } + sslCenterCreateResp, err := u.sdkClient.SslCenterCreate(sslCenterCreateReq) + u.logger.Debug("sdk request 'sslcenter.Create'", slog.Any("request", sslCenterCreateReq), slog.Any("response", sslCenterCreateResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Create'") + } + + if res, err := u.getCertIfExists(ctx, certPem); err != nil { + return nil, err + } else if res == nil { + return nil, errors.New("rainyun sslcenter: no certificate found") + } else { + return res, nil + } +} + +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { + // 解析证书内容 + certX509, err := certutil.ParseCertificateFromPEM(certPem) + if err != nil { + return nil, err + } + + // 遍历 SSL 证书列表,避免重复上传 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943046 + // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-69943048 + sslCenterListPage := int32(1) + sslCenterListPerPage := int32(100) + for { + sslCenterListReq := &rainyunsdk.SslCenterListRequest{ + Filters: &rainyunsdk.SslCenterListFilters{ + Domain: &certX509.Subject.CommonName, + }, + Page: &sslCenterListPage, + PerPage: &sslCenterListPerPage, + } + sslCenterListResp, err := u.sdkClient.SslCenterList(sslCenterListReq) + u.logger.Debug("sdk request 'sslcenter.List'", slog.Any("request", sslCenterListReq), slog.Any("response", sslCenterListResp)) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.List'") + } + + if sslCenterListResp.Data != nil && sslCenterListResp.Data.Records != nil { + for _, sslItem := range sslCenterListResp.Data.Records { + // 先对比证书的多域名 + if sslItem.Domain != strings.Join(certX509.DNSNames, ", ") { + continue + } + + // 再对比证书的有效期 + if sslItem.StartDate != certX509.NotBefore.Unix() || sslItem.ExpireDate != certX509.NotAfter.Unix() { + continue + } + + // 最后对比证书内容 + sslCenterGetResp, err := u.sdkClient.SslCenterGet(sslItem.ID) + if err != nil { + return nil, xerrors.Wrap(err, "failed to execute sdk request 'sslcenter.Get'") + } + + var isSameCert bool + if sslCenterGetResp.Data != nil { + if sslCenterGetResp.Data.Cert == certPem { + isSameCert = true + } else { + oldCertX509, err := certutil.ParseCertificateFromPEM(sslCenterGetResp.Data.Cert) + if err != nil { + continue + } + + isSameCert = certutil.EqualCertificate(certX509, oldCertX509) + } + } + + // 如果已存在相同证书,直接返回 + if isSameCert { + return &uploader.UploadResult{ + CertId: fmt.Sprintf("%d", sslItem.ID), + }, nil + } + } + } + + if sslCenterListResp.Data == nil || len(sslCenterListResp.Data.Records) < int(sslCenterListPerPage) { + break + } else { + sslCenterListPage++ + } + } + + return nil, nil +} + +func createSdkClient(apiKey string) (*rainyunsdk.Client, error) { + if apiKey == "" { + return nil, errors.New("invalid rainyun api key") + } + + client := rainyunsdk.NewClient(apiKey) + return client, nil +} diff --git a/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go new file mode 100644 index 00000000..41619401 --- /dev/null +++ b/internal/pkg/core/uploader/providers/rainyun-sslcenter/rainyun_sslcenter_test.go @@ -0,0 +1,67 @@ +package rainyunsslcenter_test + +import ( + "context" + "encoding/json" + "flag" + "fmt" + "os" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/uploader/providers/rainyun-sslcenter" +) + +var ( + fInputCertPath string + fInputKeyPath string + fApiKey string +) + +func init() { + argsPrefix := "CERTIMATE_UPLOADER_RAINYUNSSLCENTER_" + + flag.StringVar(&fInputCertPath, argsPrefix+"INPUTCERTPATH", "", "") + flag.StringVar(&fInputKeyPath, argsPrefix+"INPUTKEYPATH", "", "") + flag.StringVar(&fApiKey, argsPrefix+"APIKEY", "", "") +} + +/* +Shell command to run this test: + + go test -v ./rainyun_sslcenter_test.go -args \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTCERTPATH="/path/to/your-input-cert.pem" \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_INPUTKEYPATH="/path/to/your-input-key.pem" \ + --CERTIMATE_UPLOADER_RAINYUNSSLCENTER_APIKEY="your-api-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("APIKEY: %v", fApiKey), + }, "\n")) + + uploader, err := provider.NewUploader(&provider.UploaderConfig{ + ApiKey: fApiKey, + }) + 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/core/uploader/providers/ucloud-ussl/ucloud_ussl.go b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go index b8639bf3..4649c454 100644 --- a/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go +++ b/internal/pkg/core/uploader/providers/ucloud-ussl/ucloud_ussl.go @@ -89,10 +89,10 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe u.logger.Debug("sdk request 'ussl.UploadNormalCertificate'", slog.Any("request", uploadNormalCertificateReq), slog.Any("response", uploadNormalCertificateResp)) if err != nil { if uploadNormalCertificateResp != nil && uploadNormalCertificateResp.GetRetCode() == 80035 { - if res, err := u.getExistCert(ctx, certPem); err != nil { + if res, err := u.getCertIfExists(ctx, certPem); err != nil { return nil, err } else if res == nil { - return nil, errors.New("no certificate found") + return nil, errors.New("ucloud ssl: no certificate found") } else { u.logger.Info("ssl certificate already exists") return res, nil @@ -112,7 +112,7 @@ func (u *UploaderProvider) Upload(ctx context.Context, certPem string, privkeyPe }, nil } -func (u *UploaderProvider) getExistCert(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { +func (u *UploaderProvider) getCertIfExists(ctx context.Context, certPem string) (res *uploader.UploadResult, err error) { // 解析证书内容 certX509, err := certutil.ParseCertificateFromPEM(certPem) if err != nil { diff --git a/internal/pkg/utils/maputil/getter.go b/internal/pkg/utils/maputil/getter.go index 9ba22875..c1126496 100644 --- a/internal/pkg/utils/maputil/getter.go +++ b/internal/pkg/utils/maputil/getter.go @@ -74,6 +74,18 @@ func GetOrDefaultInt32(dict map[string]any, key string, defaultValue int32) int3 } } + if result, ok := value.(int64); ok { + if result != 0 { + return int32(result) + } + } + + if result, ok := value.(int); ok { + if result != 0 { + return int32(result) + } + } + // 兼容字符串类型的值 if str, ok := value.(string); ok { if result, err := strconv.ParseInt(str, 10, 32); err == nil { @@ -126,6 +138,12 @@ func GetOrDefaultInt64(dict map[string]any, key string, defaultValue int64) int6 } } + if result, ok := value.(int); ok { + if result != 0 { + return int64(result) + } + } + // 兼容字符串类型的值 if str, ok := value.(string); ok { if result, err := strconv.ParseInt(str, 10, 64); err == nil { diff --git a/internal/pkg/vendors/1panel-sdk/client.go b/internal/pkg/vendors/1panel-sdk/client.go index 653e3a69..02dc8f58 100644 --- a/internal/pkg/vendors/1panel-sdk/client.go +++ b/internal/pkg/vendors/1panel-sdk/client.go @@ -79,7 +79,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("1panel api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("1panel api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("1panel api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/baishan-sdk/client.go b/internal/pkg/vendors/baishan-sdk/client.go index 400e1ae1..ad906cbe 100644 --- a/internal/pkg/vendors/baishan-sdk/client.go +++ b/internal/pkg/vendors/baishan-sdk/client.go @@ -75,7 +75,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("baishan api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("baishan api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("baishan api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/btpanel-sdk/client.go b/internal/pkg/vendors/btpanel-sdk/client.go index 8fb4ad32..1e48f734 100644 --- a/internal/pkg/vendors/btpanel-sdk/client.go +++ b/internal/pkg/vendors/btpanel-sdk/client.go @@ -86,7 +86,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("baota api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("baota api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("baota api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/cachefly-sdk/client.go b/internal/pkg/vendors/cachefly-sdk/client.go index 0b11f6d2..a460ae96 100644 --- a/internal/pkg/vendors/cachefly-sdk/client.go +++ b/internal/pkg/vendors/cachefly-sdk/client.go @@ -59,7 +59,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("cachefly api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("cachefly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/cdnfly-sdk/api.go b/internal/pkg/vendors/cdnfly-sdk/api.go index 2387f6d8..263bf2cd 100644 --- a/internal/pkg/vendors/cdnfly-sdk/api.go +++ b/internal/pkg/vendors/cdnfly-sdk/api.go @@ -3,17 +3,18 @@ package cdnflysdk import ( "fmt" "net/http" + "net/url" ) func (c *Client) GetSite(req *GetSiteRequest) (*GetSiteResponse, error) { resp := &GetSiteResponse{} - err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp) return resp, err } func (c *Client) UpdateSite(req *UpdateSiteRequest) (*UpdateSiteResponse, error) { resp := &UpdateSiteResponse{} - err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/sites/%s", url.PathEscape(req.Id)), req, resp) return resp, err } @@ -25,6 +26,6 @@ func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertif func (c *Client) UpdateCertificate(req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { resp := &UpdateCertificateResponse{} - err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", req.Id), req, resp) + err := c.sendRequestWithResult(http.MethodPut, fmt.Sprintf("/v1/certs/%s", url.PathEscape(req.Id)), req, resp) return resp, err } diff --git a/internal/pkg/vendors/cdnfly-sdk/client.go b/internal/pkg/vendors/cdnfly-sdk/client.go index b43a04db..47738f29 100644 --- a/internal/pkg/vendors/cdnfly-sdk/client.go +++ b/internal/pkg/vendors/cdnfly-sdk/client.go @@ -65,7 +65,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("cdnfly api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("cdnfly api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/dnsla-sdk/client.go b/internal/pkg/vendors/dnsla-sdk/client.go index 72b0ed3d..d557635b 100644 --- a/internal/pkg/vendors/dnsla-sdk/client.go +++ b/internal/pkg/vendors/dnsla-sdk/client.go @@ -60,7 +60,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("dnsla api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("dnsla api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/edgio-sdk/applications/README.md b/internal/pkg/vendors/edgio-sdk/README.md similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/README.md rename to internal/pkg/vendors/edgio-sdk/README.md diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/cdn_configuration.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/cdn_configuration.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/cdn_configuration.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/environment.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/environment.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/environment.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/property.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/property.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/property.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/purge.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/purge.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/purge.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/tls_cert.go similarity index 100% rename from internal/pkg/vendors/edgio-sdk/applications/v7/dtos/tls_cert.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/dtos/tls_cert.go diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go similarity index 99% rename from internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go index a03436fc..fb7b7cf7 100644 --- a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client.go +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client.go @@ -6,9 +6,8 @@ import ( "fmt" "time" + "github.com/Edgio/edgio-api/applications/v7/dtos" "github.com/go-resty/resty/v2" - - "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" ) // AccessTokenResponse represents the response from the token endpoint. diff --git a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go similarity index 94% rename from internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go rename to internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go index ea5fa958..645d73aa 100644 --- a/internal/pkg/vendors/edgio-sdk/applications/v7/edgio_client_interface.go +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/applications/v7/edgio_client_interface.go @@ -3,7 +3,7 @@ package edgio_api import ( "context" - "github.com/usual2970/certimate/internal/pkg/vendors/edgio-sdk/applications/v7/dtos" + "github.com/Edgio/edgio-api/applications/v7/dtos" ) type EdgioClientInterface interface { diff --git a/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod new file mode 100644 index 00000000..2c127948 --- /dev/null +++ b/internal/pkg/vendors/edgio-sdk/edgio-api@v0.0.0-workspace/go.mod @@ -0,0 +1,3 @@ +module github.com/Edgio/edgio-api + +go 1.23.0 diff --git a/internal/pkg/vendors/gname-sdk/client.go b/internal/pkg/vendors/gname-sdk/client.go index 0a2238f2..017a3315 100644 --- a/internal/pkg/vendors/gname-sdk/client.go +++ b/internal/pkg/vendors/gname-sdk/client.go @@ -82,7 +82,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("gname api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("gname api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("gname api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/rainyun-sdk/api.go b/internal/pkg/vendors/rainyun-sdk/api.go new file mode 100644 index 00000000..50f3279d --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/api.go @@ -0,0 +1,30 @@ +package rainyunsdk + +import ( + "fmt" + "net/http" +) + +func (c *Client) SslCenterList(req *SslCenterListRequest) (*SslCenterListResponse, error) { + resp := &SslCenterListResponse{} + err := c.sendRequestWithResult(http.MethodGet, "/product/sslcenter", req, resp) + return resp, err +} + +func (c *Client) SslCenterGet(id int32) (*SslCenterGetResponse, error) { + resp := &SslCenterGetResponse{} + err := c.sendRequestWithResult(http.MethodGet, fmt.Sprintf("/product/sslcenter/%d", id), nil, resp) + return resp, err +} + +func (c *Client) SslCenterCreate(req *SslCenterCreateRequest) (*SslCenterCreateResponse, error) { + resp := &SslCenterCreateResponse{} + err := c.sendRequestWithResult(http.MethodPost, "/product/sslcenter/", req, resp) + return resp, err +} + +func (c *Client) RcdnInstanceSslBind(id int32, req *RcdnInstanceSslBindRequest) (*RcdnInstanceSslBindResponse, error) { + resp := &RcdnInstanceSslBindResponse{} + err := c.sendRequestWithResult(http.MethodPost, fmt.Sprintf("/product/rcdn/instance/%d/ssl_bind", id), req, resp) + return resp, err +} diff --git a/internal/pkg/vendors/rainyun-sdk/client.go b/internal/pkg/vendors/rainyun-sdk/client.go new file mode 100644 index 00000000..e710128b --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/client.go @@ -0,0 +1,74 @@ +package rainyunsdk + +import ( + "encoding/json" + "fmt" + "net/http" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + apiKey string + + client *resty.Client +} + +func NewClient(apiKey string) *Client { + client := resty.New() + + return &Client{ + apiKey: apiKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}) (*resty.Response, error) { + req := c.client.R().SetHeader("x-api-key", c.apiKey) + req.Method = method + req.URL = "https://api.v2.rainyun.com" + path + if strings.EqualFold(method, http.MethodGet) { + if params != nil { + jsonb, _ := json.Marshal(params) + req = req.SetQueryParam("options", string(jsonb)) + } + } else { + req = req. + SetHeader("Content-Type", "application/json"). + SetBody(params) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("rainyun api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("rainyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) sendRequestWithResult(method string, path string, params interface{}, result BaseResponse) error { + resp, err := c.sendRequest(method, path, params) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + } + return err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return fmt.Errorf("rainyun api error: failed to parse response: %w", err) + } else if errcode := result.GetCode(); errcode/100 != 2 { + return fmt.Errorf("rainyun api error: %d - %s", errcode, result.GetMessage()) + } + + return nil +} diff --git a/internal/pkg/vendors/rainyun-sdk/models.go b/internal/pkg/vendors/rainyun-sdk/models.go new file mode 100644 index 00000000..bcc44963 --- /dev/null +++ b/internal/pkg/vendors/rainyun-sdk/models.go @@ -0,0 +1,83 @@ +package rainyunsdk + +type BaseResponse interface { + GetCode() int32 + GetMessage() string +} + +type baseResponse struct { + Code *int32 `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +func (r *baseResponse) GetCode() int32 { + if r.Code != nil { + return *r.Code + } + return 0 +} + +func (r *baseResponse) GetMessage() string { + if r.Message != nil { + return *r.Message + } + return "" +} + +type SslCenterListFilters struct { + Domain *string `json:"Domain,omitempty"` +} + +type SslCenterListRequest struct { + Filters *SslCenterListFilters `json:"columnFilters,omitempty"` + Sort []*string `json:"sort,omitempty"` + Page *int32 `json:"page,omitempty"` + PerPage *int32 `json:"perPage,omitempty"` +} + +type SslCenterListResponse struct { + baseResponse + Data *struct { + TotalRecords int32 `json:"TotalRecords"` + Records []*struct { + ID int32 `json:"ID"` + UID int32 `json:"UID"` + Domain string `json:"Domain"` + Issuer string `json:"Issuer"` + StartDate int64 `json:"StartDate"` + ExpireDate int64 `json:"ExpDate"` + UploadTime int64 `json:"UploadTime"` + } `json:"Records"` + } `json:"data,omitempty"` +} + +type SslCenterGetResponse struct { + baseResponse + Data *struct { + Cert string `json:"Cert"` + Key string `json:"Key"` + Domain string `json:"DomainName"` + Issuer string `json:"Issuer"` + StartDate int64 `json:"StartDate"` + ExpireDate int64 `json:"ExpDate"` + RemainDays int32 `json:"RemainDays"` + } `json:"data,omitempty"` +} + +type SslCenterCreateRequest struct { + Cert string `json:"cert"` + Key string `json:"key"` +} + +type SslCenterCreateResponse struct { + baseResponse +} + +type RcdnInstanceSslBindRequest struct { + CertId int32 `json:"cert_id"` + Domains []string `json:"domains"` +} + +type RcdnInstanceSslBindResponse struct { + baseResponse +} diff --git a/internal/pkg/vendors/safeline-sdk/client.go b/internal/pkg/vendors/safeline-sdk/client.go index ade8acfa..c56e3485 100644 --- a/internal/pkg/vendors/safeline-sdk/client.go +++ b/internal/pkg/vendors/safeline-sdk/client.go @@ -47,7 +47,7 @@ func (c *Client) sendRequest(path string, params interface{}) (*resty.Response, if err != nil { return resp, fmt.Errorf("safeline api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("safeline api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("safeline api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/upyun-sdk/console/client.go b/internal/pkg/vendors/upyun-sdk/console/client.go index 7b968f53..cf431c2c 100644 --- a/internal/pkg/vendors/upyun-sdk/console/client.go +++ b/internal/pkg/vendors/upyun-sdk/console/client.go @@ -64,7 +64,7 @@ func (c *Client) sendRequest(method string, path string, params interface{}) (*r if err != nil { return resp, fmt.Errorf("upyun api error: failed to send request: %w", err) } else if resp.IsError() { - return resp, fmt.Errorf("upyun api error: unexpected status code: %d, %s", resp.StatusCode(), resp.Body()) + return resp, fmt.Errorf("upyun api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) } return resp, nil diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/api.go b/internal/pkg/vendors/wangsu-sdk/cdn/api.go new file mode 100644 index 00000000..fd96ba2f --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/api.go @@ -0,0 +1,58 @@ +package cdn + +import ( + "fmt" + "net/http" + "net/url" + + "github.com/go-resty/resty/v2" +) + +func (c *Client) CreateCertificate(req *CreateCertificateRequest) (*CreateCertificateResponse, error) { + resp := &CreateCertificateResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/certificates", req, resp, func(r *resty.Request) { + r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + }) + if err != nil { + return resp, err + } + + resp.CertificateUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) UpdateCertificate(certificateId string, req *UpdateCertificateRequest) (*UpdateCertificateResponse, error) { + resp := &UpdateCertificateResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPatch, fmt.Sprintf("/cdn/certificates/%s", url.PathEscape(certificateId)), req, resp, func(r *resty.Request) { + r.SetHeader("x-cnc-timestamp", fmt.Sprintf("%d", req.Timestamp)) + }) + if err != nil { + return resp, err + } + + resp.CertificateUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) GetHostnameDetail(hostname string) (*GetHostnameDetailResponse, error) { + resp := &GetHostnameDetailResponse{} + _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/hostnames/%s", url.PathEscape(hostname)), nil, resp) + return resp, err +} + +func (c *Client) CreateDeploymentTask(req *CreateDeploymentTaskRequest) (*CreateDeploymentTaskResponse, error) { + resp := &CreateDeploymentTaskResponse{} + r, err := c.client.SendRequestWithResult(http.MethodPost, "/cdn/deploymentTasks", req, resp) + if err != nil { + return resp, err + } + + resp.DeploymentTaskUrl = r.Header().Get("Location") + return resp, err +} + +func (c *Client) GetDeploymentTaskDetail(deploymentTaskId string) (*GetDeploymentTaskDetailResponse, error) { + resp := &GetDeploymentTaskDetailResponse{} + _, err := c.client.SendRequestWithResult(http.MethodGet, fmt.Sprintf("/cdn/deploymentTasks/%s", url.PathEscape(hostname)), nil, resp) + return resp, err +} diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/client.go b/internal/pkg/vendors/wangsu-sdk/cdn/client.go new file mode 100644 index 00000000..e1831960 --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/client.go @@ -0,0 +1,20 @@ +package cdn + +import ( + "time" + + "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi" +) + +type Client struct { + client *openapi.Client +} + +func NewClient(accessKey, secretKey string) *Client { + return &Client{client: openapi.NewClient(accessKey, secretKey)} +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.WithTimeout(timeout) + return c +} diff --git a/internal/pkg/vendors/wangsu-sdk/cdn/models.go b/internal/pkg/vendors/wangsu-sdk/cdn/models.go new file mode 100644 index 00000000..0126418a --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/cdn/models.go @@ -0,0 +1,107 @@ +package cdn + +import ( + "github.com/usual2970/certimate/internal/pkg/vendors/wangsu-sdk/openapi" +) + +type baseResponse struct { + RequestId *string `json:"-"` + Code *string `json:"code,omitempty"` + Message *string `json:"message,omitempty"` +} + +var _ openapi.Result = (*baseResponse)(nil) + +func (r *baseResponse) SetRequestId(requestId string) { + r.RequestId = &requestId +} + +type CertificateVersion struct { + Comments *string `json:"comments,omitempty"` + PrivateKey *string `json:"privateKey,omitempty"` + Certificate *string `json:"certificate,omitempty"` + ChainCert *string `json:"chainCert,omitempty"` + IdentificationInfo *CertificateVersionIdentificationInfo `json:"identificationInfo,omitempty"` +} + +type CertificateVersionIdentificationInfo struct { + Country *string `json:"country,omitempty"` + State *string `json:"state,omitempty"` + City *string `json:"city,omitempty"` + Company *string `json:"company,omitempty"` + Department *string `json:"department,omitempty"` + CommonName *string `json:"commonName,omitempty" required:"true"` + Email *string `json:"email,omitempty"` + SubjectAlternativeNames *[]string `json:"subjectAlternativeNames,omitempty" required:"true"` +} + +type CreateCertificateRequest struct { + Timestamp int64 `json:"-"` + Name *string `json:"name,omitempty" required:"true"` + Description *string `json:"description,omitempty"` + AutoRenew *string `json:"autoRenew,omitempty"` + ForceRenew *bool `json:"forceRenew,omitempty"` + NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"` +} + +type CreateCertificateResponse struct { + baseResponse + CertificateUrl string `json:"-"` +} + +type UpdateCertificateRequest struct { + Timestamp int64 `json:"-"` + Name *string `json:"name,omitempty"` + Description *string `json:"description,omitempty"` + AutoRenew *string `json:"autoRenew,omitempty"` + ForceRenew *bool `json:"forceRenew,omitempty"` + NewVersion *CertificateVersion `json:"newVersion,omitempty" required:"true"` +} + +type UpdateCertificateResponse struct { + baseResponse + CertificateUrl string `json:"-"` +} + +type HostnameProperty struct { + PropertyId string `json:"propertyId"` + Version int32 `json:"version"` + CertificateId *string `json:"certificateId,omitempty"` +} + +type GetHostnameDetailResponse struct { + baseResponse + Hostname string `json:"hostname"` + PropertyInProduction *HostnameProperty `json:"propertyInProduction,omitempty"` + PropertyInStaging *HostnameProperty `json:"propertyInStaging,omitempty"` +} + +type DeploymentTaskAction struct { + Action *string `json:"action,omitempty" required:"true"` + PropertyId *string `json:"propertyId,omitempty"` + CertificateId *string `json:"certificateId,omitempty"` + Version *string `json:"version,omitempty"` +} + +type CreateDeploymentTaskRequest struct { + Name *string `json:"name,omitempty"` + Target *string `json:"target,omitempty" required:"true"` + Actions *[]DeploymentTaskAction `json:"actions,omitempty" required:"true"` + Webhook *string `json:"webhook,omitempty"` +} + +type CreateDeploymentTaskResponse struct { + baseResponse + DeploymentTaskUrl string `json:"-"` +} + +type GetDeploymentTaskDetailResponse struct { + baseResponse + Target string `json:"target"` + Actions []DeploymentTaskAction `json:"actions"` + Status string `json:"status"` + StatusDetails string `json:"statusDetails"` + SubmissionTime string `json:"submissionTime"` + FinishTime string `json:"finishTime"` + ApiRequestId string `json:"apiRequestId"` +} diff --git a/internal/pkg/vendors/wangsu-sdk/openapi/client.go b/internal/pkg/vendors/wangsu-sdk/openapi/client.go new file mode 100644 index 00000000..6492aba8 --- /dev/null +++ b/internal/pkg/vendors/wangsu-sdk/openapi/client.go @@ -0,0 +1,187 @@ +package openapi + +import ( + "crypto/hmac" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/go-resty/resty/v2" +) + +type Client struct { + accessKey string + secretKey string + + client *resty.Client +} + +type Result interface { + SetRequestId(requestId string) +} + +func NewClient(accessKey, secretKey string) *Client { + client := resty.New(). + SetBaseURL("https://open.chinanetcenter.com"). + SetHeader("Host", "open.chinanetcenter.com"). + SetHeader("Accept", "application/json"). + SetHeader("Content-Type", "application/json"). + SetPreRequestHook(func(c *resty.Client, req *http.Request) error { + // Step 1: Get request method + method := req.Method + method = strings.ToUpper(method) + + // Step 2: Get request path + path := "/" + if req.URL != nil { + path = req.URL.Path + } + + // Step 3: Get unencoded query string + queryString := "" + if method != http.MethodPost && req.URL != nil { + queryString = req.URL.RawQuery + + s, err := url.QueryUnescape(queryString) + if err != nil { + return err + } + + queryString = s + } + + // Step 4: Get canonical headers & signed headers + canonicalHeaders := "" + + "content-type:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Content-Type"))) + "\n" + + "host:" + strings.TrimSpace(strings.ToLower(req.Header.Get("Host"))) + "\n" + signedHeaders := "content-type;host" + + // Step 5: Get request payload + payload := "" + if method != http.MethodGet && req.Body != nil { + reader, err := req.GetBody() + if err != nil { + return err + } + + defer reader.Close() + + payloadb, err := io.ReadAll(reader) + if err != nil { + return err + } + + payload = string(payloadb) + } + hashedPayload := sha256.Sum256([]byte(payload)) + hashedPayloadHex := strings.ToLower(hex.EncodeToString(hashedPayload[:])) + + // Step 6: Get timestamp + var reqtime time.Time + timestampString := req.Header.Get("x-cnc-timestamp") + if timestampString == "" { + reqtime = time.Now().UTC() + timestampString = fmt.Sprintf("%d", reqtime.Unix()) + } else { + timestamp, err := strconv.ParseInt(timestampString, 10, 64) + if err != nil { + return err + } + reqtime = time.Unix(timestamp, 0).UTC() + } + + // Step 7: Get canonical request string + canonicalRequest := fmt.Sprintf("%s\n%s\n%s\n%s\n%s\n%s", method, path, queryString, canonicalHeaders, signedHeaders, hashedPayloadHex) + hashedCanonicalRequest := sha256.Sum256([]byte(canonicalRequest)) + hashedCanonicalRequestHex := strings.ToLower(hex.EncodeToString(hashedCanonicalRequest[:])) + + // Step 8: String to sign + const SignAlgorithmHeader = "CNC-HMAC-SHA256" + stringToSign := fmt.Sprintf("%s\n%s\n%s", SignAlgorithmHeader, timestampString, hashedCanonicalRequestHex) + hmac := hmac.New(sha256.New, []byte(secretKey)) + hmac.Write([]byte(stringToSign)) + sign := hmac.Sum(nil) + signHex := strings.ToLower(hex.EncodeToString(sign)) + + // Step 9: Add headers to request + req.Header.Set("x-cnc-accessKey", accessKey) + req.Header.Set("x-cnc-timestamp", timestampString) + req.Header.Set("x-cnc-auth-method", "AKSK") + req.Header.Set("Authorization", fmt.Sprintf("%s Credential=%s, SignedHeaders=%s, Signature=%s", SignAlgorithmHeader, accessKey, signedHeaders, signHex)) + req.Header.Set("Date", reqtime.Format("Mon, 02 Jan 2006 15:04:05 GMT")) + + return nil + }) + + return &Client{ + accessKey: accessKey, + secretKey: secretKey, + client: client, + } +} + +func (c *Client) WithTimeout(timeout time.Duration) *Client { + c.client.SetTimeout(timeout) + return c +} + +func (c *Client) sendRequest(method string, path string, params interface{}, configureReq ...func(req *resty.Request)) (*resty.Response, error) { + req := c.client.R() + req.Method = method + req.URL = path + if strings.EqualFold(method, http.MethodGet) { + qs := make(map[string]string) + if params != nil { + temp := make(map[string]any) + jsonb, _ := json.Marshal(params) + json.Unmarshal(jsonb, &temp) + for k, v := range temp { + if v != nil { + qs[k] = fmt.Sprintf("%v", v) + } + } + } + + req = req.SetQueryParams(qs) + } else { + req = req.SetBody(params) + } + + for _, fn := range configureReq { + fn(req) + } + + resp, err := req.Send() + if err != nil { + return resp, fmt.Errorf("wangsu api error: failed to send request: %w", err) + } else if resp.IsError() { + return resp, fmt.Errorf("wangsu api error: unexpected status code: %d, resp: %s", resp.StatusCode(), resp.Body()) + } + + return resp, nil +} + +func (c *Client) SendRequestWithResult(method string, path string, params interface{}, result Result, configureReq ...func(req *resty.Request)) (*resty.Response, error) { + resp, err := c.sendRequest(method, path, params, configureReq...) + if err != nil { + if resp != nil { + json.Unmarshal(resp.Body(), &result) + result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + } + return resp, err + } + + if err := json.Unmarshal(resp.Body(), &result); err != nil { + return resp, fmt.Errorf("wangsu api error: failed to parse response: %w", err) + } + + result.SetRequestId(resp.Header().Get("x-cnc-request-id")) + return resp, nil +} diff --git a/migrations/1742209200_upgrade.go b/migrations/1742209200_upgrade.go index d2ed7f9d..5cda0c35 100644 --- a/migrations/1742209200_upgrade.go +++ b/migrations/1742209200_upgrade.go @@ -258,15 +258,15 @@ func init() { } type dWorkflowNode struct { - Id string `json:"id"` - Type string `json:"type"` - Name string `json:"name"` - Config map[string]any `json:"config"` - Inputs map[string]any `json:"inputs"` - Outputs map[string]any `json:"outputs"` - Next *dWorkflowNode `json:"next,omitempty"` - Branches []dWorkflowNode `json:"branches,omitempty"` - Validated bool `json:"validated"` + Id string `json:"id"` + Type string `json:"type"` + Name string `json:"name"` + Config map[string]any `json:"config"` + Inputs []map[string]any `json:"inputs"` + Outputs []map[string]any `json:"outputs"` + Next *dWorkflowNode `json:"next,omitempty"` + Branches []dWorkflowNode `json:"branches,omitempty"` + Validated bool `json:"validated"` } for _, workflowRun := range workflowRuns { diff --git a/migrations/1744192800_upgrade.go b/migrations/1744192800_upgrade.go new file mode 100644 index 00000000..83e83ee6 --- /dev/null +++ b/migrations/1744192800_upgrade.go @@ -0,0 +1,91 @@ +package migrations + +import ( + "github.com/pocketbase/pocketbase/core" + m "github.com/pocketbase/pocketbase/migrations" +) + +func init() { + m.Register(func(app core.App) error { + collection, err := app.FindCollectionByNameOrId("4yzbv8urny5ja1e") + if err != nil { + return err + } + + // update field + if err := collection.Fields.AddMarshaledJSONAt(2, []byte(`{ + "hidden": false, + "id": "hwy7m03o", + "maxSelect": 1, + "name": "provider", + "presentable": false, + "required": false, + "system": false, + "type": "select", + "values": [ + "1panel", + "acmehttpreq", + "akamai", + "aliyun", + "aws", + "azure", + "baiducloud", + "baishan", + "baotapanel", + "byteplus", + "buypass", + "cachefly", + "cdnfly", + "cloudflare", + "cloudns", + "cmcccloud", + "ctcccloud", + "cucccloud", + "desec", + "dnsla", + "dogecloud", + "dynv6", + "edgio", + "fastly", + "gname", + "gcore", + "godaddy", + "goedge", + "googletrustservices", + "huaweicloud", + "jdcloud", + "k8s", + "letsencrypt", + "letsencryptstaging", + "local", + "namecheap", + "namedotcom", + "namesilo", + "ns1", + "porkbun", + "powerdns", + "qiniu", + "qingcloud", + "rainyun", + "safeline", + "ssh", + "sslcom", + "tencentcloud", + "ucloud", + "upyun", + "vercel", + "volcengine", + "wangsu", + "webhook", + "westcn", + "zerossl" + ] + }`)); err != nil { + return err + } + + return app.Save(collection) + }, func(app core.App) error { + return nil + }) +} diff --git a/ui/public/imgs/providers/wangsu.svg b/ui/public/imgs/providers/wangsu.svg new file mode 100644 index 00000000..276ec1cc --- /dev/null +++ b/ui/public/imgs/providers/wangsu.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index d5906434..bffb1f49 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -51,6 +51,7 @@ import AccessFormUCloudConfig from "./AccessFormUCloudConfig"; import AccessFormUpyunConfig from "./AccessFormUpyunConfig"; import AccessFormVercelConfig from "./AccessFormVercelConfig"; import AccessFormVolcEngineConfig from "./AccessFormVolcEngineConfig"; +import AccessFormWangsuConfig from "./AccessFormWangsuConfig"; import AccessFormWebhookConfig from "./AccessFormWebhookConfig"; import AccessFormWestcnConfig from "./AccessFormWestcnConfig"; import AccessFormZeroSSLConfig from "./AccessFormZeroSSLConfig"; @@ -229,6 +230,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.VOLCENGINE: return ; + case ACCESS_PROVIDERS.WANGSU: + return ; case ACCESS_PROVIDERS.WEBHOOK: return ; case ACCESS_PROVIDERS.WESTCN: diff --git a/ui/src/components/access/AccessFormWangsuConfig.tsx b/ui/src/components/access/AccessFormWangsuConfig.tsx new file mode 100644 index 00000000..f9676829 --- /dev/null +++ b/ui/src/components/access/AccessFormWangsuConfig.tsx @@ -0,0 +1,76 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForWangsu } from "@/domain/access"; + +type AccessFormWangsuConfigFieldValues = Nullish; + +export type AccessFormWangsuConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormWangsuConfigFieldValues; + onValuesChange?: (values: AccessFormWangsuConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormWangsuConfigFieldValues => { + return { + accessKeyId: "", + accessKeySecret: "", + }; +}; + +const AccessFormWangsuConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange: onValuesChange }: AccessFormWangsuConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessKeyId: z + .string() + .min(1, t("access.form.wangsu_access_key_id.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + accessKeySecret: z + .string() + .min(1, t("access.form.wangsu_access_key_secret.placeholder")) + .max(64, t("common.errmsg.string_max", { max: 64 })) + .trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + + + } + > + + +
+ ); +}; + +export default AccessFormWangsuConfig; diff --git a/ui/src/components/provider/AccessProviderSelect.tsx b/ui/src/components/provider/AccessProviderSelect.tsx index fbb4099f..79f1539e 100644 --- a/ui/src/components/provider/AccessProviderSelect.tsx +++ b/ui/src/components/provider/AccessProviderSelect.tsx @@ -50,26 +50,27 @@ const AccessProviderSelect = ({ filter, showOptionTags, ...props }: AccessProvid
- + {t(provider.name)} - {showOptionTags && ( -
- - {t("access.props.provider.usage.dns")} - - - {t("access.props.provider.usage.hosting")} - - - {t("access.props.provider.usage.ca")} - - - {t("access.props.provider.usage.notification")} - -
- )} +
+ + {t("access.props.provider.builtin")} + + + {t("access.props.provider.usage.dns")} + + + {t("access.props.provider.usage.hosting")} + + + {t("access.props.provider.usage.ca")} + + + {t("access.props.provider.usage.notification")} + +
); }; diff --git a/ui/src/components/workflow/WorkflowRuns.tsx b/ui/src/components/workflow/WorkflowRuns.tsx index 25f3891a..718a7913 100644 --- a/ui/src/components/workflow/WorkflowRuns.tsx +++ b/ui/src/components/workflow/WorkflowRuns.tsx @@ -11,7 +11,7 @@ import { SyncOutlined as SyncOutlinedIcon, } from "@ant-design/icons"; import { useRequest } from "ahooks"; -import { Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd"; +import { Alert, Button, Empty, Modal, Space, Table, type TableProps, Tag, Tooltip, notification } from "antd"; import dayjs from "dayjs"; import { ClientResponseError } from "pocketbase"; @@ -284,6 +284,8 @@ const WorkflowRuns = ({ className, style, workflowId }: WorkflowRunsProps) => { {NotificationContextHolder}
+ } /> + columns={tableColumns} dataSource={tableData} diff --git a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx index 33d1012c..5565c985 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigForm.tsx @@ -56,6 +56,7 @@ import DeployNodeConfigFormLocalConfig from "./DeployNodeConfigFormLocalConfig"; import DeployNodeConfigFormQiniuCDNConfig from "./DeployNodeConfigFormQiniuCDNConfig"; import DeployNodeConfigFormQiniuKodoConfig from "./DeployNodeConfigFormQiniuKodoConfig"; import DeployNodeConfigFormQiniuPiliConfig from "./DeployNodeConfigFormQiniuPiliConfig"; +import DeployNodeConfigFormRainYunRCDNConfig from "./DeployNodeConfigFormRainYunRCDNConfig"; import DeployNodeConfigFormSafeLineConfig from "./DeployNodeConfigFormSafeLineConfig"; import DeployNodeConfigFormSSHConfig from "./DeployNodeConfigFormSSHConfig.tsx"; import DeployNodeConfigFormTencentCloudCDNConfig from "./DeployNodeConfigFormTencentCloudCDNConfig.tsx"; @@ -80,6 +81,7 @@ import DeployNodeConfigFormVolcEngineDCDNConfig from "./DeployNodeConfigFormVolc import DeployNodeConfigFormVolcEngineImageXConfig from "./DeployNodeConfigFormVolcEngineImageXConfig.tsx"; import DeployNodeConfigFormVolcEngineLiveConfig from "./DeployNodeConfigFormVolcEngineLiveConfig.tsx"; import DeployNodeConfigFormVolcEngineTOSConfig from "./DeployNodeConfigFormVolcEngineTOSConfig.tsx"; +import DeployNodeConfigFormWangsuCDNProConfig from "./DeployNodeConfigFormWangsuCDNProConfig.tsx"; import DeployNodeConfigFormWebhookConfig from "./DeployNodeConfigFormWebhookConfig.tsx"; type DeployNodeConfigFormFieldValues = Partial; @@ -251,6 +253,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.QINIU_PILI: return ; + case DEPLOY_PROVIDERS.RAINYUN_RCDN: + return ; case DEPLOY_PROVIDERS.SAFELINE: return ; case DEPLOY_PROVIDERS.SSH: @@ -299,6 +303,8 @@ const DeployNodeConfigForm = forwardRef; case DEPLOY_PROVIDERS.VOLCENGINE_TOS: return ; + case DEPLOY_PROVIDERS.WANGSU_CDNPRO: + return ; case DEPLOY_PROVIDERS.WEBHOOK: return ; } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx index 00dc48dd..c06087de 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormGcoreCDNConfig.tsx @@ -4,7 +4,7 @@ import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; type DeployNodeConfigFormGcoreCDNConfigFieldValues = Nullish<{ - resourceId?: string | number; + resourceId: string | number; }>; export type DeployNodeConfigFormGcoreCDNConfigProps = { @@ -27,7 +27,7 @@ const DeployNodeConfigFormGcoreCDNConfig = ({ form: formInst, formName, disabled const formSchema = z.object({ resourceId: z.union([z.string(), z.number()]).refine((v) => { return /^\d+$/.test(v + "") && +v > 0; - }, t("workflow_node.deploy.form.gcore_cdn_certificate_id.placeholder")), + }, t("workflow_node.deploy.form.gcore_cdn_resource_id.placeholder")), }); const formRule = createSchemaFieldRule(formSchema); diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx new file mode 100644 index 00000000..b13ad5cb --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormRainYunRCDNConfig.tsx @@ -0,0 +1,80 @@ +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 DeployNodeConfigFormRainYunRCDNConfigFieldValues = Nullish<{ + instanceId: string | number; + domain: string; +}>; + +export type DeployNodeConfigFormRainYunRCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormRainYunRCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormRainYunRCDNConfigFieldValues) => void; +}; + +const initFormModel = (): DeployNodeConfigFormRainYunRCDNConfigFieldValues => { + return { + instanceId: "", + }; +}; + +const DeployNodeConfigFormRainYunRCDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormRainYunRCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + instanceId: z.union([z.string(), z.number()]).refine((v) => { + return /^\d+$/.test(v + "") && +v > 0; + }, t("workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder")), + domain: z + .string({ message: t("workflow_node.deploy.form.rainyun_rcdn_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 DeployNodeConfigFormRainYunRCDNConfig; diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx index d831fd7f..348f4d8d 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineALBConfig.tsx @@ -113,7 +113,7 @@ const DeployNodeConfigFormVolcEngineALBConfig = ({ } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx index c3ddfd03..99263044 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormVolcEngineCLBConfig.tsx @@ -104,7 +104,7 @@ const DeployNodeConfigFormVolcEngineCLBConfig = ({ } diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx new file mode 100644 index 00000000..90bdb064 --- /dev/null +++ b/ui/src/components/workflow/node/DeployNodeConfigFormWangsuCDNProConfig.tsx @@ -0,0 +1,107 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input, Select } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { validDomainName } from "@/utils/validators"; + +type DeployNodeConfigFormBaishanCDNConfigFieldValues = Nullish<{ + environment: string; + domain: string; + certificateId?: string; + webhookId?: string; +}>; + +export type DeployNodeConfigFormBaishanCDNConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: DeployNodeConfigFormBaishanCDNConfigFieldValues; + onValuesChange?: (values: DeployNodeConfigFormBaishanCDNConfigFieldValues) => void; +}; + +const ENVIRONMENT_PRODUCTION = "production" as const; +const ENVIRONMENT_STAGING = "stating" as const; + +const initFormModel = (): DeployNodeConfigFormBaishanCDNConfigFieldValues => { + return { + environment: ENVIRONMENT_PRODUCTION, + }; +}; + +const DeployNodeConfigFormBaishanCDNConfig = ({ + form: formInst, + formName, + disabled, + initialValues, + onValuesChange, +}: DeployNodeConfigFormBaishanCDNConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + resourceType: z.union([z.literal(ENVIRONMENT_PRODUCTION), z.literal(ENVIRONMENT_STAGING)], { + message: t("workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder"), + }), + domain: z + .string({ message: t("workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder") }) + .refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), + certificateId: z.string().nullish(), + webhookId: z.string().nullish(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ + + + + } + > + + + + } + > + + + + } + > + + +
+ ); +}; + +export default DeployNodeConfigFormBaishanCDNConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 86d644fa..3015e4d0 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -47,6 +47,7 @@ export interface AccessModel extends BaseModel { | AccessConfigForUpyun | AccessConfigForVercel | AccessConfigForVolcEngine + | AccessConfigForWangsu | AccessConfigForWebhook | AccessConfigForWestcn | AccessConfigForZeroSSL @@ -268,6 +269,11 @@ export type AccessConfigForVolcEngine = { secretAccessKey: string; }; +export type AccessConfigForWangsu = { + accessKeyId: string; + accessKeySecret: string; +}; + export type AccessConfigForWebhook = { url: string; allowInsecureConnections?: boolean; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 74296917..dbd81a0f 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -50,6 +50,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ UPYUN: "upyun", VERCEL: "vercel", VOLCENGINE: "volcengine", + WANGSU: "wangsu", WEBHOOK: "webhook", WESTCN: "westcn", ZEROSSL: "zerossl", @@ -94,10 +95,12 @@ export const accessProvidersMap: Maphttps://portal.qiniu.com/", "access.form.rainyun_api_key.label": "Rain Yun API key", "access.form.rainyun_api_key.placeholder": "Please enter Rain Yun API key", - "access.form.rainyun_api_key.tooltip": "For more information, see https://www.rainyun.com/docs/account/racc/setting", + "access.form.rainyun_api_key.tooltip": "For more information, see https://app.rainyun.com/account/settings/api-key", "access.form.safeline_api_url.label": "SafeLine URL", "access.form.safeline_api_url.placeholder": "Please enter SafeLine URL", "access.form.safeline_api_url.tooltip": "For more information, see https://docs.waf.chaitin.com/en/tutorials/install", @@ -297,6 +298,12 @@ "access.form.volcengine_secret_access_key.label": "VolcEngine SecretAccessKey", "access.form.volcengine_secret_access_key.placeholder": "Please enter VolcEngine SecretAccessKey", "access.form.volcengine_secret_access_key.tooltip": "For more information, see https://www.volcengine.com/docs/6291/216571", + "access.form.wangsu_access_key_id.label": "Wangsu Cloud AccessKeyId", + "access.form.wangsu_access_key_id.placeholder": "Please enter Wangsu Cloud AccessKeyId", + "access.form.wangsu_access_key_id.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775", + "access.form.wangsu_access_key_secret.label": "Wangsu Cloud AccessKeySecret", + "access.form.wangsu_access_key_secret.placeholder": "Please enter Wangsu Cloud AccessKeySecret", + "access.form.wangsu_access_key_secret.tooltip": "For more information, see https://en.wangsu.com/document/account-manage/15775", "access.form.webhook_url.label": "Webhook URL", "access.form.webhook_url.placeholder": "Please enter Webhook URL", "access.form.webhook_allow_insecure_conns.label": "Insecure SSL/TLS connections", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index a7b5c292..b4c866d9 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -5,7 +5,7 @@ "provider.acmehttpreq": "Http Request (ACME Proxy)", "provider.aliyun": "Alibaba Cloud", "provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)", - "provider.aliyun.cas": "Alibaba Cloud - Upload to CAS (Certificate Management Service)", + "provider.aliyun.cas_upload": "Alibaba Cloud - Upload to CAS (Certificate Management Service)", "provider.aliyun.cas_deploy": "Alibaba Cloud - Deploy via CAS (Certificate Management Service)", "provider.aliyun.cdn": "Alibaba Cloud - CDN (Content Delivery Network)", "provider.aliyun.clb": "Alibaba Cloud - CLB (Classic Load Balancer)", @@ -31,7 +31,7 @@ "provider.baiducloud.appblb": "Baidu Cloud - AppBLB (Application Baidu Load Balancer)", "provider.baiducloud.blb": "Baidu Cloud - BLB (Baidu Load Balancer)", "provider.baiducloud.cdn": "Baidu Cloud - CDN (Content Delivery Network)", - "provider.baiducloud.cert": "Baidu Cloud - Upload to SSL Certificate Service", + "provider.baiducloud.cert_upload": "Baidu Cloud - Upload to SSL Certificate Service", "provider.baiducloud.dns": "Baidu Cloud - DNS (Domain Name Service)", "provider.baishan": "Baishan", "provider.baishan.cdn": "Baishan - CDN (Content Delivery Network)", @@ -67,7 +67,7 @@ "provider.huaweicloud.cdn": "Huawei Cloud - CDN (Content Delivery Network)", "provider.huaweicloud.dns": "Huawei Cloud - DNS (Domain Name Service)", "provider.huaweicloud.elb": "Huawei Cloud - ELB (Elastic Load Balance)", - "provider.huaweicloud.scm": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)", + "provider.huaweicloud.scm_upload": "Huawei Cloud - Upload to SCM (SSL Certificate Manager)", "provider.huaweicloud.waf": "Huawei Cloud - WAF (Web Application Firewall)", "provider.jdcloud": "JD Cloud", "provider.jdcloud.alb": "JD Cloud - ALB (Application Load Balancer)", @@ -91,6 +91,7 @@ "provider.qiniu.kodo": "Qiniu - Kodo", "provider.qiniu.pili": "Qiniu - Pili", "provider.rainyun": "Rain Yun", + "provider.rainyun.rcdn": "Rain Yun - RCDN (Rain Content Delivery Network)", "provider.safeline": "SafeLine", "provider.ssh": "SSH deployment", "provider.sslcom": "SSL.com", @@ -103,7 +104,7 @@ "provider.tencentcloud.ecdn": "Tencent Cloud - ECDN (Enterprise Content Delivery Network)", "provider.tencentcloud.eo": "Tencent Cloud - EdgeOne", "provider.tencentcloud.scf": "Tencent Cloud - SCF (Serverless Cloud Function)", - "provider.tencentcloud.ssl": "Tencent Cloud - Upload to SSL Certificate Service", + "provider.tencentcloud.ssl_upload": "Tencent Cloud - Upload to SSL Certificate Service", "provider.tencentcloud.ssl_deploy": "Tencent Cloud - Deploy via SSL Certificate Service", "provider.tencentcloud.vod": "Tencent Cloud - VOD (Video on Demand)", "provider.tencentcloud.waf": "Tencent Cloud - WAF (Web Application Firewall)", @@ -112,18 +113,20 @@ "provider.ucloud.us3": "UCloud - US3 (UCloud Object-based Storage)", "provider.upyun": "UPYUN", "provider.upyun.cdn": "UPYUN - CDN (Content Delivery Network)", - "provider.upyun.file": "UPYUN - File Storage", + "provider.upyun.file": "UPYUN - USS (Storage Service)", "provider.vercel": "Vercel", "provider.volcengine": "Volcengine", "provider.volcengine.alb": "Volcengine - ALB (Application Load Balancer)", "provider.volcengine.cdn": "Volcengine - CDN (Content Delivery Network)", - "provider.volcengine.certcenter": "Volcengine - Upload to Certificate Center", + "provider.volcengine.certcenter_upload": "Volcengine - Upload to Certificate Center", "provider.volcengine.clb": "Volcengine - CLB (Cloud Load Balancer)", "provider.volcengine.dcdn": "Volcengine - DCDN (Dynamic Content Delivery Network)", "provider.volcengine.dns": "Volcengine - DNS (Domain Name Service)", "provider.volcengine.imagex": "Volcengine - ImageX", "provider.volcengine.live": "Volcengine - Live", "provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)", + "provider.wangsu": "Wangsu Cloud", + "provider.wangsu.cdnpro": "Wangsu Cloud - CDN Pro", "provider.webhook": "Webhook", "provider.westcn": "West.cn", "provider.zerossl": "ZeroSSL", diff --git a/ui/src/i18n/locales/en/nls.settings.json b/ui/src/i18n/locales/en/nls.settings.json index eb3aeb95..67ca64c4 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -53,15 +53,15 @@ "settings.notification.channel.form.email_sender_address.placeholder": "Please enter sender email address", "settings.notification.channel.form.email_receiver_address.label": "Receiver email address", "settings.notification.channel.form.email_receiver_address.placeholder": "Please enter receiver email address", - "settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL", "settings.notification.channel.form.gotify_url.label": "Service URL", - "settings.notification.channel.form.gotify_url.tooltip": "Example: https://gotify.exmaple.com, the protocol needs to be included but the trailing '/' should not be included.
For more information, see https://gotify.net/docs/pushmsg", - "settings.notification.channel.form.gotify_token.placeholder": "Please enter Application Token", - "settings.notification.channel.form.gotify_token.label": "Application Token", + "settings.notification.channel.form.gotify_url.placeholder": "Please enter Service URL", + "settings.notification.channel.form.gotify_url.tooltip": "For more information, see https://gotify.net/docs/pushmsg

Example: https://gotify.exmaple.com, the trailing '/' should not be included.", + "settings.notification.channel.form.gotify_token.label": "Application token", + "settings.notification.channel.form.gotify_token.placeholder": "Please enter Application token", "settings.notification.channel.form.gotify_token.tooltip": "For more information, see https://gotify.net/docs/pushmsg", "settings.notification.channel.form.gotify_priority.placeholder": "Please enter message priority", - "settings.notification.channel.form.gotify_priority.label": "Message Priority", - "settings.notification.channel.form.gotify_priority.tooltip": "Message Priority, you can set it to 1 as default.
For more information, see https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.label": "Message priority", + "settings.notification.channel.form.gotify_priority.tooltip": "For more information, see https://gotify.net/docs/pushmsg, https://github.com/gotify/android/issues/18#issuecomment-437403888", "settings.notification.channel.form.gotify_priority.error.gte0": "Message Priority must be greater than or equal to 0.", "settings.notification.channel.form.lark_webhook_url.label": "Webhook URL", "settings.notification.channel.form.lark_webhook_url.placeholder": "Please enter Webhook URL", @@ -74,6 +74,7 @@ "settings.notification.channel.form.pushover_user.tooltip": "For more information, see https://pushover.net/api#identifiers", "settings.notification.channel.form.pushplus_token.placeholder": "Please enter Token", "settings.notification.channel.form.pushplus_token.label": "Token", + "settings.notification.channel.form.pushplus_token.placeholder": "Please enter token", "settings.notification.channel.form.pushplus_token.tooltip": "For more information, see https://www.pushplus.plus/push1.html", "settings.notification.channel.form.serverchan_url.label": "Server URL", "settings.notification.channel.form.serverchan_url.placeholder": "Please enter ServerChan server URL (e.g. https://sctapi.ftqq.com/*****.send)", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index af3a6796..4ac796b3 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -95,7 +95,7 @@ "workflow_node.deploy.form.provider.placeholder": "Please select deploy target", "workflow_node.deploy.form.provider_access.label": "Host provider authorization", "workflow_node.deploy.form.provider_access.placeholder": "Please select an authorization of host provider", - "workflow_node.deploy.form.provider_access.tooltip": "Used to deploy certificates.", + "workflow_node.deploy.form.provider_access.tooltip": "Used to invoke API during deployment.", "workflow_node.deploy.form.provider_access.button": "Create", "workflow_node.deploy.form.provider_access.guide_for_local": "Tips: If you are running Certimate in Docker, the \"Local\" refers to the container rather than the host.", "workflow_node.deploy.form.certificate.label": "Certificate", @@ -269,11 +269,11 @@ "workflow_node.deploy.form.baiducloud_cdn_domain.label": "Baidu Cloud CDN domain", "workflow_node.deploy.form.baiducloud_cdn_domain.placeholder": "Please enter Baidu Cloud CDN domain name", "workflow_node.deploy.form.baiducloud_cdn_domain.tooltip": "For more information, see https://console.bce.baidu.com/cdn", - "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan CDN domain", - "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan CDN domain name", + "workflow_node.deploy.form.baishan_cdn_domain.label": "Baishan Cloud CDN domain", + "workflow_node.deploy.form.baishan_cdn_domain.placeholder": "Please enter Baishan Cloud CDN domain name", "workflow_node.deploy.form.baishan_cdn_domain.tooltip": "For more information, see https://cdnx.console.baishan.com", - "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan CDN certificate ID (Optional", - "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan CDN certificate ID", + "workflow_node.deploy.form.baishan_cdn_certificate_id.label": "Baishan Cloud CDN certificate ID (Optional)", + "workflow_node.deploy.form.baishan_cdn_certificate_id.placeholder": "Please enter Baishan Cloud CDN certificate ID", "workflow_node.deploy.form.baishan_cdn_certificate_id.tooltip": "For more information, see https://cdnx.console.baishan.com/#/cdn/cert", "workflow_node.deploy.form.baotapanel_console_auto_restart.label": "Auto restart after deployment", "workflow_node.deploy.form.baotapanel_site_type.label": "aaPanel site type", @@ -436,6 +436,12 @@ "workflow_node.deploy.form.qiniu_pili_domain.label": "Qiniu Pili streaming domain", "workflow_node.deploy.form.qiniu_pili_domain.placeholder": "Please enter Qiniu Pili streaming domain name", "workflow_node.deploy.form.qiniu_pili_domain.tooltip": "For more information, see https://portal.qiniu.com/hub", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "Rain Yun RCDN instance ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "Please enter Rain Yun RCDN instance ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "For more information, see https://app.rainyun.com/apps/rcdn/list", + "workflow_node.deploy.form.rainyun_rcdn_domain.label": "Rain Yun RCDN domain", + "workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "Please enter Rain Yun RCDN domain name", + "workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "For more information, see https://app.rainyun.com/apps/rcdn/list", "workflow_node.deploy.form.safeline_resource_type.label": "Resource type", "workflow_node.deploy.form.safeline_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "Certificate", @@ -633,6 +639,19 @@ "workflow_node.deploy.form.volcengine_tos_domain.label": "VolcEngine TOS domain", "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "Please enter VolcEngine TOS domain name", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "For more information, see https://console.volcengine.com/tos", + "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "Wangsu Cloud environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "Please select Wangsu Cloud environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "Production environment", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "Staging environment", + "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "Wangsu Cloud CDN domain", + "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "Please enter Wangsu Cloud CDN domain name", + "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/properties", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "Wangsu Cloud CDN certificate ID (Optional)", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "Please enter Wangsu Cloud CDN certificate ID", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "Wangsu Cloud CDN Webhook ID (Optional)", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "Please enter Wangsu Cloud CDN Webhook ID", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "For more information, see https://cdnpro.console.wangsu.com/v2/index/#/certificate", "workflow_node.deploy.form.webhook_data.label": "Webhook data (JSON format)", "workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data", "workflow_node.deploy.form.webhook_data.guide": "Tips: The Webhook data should be a key-value pair in JSON format. The values in JSON support template variables, which will be replaced by actual values when sent to the Webhook URL.

Supported variables:
${DOMAIN}: The primary domain of the certificate (CommonName).
${DOMAINS}: The domain list of the certificate (SubjectAltNames).
${CERTIFICATE}: The PEM format content of the certificate file.
${PRIVATE_KEY}: The PEM format content of the private key file.", diff --git a/ui/src/i18n/locales/en/nls.workflow.runs.json b/ui/src/i18n/locales/en/nls.workflow.runs.json index c48e54b7..551a1e90 100644 --- a/ui/src/i18n/locales/en/nls.workflow.runs.json +++ b/ui/src/i18n/locales/en/nls.workflow.runs.json @@ -5,6 +5,8 @@ "workflow_run.action.delete": "Delete run", "workflow_run.action.delete.confirm": "Are you sure to delete this run?", + "workflow_run.table.alert": "Attention: The workflow run contains the execution results of each node. Deleting it may trigger re-application or re-deployment of certificates due to the inability to find the previous execution result. Please do not delete unless necessary. It is recommended to keep it for at least 180 days.", + "workflow_run.props.id": "ID", "workflow_run.props.status": "Status", "workflow_run.props.status.pending": "Pending", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 657c2d68..bf068260 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -17,6 +17,7 @@ "access.props.provider.usage.hosting": "主机提供商", "access.props.provider.usage.ca": "证书颁发机构", "access.props.provider.usage.notification": "通知渠道", + "access.props.provider.builtin": "内置", "access.props.range.both_dns_hosting": "提供商", "access.props.range.ca_only": "证书颁发机构", "access.props.range.notify_only": "通知渠道", @@ -231,7 +232,7 @@ "access.form.qiniu_secret_key.tooltip": "这是什么?请参阅 https://portal.qiniu.com/", "access.form.rainyun_api_key.label": "雨云 API 密钥", "access.form.rainyun_api_key.placeholder": "请输入雨云 API 密钥", - "access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://www.rainyun.com/docs/account/racc/setting", + "access.form.rainyun_api_key.tooltip": "这是什么?请参阅 https://app.rainyun.com/account/settings/api-key", "access.form.safeline_api_url.label": "雷池 URL", "access.form.safeline_api_url.placeholder": "请输入雷池 URL", "access.form.safeline_api_url.tooltip": "这是什么?请参阅 https://docs.waf-ce.chaitin.cn/zh/上手指南/安装雷池", @@ -297,6 +298,12 @@ "access.form.volcengine_secret_access_key.label": "火山引擎 SecretAccessKey", "access.form.volcengine_secret_access_key.placeholder": "请输入火山引擎 SecretAccessKey", "access.form.volcengine_secret_access_key.tooltip": "这是什么?请参阅 https://www.volcengine.com/docs/6291/216571", + "access.form.wangsu_access_key_id.label": "网宿云 AccessKeyId", + "access.form.wangsu_access_key_id.placeholder": "请输入网宿科技 AccessKeyId", + "access.form.wangsu_access_key_id.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775", + "access.form.wangsu_access_key_secret.label": "网宿科技 AccessKeySecret", + "access.form.wangsu_access_key_secret.placeholder": "请输入网宿科技 AccessKeySecret", + "access.form.wangsu_access_key_secret.tooltip": "这是什么?请参阅 https://www.wangsu.com/document/account-manage/15775", "access.form.webhook_url.label": "Webhook 回调地址", "access.form.webhook_url.placeholder": "请输入 Webhook 回调地址", "access.form.webhook_allow_insecure_conns.label": "忽略 SSL/TLS 证书错误", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 3d7c1e58..216d3363 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -5,7 +5,7 @@ "provider.acmehttpreq": "Http Request (ACME Proxy)", "provider.aliyun": "阿里云", "provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB", - "provider.aliyun.cas": "阿里云 - 上传到数字证书管理服务 CAS", + "provider.aliyun.cas_upload": "阿里云 - 上传到数字证书管理服务 CAS", "provider.aliyun.cas_deploy": "阿里云 - 通过数字证书管理服务 CAS 创建部署任务", "provider.aliyun.cdn": "阿里云 - 内容分发网络 CDN", "provider.aliyun.clb": "阿里云 - 传统型负载均衡 CLB", @@ -31,7 +31,7 @@ "provider.baiducloud.appblb": "百度智能云 - 应用型负载均衡 BLB", "provider.baiducloud.blb": "百度智能云 - 普通型负载均衡 BLB", "provider.baiducloud.cdn": "百度智能云 - 内容分发网络 CDN", - "provider.baiducloud.cert": "百度智能云 - 上传到 SSL 证书服务", + "provider.baiducloud.cert_upload": "百度智能云 - 上传到 SSL 证书服务", "provider.baiducloud.dns": "百度智能云 - 智能云解析 DNS", "provider.baishan": "白山云", "provider.baishan.cdn": "白山云 - 内容分发网络 CDN", @@ -67,7 +67,7 @@ "provider.huaweicloud.cdn": "华为云 - 内容分发网络 CDN", "provider.huaweicloud.dns": "华为云 - 云解析 DNS", "provider.huaweicloud.elb": "华为云 - 弹性负载均衡 ELB", - "provider.huaweicloud.scm": "华为云 - 上传到云证书管理服务 SCM", + "provider.huaweicloud.scm_upload": "华为云 - 上传到云证书管理服务 SCM", "provider.huaweicloud.waf": "华为云 - Web 应用防火墙 WAF", "provider.jdcloud": "京东云", "provider.jdcloud.alb": "京东云 - 应用负载均衡 ALB", @@ -91,6 +91,7 @@ "provider.qiniu.kodo": "七牛云 - 对象存储 Kodo", "provider.qiniu.pili": "七牛云 - 视频直播 Pili", "provider.rainyun": "雨云", + "provider.rainyun.rcdn": "雨云 - 雨盾 CDN", "provider.safeline": "雷池", "provider.ssh": "SSH 部署", "provider.sslcom": "SSL.com", @@ -103,7 +104,7 @@ "provider.tencentcloud.ecdn": "腾讯云 - 全站加速网络 ECDN", "provider.tencentcloud.eo": "腾讯云 - 边缘安全加速平台 EdgeOne", "provider.tencentcloud.scf": "腾讯云 - 云函数 SCF", - "provider.tencentcloud.ssl": "腾讯云 - 上传到 SSL 证书服务", + "provider.tencentcloud.ssl_upload": "腾讯云 - 上传到 SSL 证书服务", "provider.tencentcloud.ssl_deploy": "腾讯云 - 通过 SSL 证书服务创建部署任务", "provider.tencentcloud.vod": "腾讯云 - 云点播 VOD", "provider.tencentcloud.waf": "腾讯云 - Web 应用防火墙 WAF", @@ -112,18 +113,20 @@ "provider.ucloud.us3": "优刻得 - 对象存储 US3", "provider.upyun": "又拍云", "provider.upyun.cdn": "又拍云 - 云分发 CDN", - "provider.upyun.file": "又拍云 - 云存储", + "provider.upyun.file": "又拍云 - 云存储 USS", "provider.vercel": "Vercel", "provider.volcengine": "火山引擎", "provider.volcengine.alb": "火山引擎 - 应用型负载均衡 ALB", "provider.volcengine.cdn": "火山引擎 - 内容分发网络 CDN", - "provider.volcengine.certcenter": "火山引擎 - 上传到证书中心", + "provider.volcengine.certcenter_upload": "火山引擎 - 上传到证书中心", "provider.volcengine.clb": "火山引擎 - 负载均衡 CLB", "provider.volcengine.dcdn": "火山引擎 - 全站加速 DCDN", "provider.volcengine.dns": "火山引擎 - 云解析 DNS", "provider.volcengine.imagex": "火山引擎 - 图片服务 ImageX", "provider.volcengine.live": "火山引擎 - 视频直播 Live", "provider.volcengine.tos": "火山引擎 - 对象存储 TOS", + "provider.wangsu": "网宿云", + "provider.wangsu.cdnpro": "网宿云 - CDN Pro", "provider.webhook": "Webhook", "provider.westcn": "西部数码", "provider.zerossl": "ZeroSSL", diff --git a/ui/src/i18n/locales/zh/nls.settings.json b/ui/src/i18n/locales/zh/nls.settings.json index b892d50c..41f8694d 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -53,16 +53,16 @@ "settings.notification.channel.form.email_sender_address.placeholder": "请输入发送邮箱地址", "settings.notification.channel.form.email_receiver_address.label": "接收邮箱地址", "settings.notification.channel.form.email_receiver_address.placeholder": "请输入接收邮箱地址", - "settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址", "settings.notification.channel.form.gotify_url.label": "服务地址", - "settings.notification.channel.form.gotify_url.tooltip": "示例: https://gotify.exmaple.com,需要包含协议但不要包含末尾的'/'。
请参阅 https://gotify.net/docs/pushmsg", - "settings.notification.channel.form.gotify_token.placeholder": "请输入应用Token", - "settings.notification.channel.form.gotify_token.label": "应用Token", - "settings.notification.channel.form.gotify_token.tooltip": "应用Token。
请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.gotify_url.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsg

示例: https://gotify.exmaple.com,不要包含末尾的'/'。", + "settings.notification.channel.form.gotify_token.label": "应用 Token", + "settings.notification.channel.form.gotify_token.placeholder": "请输入应用 Token", + "settings.notification.channel.form.gotify_token.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsg", + "settings.notification.channel.form.gotify_priority.label": "消息优先级(可选)", "settings.notification.channel.form.gotify_priority.placeholder": "请输入消息优先级", - "settings.notification.channel.form.gotify_priority.label": "消息优先级", - "settings.notification.channel.form.gotify_priority.tooltip": "消息优先级, 可以设置为1作为默认值。
请参阅 https://gotify.net/docs/pushmsg
https://github.com/gotify/android/issues/18#issuecomment-437403888", - "settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于0", + "settings.notification.channel.form.gotify_priority.tooltip": "这是什么?请参阅 https://gotify.net/docs/pushmsghttps://github.com/gotify/android/issues/18#issuecomment-437403888", + "settings.notification.channel.form.gotify_priority.error.gte0": "消息优先级需要大于等于 0", "settings.notification.channel.form.lark_webhook_url.label": "机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.placeholder": "请输入机器人 Webhook 地址", "settings.notification.channel.form.lark_webhook_url.tooltip": "这是什么?请参阅 https://www.feishu.cn/hc/zh-CN/articles/807992406756", @@ -74,7 +74,8 @@ "settings.notification.channel.form.pushover_user.tooltip": "这是什么?请参阅 https://pushover.net/api#identifiers", "settings.notification.channel.form.pushplus_token.placeholder": "请输入Token", "settings.notification.channel.form.pushplus_token.label": "Token", - "settings.notification.channel.form.pushplus_token.tooltip": "请参阅 https://www.pushplus.plus/push1.html", + "settings.notification.channel.form.pushplus_token.placeholder": "请输入 Token", + "settings.notification.channel.form.pushplus_token.tooltip": "这是什么?请参阅 https://www.pushplus.plus/push1.html", "settings.notification.channel.form.serverchan_url.label": "服务器地址", "settings.notification.channel.form.serverchan_url.placeholder": "请输入服务器地址(形如: https://sctapi.ftqq.com/*****.send)", "settings.notification.channel.form.serverchan_url.tooltip": "这是什么?请参阅 https://sct.ftqq.com/forward", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 7338912f..5fcb201d 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -94,7 +94,7 @@ "workflow_node.deploy.form.provider.placeholder": "请选择部署目标", "workflow_node.deploy.form.provider_access.label": "主机提供商授权", "workflow_node.deploy.form.provider_access.placeholder": "请选择主机提供商授权", - "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书,注意与申请阶段所需的 DNS 提供商相区分。", + "workflow_node.deploy.form.provider_access.tooltip": "用于部署证书时调用相关 API,注意与申请阶段所需的 DNS 提供商相区分。", "workflow_node.deploy.form.provider_access.button": "新建", "workflow_node.deploy.form.provider_access.guide_for_local": "小贴士:如果你正在使用 Docker 运行 Certimate,“本地”指的是容器内而非宿主机。", "workflow_node.deploy.form.certificate.label": "待部署证书", @@ -435,6 +435,12 @@ "workflow_node.deploy.form.qiniu_pili_domain.label": "七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.placeholder": "请输入七牛云视频直播流域名", "workflow_node.deploy.form.qiniu_pili_domain.tooltip": "这是什么?请参阅 https://portal.qiniu.com/hub", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.label": "雨云 RCDN 实例 ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.placeholder": "请输入雨云 RCDN 实例 ID", + "workflow_node.deploy.form.rainyun_rcdn_instance_id.tooltip": "这是什么?请参阅 https://app.rainyun.com/apps/rcdn/list", + "workflow_node.deploy.form.rainyun_rcdn_domain.label": "雨云 RCDN 加速域名", + "workflow_node.deploy.form.rainyun_rcdn_domain.placeholder": "请输入雨云 RCDN 加速域名(支持泛域名)", + "workflow_node.deploy.form.rainyun_rcdn_domain.tooltip": "这是什么?请参阅 https://app.rainyun.com/apps/rcdn/list", "workflow_node.deploy.form.safeline_resource_type.label": "证书替换方式", "workflow_node.deploy.form.safeline_resource_type.placeholder": "请选择证书替换方式", "workflow_node.deploy.form.safeline_resource_type.option.certificate.label": "替换指定证书", @@ -632,6 +638,19 @@ "workflow_node.deploy.form.volcengine_tos_domain.label": "火山引擎 TOS 自定义域名", "workflow_node.deploy.form.volcengine_tos_domain.placeholder": "请输入火山引擎 TOS 自定义域名", "workflow_node.deploy.form.volcengine_tos_domain.tooltip": "这是什么?请参阅 see https://console.volcengine.com/tos", + "workflow_node.deploy.form.wangsu_cdnpro_environment.label": "网宿云环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.placeholder": "请选择网宿云环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.production.label": "生产环境", + "workflow_node.deploy.form.wangsu_cdnpro_environment.option.staging.label": "演练环境", + "workflow_node.deploy.form.wangsu_cdnpro_domain.label": "网宿云 CDN Pro 加速域名", + "workflow_node.deploy.form.wangsu_cdnpro_domain.placeholder": "请输入网宿云 CDN Pro 加速域名(支持泛域名)", + "workflow_node.deploy.form.wangsu_cdnpro_domain.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/properties", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.label": "网宿云 CDN Pro 原证书 ID(可选)", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.placeholder": "请输入网宿云 CDN Pro 原证书 ID", + "workflow_node.deploy.form.wangsu_cdnpro_certificate_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate

不填写时,将上传新证书;否则,将替换原证书。", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.label": "网宿云 CDN Pro 部署任务 Webhook ID(可选)", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.placeholder": "请输入网宿云 CDN Pro 部署任务 Webhook ID", + "workflow_node.deploy.form.wangsu_cdnpro_webhook_id.tooltip": "这是什么?请参阅 https://cdnpro.console.wangsu.com/v2/index/#/certificate", "workflow_node.deploy.form.webhook_data.label": "Webhook 回调数据(JSON 格式)", "workflow_node.deploy.form.webhook_data.placeholder": "请输入 Webhook 回调数据", "workflow_node.deploy.form.webhook_data.guide": "小贴士:回调数据是一个 JSON 格式的键值对。其中值支持模板变量,将在被发送到指定的 Webhook URL 时被替换为实际值;其他内容将保持原样。

支持的变量:
${DOMAIN}:证书的主域名(即 CommonName
${DOMAINS}:证书的多域名列表(即 SubjectAltNames
${CERTIFICATE}:证书文件 PEM 格式内容
${PRIVATE_KEY}:私钥文件 PEM 格式内容", @@ -639,7 +658,7 @@ "workflow_node.deploy.form.webhook_data_preset.button": "使用预设模板", "workflow_node.deploy.form.strategy_config.label": "执行策略", "workflow_node.deploy.form.skip_on_last_succeeded.label": "重复部署", - "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书已成功时", + "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "当上次部署相同证书成功时,", "workflow_node.deploy.form.skip_on_last_succeeded.suffix": "重新部署。", "workflow_node.deploy.form.skip_on_last_succeeded.switch.on": "跳过", "workflow_node.deploy.form.skip_on_last_succeeded.switch.off": "不跳过", diff --git a/ui/src/i18n/locales/zh/nls.workflow.runs.json b/ui/src/i18n/locales/zh/nls.workflow.runs.json index ce3ac15d..4f8640e6 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.runs.json +++ b/ui/src/i18n/locales/zh/nls.workflow.runs.json @@ -5,6 +5,8 @@ "workflow_run.action.delete": "删除执行", "workflow_run.action.delete.confirm": "确定要删除此执行吗?请注意此操作仅清除日志历史,但不会影响签发的证书。", + "workflow_run.table.alert": "注意:执行记录中包含工作流各节点的执行结果,删除后可能导致因找不到前次执行结果而触发重新申请或部署证书。如无必要请勿提前删除,建议保留至少 180 天。", + "workflow_run.props.id": "ID", "workflow_run.props.status": "状态", "workflow_run.props.status.pending": "等待执行", diff --git a/ui/src/pages/dashboard/Dashboard.tsx b/ui/src/pages/dashboard/Dashboard.tsx index 83f8cd47..9915a8a7 100644 --- a/ui/src/pages/dashboard/Dashboard.tsx +++ b/ui/src/pages/dashboard/Dashboard.tsx @@ -285,7 +285,7 @@ const Dashboard = () => { }} pagination={false} rowKey={(record) => record.id} - scroll={{ x: "max(100%, 960px)" }} + scroll={{ x: "max(100%, 720px)" }} size="small" />