diff --git a/.github/ISSUE_TEMPLATE/3-questions.yml b/.github/ISSUE_TEMPLATE/3-questions.yml index fd847486..f4918034 100644 --- a/.github/ISSUE_TEMPLATE/3-questions.yml +++ b/.github/ISSUE_TEMPLATE/3-questions.yml @@ -1,6 +1,6 @@ name: "❓ Questions" description: "遇到了困难需要求助? / Have problem in use and need help?" -title: "[Feature] 简要描述你遇到的问题" +title: "简要描述你遇到的问题" body: - type: markdown attributes: @@ -19,6 +19,14 @@ body: 3. Yes, I've read the [documentation](https://docs.certimate.me/en/) and didn't find any similar. 4. Please describe the problem in detail according to the template specification, otherwise the issue will be closed directly. + - type: input + attributes: + label: 软件版本 / Release Version + description: 请提供 Certimate 的具体版本。 / Please provide the specific version of Certimate. + placeholder: (e.g. v1.0.0) + validations: + required: true + - type: textarea attributes: label: 问题描述 / Description diff --git a/go.mod b/go.mod index 7c8723af..c9cdb902 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/Azure/azure-sdk-for-go/sdk/azcore v1.17.0 github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.8.2 github.com/Azure/azure-sdk-for-go/sdk/keyvault/azcertificates v0.9.0 + github.com/Edgio/edgio-api v0.0.0-workspace github.com/G-Core/gcorelabscdn-go v1.0.28 github.com/alibabacloud-go/alb-20200616/v2 v2.2.8 github.com/alibabacloud-go/cas-20200407/v3 v3.0.4 @@ -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/go.sum b/go.sum index 98e8211f..f7267783 100644 --- a/go.sum +++ b/go.sum @@ -498,6 +498,8 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-hclog v0.12.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-hclog v0.16.2/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-immutable-radix v1.3.1/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= 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/notify.go b/internal/domain/notify.go index 4bc57b85..6142dae5 100644 --- a/internal/domain/notify.go +++ b/internal/domain/notify.go @@ -14,6 +14,8 @@ const ( NotifyChannelTypeEmail = NotifyChannelType("email") NotifyChannelTypeGotify = NotifyChannelType("gotify") NotifyChannelTypeLark = NotifyChannelType("lark") + NotifyChannelTypeMattermost = NotifyChannelType("mattermost") + NotifyChannelTypePushover = NotifyChannelType("pushover") NotifyChannelTypePushPlus = NotifyChannelType("pushplus") NotifyChannelTypeServerChan = NotifyChannelType("serverchan") NotifyChannelTypeTelegram = NotifyChannelType("telegram") 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/notify/providers.go b/internal/notify/providers.go index 3a7cadf9..c5b5e975 100644 --- a/internal/notify/providers.go +++ b/internal/notify/providers.go @@ -10,6 +10,8 @@ import ( pEmail "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/email" pGotify "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/gotify" pLark "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/lark" + pMattermost "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" + pPushover "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover" pPushPlus "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushplus" pServerChan "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/serverchan" pTelegram "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/telegram" @@ -59,6 +61,19 @@ func createNotifier(channel domain.NotifyChannelType, channelConfig map[string]a WebhookUrl: maputil.GetString(channelConfig, "webhookUrl"), }) + case domain.NotifyChannelTypeMattermost: + return pMattermost.NewNotifier(&pMattermost.NotifierConfig{ + ServerUrl: maputil.GetString(channelConfig, "serverUrl"), + ChannelId: maputil.GetString(channelConfig, "channelId"), + Username: maputil.GetString(channelConfig, "username"), + Password: maputil.GetString(channelConfig, "password"), + }) + case domain.NotifyChannelTypePushover: + return pPushover.NewNotifier(&pPushover.NotifierConfig{ + Token: maputil.GetString(channelConfig, "token"), + User: maputil.GetString(channelConfig, "user"), + }) + case domain.NotifyChannelTypePushPlus: return pPushPlus.NewNotifier(&pPushPlus.NotifierConfig{ Token: maputil.GetString(channelConfig, "token"), 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/tencentcloud-ecdn/tencentcloud_ecdn.go b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go index bd2aebe2..beb5a043 100644 --- a/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go +++ b/internal/pkg/core/deployer/providers/tencentcloud-ecdn/tencentcloud_ecdn.go @@ -107,7 +107,7 @@ func (d *DeployerProvider) Deploy(ctx context.Context, certPem string, privkeyPe // REF: https://cloud.tencent.com/document/product/400/91667 deployCertificateInstanceReq := tcssl.NewDeployCertificateInstanceRequest() deployCertificateInstanceReq.CertificateId = common.StringPtr(upres.CertId) - deployCertificateInstanceReq.ResourceType = common.StringPtr("ecdn") + deployCertificateInstanceReq.ResourceType = common.StringPtr("cdn") deployCertificateInstanceReq.Status = common.Int64Ptr(1) deployCertificateInstanceReq.InstanceIdList = common.StringPtrs(instanceIds) deployCertificateInstanceResp, err := d.sdkClients.SSL.DeployCertificateInstance(deployCertificateInstanceReq) 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/notifier/providers/mattermost/mattermost.go b/internal/pkg/core/notifier/providers/mattermost/mattermost.go new file mode 100644 index 00000000..24890794 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost.go @@ -0,0 +1,89 @@ +package mattermost + +import ( + "bytes" + "context" + "encoding/json" + "github.com/nikoksr/notify/service/mattermost" + "github.com/usual2970/certimate/internal/pkg/core/notifier" + "io" + "log/slog" + "net/http" +) + +type NotifierConfig struct { + // Mattermost 服务地址。 + ServerUrl string `json:"serverUrl"` + // 频道ID + ChannelId string `json:"channelId"` + // 用户名 + Username string `json:"username"` + // 密码 + Password string `json:"password"` +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + }, nil +} + +func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { + if logger == nil { + n.logger = slog.Default() + } else { + n.logger = logger + } + return n +} + +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + srv := mattermost.New(n.config.ServerUrl) + + if err := srv.LoginWithCredentials(ctx, n.config.Username, n.config.Password); err != nil { + return nil, err + } + + srv.AddReceivers(n.config.ChannelId) + + // 复写消息样式 + srv.PreSend(func(req *http.Request) error { + m := map[string]interface{}{ + "channel_id": n.config.ChannelId, + "props": map[string]interface{}{ + "attachments": []map[string]interface{}{ + { + "title": subject, + "text": message, + }, + }, + }, + } + + if body, err := json.Marshal(m); err != nil { + return err + } else { + req.ContentLength = int64(len(body)) + req.Body = io.NopCloser(bytes.NewReader(body)) + } + + return nil + }) + + if err = srv.Send(ctx, subject, message); err != nil { + return nil, err + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go new file mode 100644 index 00000000..6db6cc42 --- /dev/null +++ b/internal/pkg/core/notifier/providers/mattermost/mattermost_test.go @@ -0,0 +1,74 @@ +package mattermost_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/mattermost" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fServerUrl string + fChannelId string + fUsername string + fPassword string +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_MATTERMOST_" + + flag.StringVar(&fServerUrl, argsPrefix+"SERVERURL", "", "") + flag.StringVar(&fChannelId, argsPrefix+"CHANNELID", "", "") + flag.StringVar(&fUsername, argsPrefix+"USERNAME", "", "") + flag.StringVar(&fPassword, argsPrefix+"PASSWORD", "", "") +} + +/* +Shell command to run this test: + + go test -v ./mattermost_test.go -args \ + --CERTIMATE_NOTIFIER_MATTERMOST_SERVERURL="https://example.com/your-server-url" \ + --CERTIMATE_NOTIFIER_MATTERMOST_CHANNELID="your-chanel-id" \ + --CERTIMATE_NOTIFIER_MATTERMOST_USERNAME="your-username" \ + --CERTIMATE_NOTIFIER_MATTERMOST_PASSWORD="your-password" +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("SERVERURL: %v", fServerUrl), + fmt.Sprintf("CHANNELID: %v", fChannelId), + fmt.Sprintf("USERNAME: %v", fUsername), + fmt.Sprintf("PASSWORD: %v", fPassword), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + ServerUrl: fServerUrl, + ChannelId: fChannelId, + Username: fUsername, + Password: fPassword, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + t.Logf("ok: %v", res) + }) +} diff --git a/internal/pkg/core/notifier/providers/pushover/pushover.go b/internal/pkg/core/notifier/providers/pushover/pushover.go new file mode 100644 index 00000000..8f84dfd2 --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushover/pushover.go @@ -0,0 +1,102 @@ +package pushover + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "log/slog" + "net/http" + + "github.com/pkg/errors" + + "github.com/usual2970/certimate/internal/pkg/core/notifier" +) + +type NotifierConfig struct { + Token string `json:"token"` // 应用 API Token + User string `json:"user"` // 用户/分组 Key +} + +type NotifierProvider struct { + config *NotifierConfig + logger *slog.Logger + // 未来将移除 + httpClient *http.Client +} + +var _ notifier.Notifier = (*NotifierProvider)(nil) + +func NewNotifier(config *NotifierConfig) (*NotifierProvider, error) { + if config == nil { + panic("config is nil") + } + + return &NotifierProvider{ + config: config, + httpClient: http.DefaultClient, + }, nil +} + +func (n *NotifierProvider) WithLogger(logger *slog.Logger) notifier.Notifier { + if logger == nil { + n.logger = slog.Default() + } else { + n.logger = logger + } + return n +} + +// Notify 发送通知 +// 参考文档:https://pushover.net/api +func (n *NotifierProvider) Notify(ctx context.Context, subject string, message string) (res *notifier.NotifyResult, err error) { + // 请求体 + reqBody := &struct { + Token string `json:"token"` + User string `json:"user"` + Title string `json:"title"` + Message string `json:"message"` + }{ + Token: n.config.Token, + User: n.config.User, + Title: subject, + Message: message, + } + + // Make request + body, err := json.Marshal(reqBody) + if err != nil { + return nil, errors.Wrap(err, "encode message body") + } + + req, err := http.NewRequestWithContext( + ctx, + http.MethodPost, + "https://api.pushover.net/1/messages.json", + bytes.NewReader(body), + ) + if err != nil { + return nil, errors.Wrap(err, "create new request") + } + + req.Header.Set("Content-Type", "application/json; charset=utf-8") + + // Send request to pushover service + resp, err := n.httpClient.Do(req) + if err != nil { + return nil, errors.Wrapf(err, "send request to pushover server") + } + defer resp.Body.Close() + + result, err := io.ReadAll(resp.Body) + if err != nil { + return nil, errors.Wrap(err, "read response") + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("pushover returned status code %d: %s", resp.StatusCode, string(result)) + } + + return ¬ifier.NotifyResult{}, nil +} diff --git a/internal/pkg/core/notifier/providers/pushover/pushover_test.go b/internal/pkg/core/notifier/providers/pushover/pushover_test.go new file mode 100644 index 00000000..450beac1 --- /dev/null +++ b/internal/pkg/core/notifier/providers/pushover/pushover_test.go @@ -0,0 +1,62 @@ +package pushover_test + +import ( + "context" + "flag" + "fmt" + "strings" + "testing" + + provider "github.com/usual2970/certimate/internal/pkg/core/notifier/providers/pushover" +) + +const ( + mockSubject = "test_subject" + mockMessage = "test_message" +) + +var ( + fToken string + fUser string +) + +func init() { + argsPrefix := "CERTIMATE_NOTIFIER_PUSHOVER_" + flag.StringVar(&fToken, argsPrefix+"TOKEN", "", "") + flag.StringVar(&fUser, argsPrefix+"USER", "", "") +} + +/* +Shell command to run this test: + + go test -v ./pushover_test.go -args \ + --CERTIMATE_NOTIFIER_PUSHOVER_TOKEN="your-pushover-token" \ + --CERTIMATE_NOTIFIER_PUSHOVER_USER="your-pushover-user" \ +*/ +func TestNotify(t *testing.T) { + flag.Parse() + + t.Run("Notify", func(t *testing.T) { + t.Log(strings.Join([]string{ + "args:", + fmt.Sprintf("TOKEN: %v", fToken), + }, "\n")) + + notifier, err := provider.NewNotifier(&provider.NotifierConfig{ + Token: fToken, + User: fUser, + }) + if err != nil { + t.Errorf("err: %+v", err) + return + } + + res, err := notifier.Notify(context.Background(), mockSubject, mockMessage) + 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..dff719f1 --- /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", deploymentTaskId), 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/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index aa3f4f12..c9af4acd 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -9,6 +9,8 @@ import NotifyChannelEditFormDingTalkFields from "./NotifyChannelEditFormDingTalk import NotifyChannelEditFormEmailFields from "./NotifyChannelEditFormEmailFields"; import NotifyChannelEditFormGotifyFields from "./NotifyChannelEditFormGotifyFields.tsx"; import NotifyChannelEditFormLarkFields from "./NotifyChannelEditFormLarkFields"; +import NotifyChannelEditFormMattermostFields from "./NotifyChannelEditFormMattermostFields.tsx"; +import NotifyChannelEditFormPushoverFields from "./NotifyChannelEditFormPushoverFields"; import NotifyChannelEditFormPushPlusFields from "./NotifyChannelEditFormPushPlusFields"; import NotifyChannelEditFormServerChanFields from "./NotifyChannelEditFormServerChanFields"; import NotifyChannelEditFormTelegramFields from "./NotifyChannelEditFormTelegramFields"; @@ -54,6 +56,10 @@ const NotifyChannelEditForm = forwardRef; case NOTIFY_CHANNELS.LARK: return ; + case NOTIFY_CHANNELS.MATTERMOST: + return ; + case NOTIFY_CHANNELS.PUSHOVER: + return ; case NOTIFY_CHANNELS.PUSHPLUS: return ; case NOTIFY_CHANNELS.SERVERCHAN: diff --git a/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx new file mode 100644 index 00000000..a847be75 --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormMattermostFields.tsx @@ -0,0 +1,64 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormMattermostFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + serverUrl: z + .string({ message: t("settings.notification.channel.form.mattermost_server_url.placeholder") }) + .url(t("common.errmsg.url_invalid")), + channelId: z + .string({ message: t("settings.notification.channel.form.mattermost_channel_id.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_channel_id.placeholder")), + username: z + .string({ message: t("settings.notification.channel.form.mattermost_username.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_username.placeholder")), + password: z + .string({ message: t("settings.notification.channel.form.mattermost_password.placeholder") }) + .nonempty(t("settings.notification.channel.form.mattermost_password.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + + } + > + + + + + + + + + + + + ); +}; + +export default NotifyChannelEditFormMattermostFields; diff --git a/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx b/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx new file mode 100644 index 00000000..449c98fa --- /dev/null +++ b/ui/src/components/notification/NotifyChannelEditFormPushoverFields.tsx @@ -0,0 +1,41 @@ +import { useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +const NotifyChannelEditFormPushoverFields = () => { + const { t } = useTranslation(); + + const formSchema = z.object({ + token: z + .string({ message: t("settings.notification.channel.form.pushover_token.placeholder") }) + .nonempty(t("settings.notification.channel.form.pushover_token.placeholder")), + user: z + .string({ message: t("settings.notification.channel.form.pushover_user.placeholder") }) + .nonempty(t("settings.notification.channel.form.pushover_user.placeholder")), + }); + const formRule = createSchemaFieldRule(formSchema); + + return ( + <> + } + > + + + } + > + + + + ); +}; + +export default NotifyChannelEditFormPushoverFields; 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: Map = new [NOTIFY_CHANNELS.DINGTALK, "common.notifier.dingtalk"], [NOTIFY_CHANNELS.GOTIFY, "common.notifier.gotify"], [NOTIFY_CHANNELS.LARK, "common.notifier.lark"], + [NOTIFY_CHANNELS.MATTERMOST, "common.notifier.mattermost"], + [NOTIFY_CHANNELS.PUSHOVER, "common.notifier.pushover"], [NOTIFY_CHANNELS.PUSHPLUS, "common.notifier.pushplus"], [NOTIFY_CHANNELS.WECOM, "common.notifier.wecom"], [NOTIFY_CHANNELS.TELEGRAM, "common.notifier.telegram"], diff --git a/ui/src/domain/version.ts b/ui/src/domain/version.ts index f437dc74..8818dd83 100644 --- a/ui/src/domain/version.ts +++ b/ui/src/domain/version.ts @@ -1 +1 @@ -export const version = "v0.3.7"; +export const version = "v0.3.8"; diff --git a/ui/src/i18n/locales/en/nls.access.json b/ui/src/i18n/locales/en/nls.access.json index 791b81fc..b0b7f140 100644 --- a/ui/src/i18n/locales/en/nls.access.json +++ b/ui/src/i18n/locales/en/nls.access.json @@ -17,6 +17,7 @@ "access.props.provider.usage.hosting": "Hosting provider", "access.props.provider.usage.ca": "Certificate authority", "access.props.provider.usage.notification": "Notification channel", + "access.props.provider.builtin": "Built-in", "access.props.range.both_dns_hosting": "Provider", "access.props.range.ca_only": "Certificate authority", "access.props.range.notify_only": "Notification channel", @@ -237,7 +238,7 @@ "access.form.qiniu_secret_key.tooltip": "For more information, see https://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.common.json b/ui/src/i18n/locales/en/nls.common.json index c5949d28..0c3cd5df 100644 --- a/ui/src/i18n/locales/en/nls.common.json +++ b/ui/src/i18n/locales/en/nls.common.json @@ -41,6 +41,8 @@ "common.notifier.email": "Email", "common.notifier.gotify": "Gotify", "common.notifier.lark": "Lark", + "common.notifier.mattermost": "Mattermost", + "common.notifier.pushover": "Pushover", "common.notifier.pushplus": "PushPlus", "common.notifier.serverchan": "ServerChan", "common.notifier.telegram": "Telegram", 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 d436665c..b56113c4 100644 --- a/ui/src/i18n/locales/en/nls.settings.json +++ b/ui/src/i18n/locales/en/nls.settings.json @@ -53,21 +53,38 @@ "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", "settings.notification.channel.form.lark_webhook_url.tooltip": "For more information, see https://www.feishu.cn/hc/en-US/articles/807992406756", + "settings.notification.channel.form.mattermost_server_url.label": "Service URL", + "settings.notification.channel.form.mattermost_server_url.placeholder": "Please enter service URL", + "settings.notification.channel.form.mattermost_server_url.tooltip": "Example: https://exmaple.com, the protocol needs to be included but the trailing '/' should not be included.", + "settings.notification.channel.form.mattermost_channel_id.label": "Channel ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "Please enter channel ID", + "settings.notification.channel.form.mattermost_channel_id.tooltip": "How to get the channel ID? Select the target channel from the left sidebar, click on the channel name at the top, and choose ”Channel Details.” You can directly see the channel ID on the pop-up page.", + "settings.notification.channel.form.mattermost_username.label": "Username", + "settings.notification.channel.form.mattermost_username.placeholder": "Please enter username", + "settings.notification.channel.form.mattermost_password.label": "Password", + "settings.notification.channel.form.mattermost_password.placeholder": "Please enter password", + "settings.notification.channel.form.pushover_token.placeholder": "Please enter Application API Token", + "settings.notification.channel.form.pushover_token.label": "Application API Token", + "settings.notification.channel.form.pushover_token.tooltip": "For more information, see https://pushover.net/api#registration", + "settings.notification.channel.form.pushover_user.placeholder": "Please enter User/Group Key", + "settings.notification.channel.form.pushover_user.label": "User/Group Key", + "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.common.json b/ui/src/i18n/locales/zh/nls.common.json index 726c5ca2..fe3a9e06 100644 --- a/ui/src/i18n/locales/zh/nls.common.json +++ b/ui/src/i18n/locales/zh/nls.common.json @@ -41,6 +41,8 @@ "common.notifier.email": "邮件", "common.notifier.gotify": "Gotify", "common.notifier.lark": "飞书", + "common.notifier.mattermost": "Mattermost", + "common.notifier.pushover": "Pushover", "common.notifier.pushplus": "PushPlus推送加", "common.notifier.serverchan": "Server 酱", "common.notifier.telegram": "Telegram", 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 c00d158a..24342452 100644 --- a/ui/src/i18n/locales/zh/nls.settings.json +++ b/ui/src/i18n/locales/zh/nls.settings.json @@ -53,22 +53,39 @@ "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", + "settings.notification.channel.form.mattermost_server_url.label": "服务地址", + "settings.notification.channel.form.mattermost_server_url.placeholder": "请输入服务地址", + "settings.notification.channel.form.mattermost_server_url.tooltip": "示例: https://exmaple.com,需要包含协议但不要包含末尾的'/'", + "settings.notification.channel.form.mattermost_channel_id.label": "频道ID", + "settings.notification.channel.form.mattermost_channel_id.placeholder": "请输入频道ID", + "settings.notification.channel.form.mattermost_channel_id.tooltip": "频道ID怎么获取?从左侧边栏中选择目标频道,点击顶部的频道名称,选择“频道详情”,即可在弹出页面中直接看到频道ID", + "settings.notification.channel.form.mattermost_username.label": "用户名", + "settings.notification.channel.form.mattermost_username.placeholder": "请输入用户名", + "settings.notification.channel.form.mattermost_password.label": "密码", + "settings.notification.channel.form.mattermost_password.placeholder": "请输入密码", + "settings.notification.channel.form.pushover_token.placeholder": "请输入应用 API Token", + "settings.notification.channel.form.pushover_token.label": "应用 API Token", + "settings.notification.channel.form.pushover_token.tooltip": "这是什么?请参阅 https://pushover.net/api#registration", + "settings.notification.channel.form.pushover_user.placeholder": "请输入用户/分组 Key", + "settings.notification.channel.form.pushover_user.label": "用户/分组 Key", + "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..919b53d3 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" /> @@ -310,7 +310,7 @@ const StatisticCard = ({ onClick?: () => void; }) => { return ( - + {icon}