diff --git a/go.mod b/go.mod index 0140ddc8..861bfceb 100644 --- a/go.mod +++ b/go.mod @@ -140,6 +140,7 @@ require ( github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect + github.com/vultr/govultr/v3 v3.21.1 // indirect github.com/x448/float16 v0.8.4 // indirect go.mongodb.org/mongo-driver v1.17.2 // indirect go.yaml.in/yaml/v2 v2.4.2 // indirect diff --git a/go.sum b/go.sum index 74227562..959dfd1a 100644 --- a/go.sum +++ b/go.sum @@ -887,6 +887,8 @@ github.com/volcengine/volc-sdk-golang v1.0.219 h1:IqMCdpJ6uuqS2ZZQYUVHKVd+2H1au0 github.com/volcengine/volc-sdk-golang v1.0.219/go.mod h1:zHJlaqiMbIB+0mcrsZPTwOb3FB7S/0MCfqlnO8R7hlM= github.com/volcengine/volcengine-go-sdk v1.1.30 h1:D85LL8euXgxwsZzwQ0azT/hannEsiKxKY4h/7/HU764= github.com/volcengine/volcengine-go-sdk v1.1.30/go.mod h1:EyKoi6t6eZxoPNGr2GdFCZti2Skd7MO3eUzx7TtSvNo= +github.com/vultr/govultr/v3 v3.21.1 h1:0cnA8fXiqayPGbAlNHaW+5oCQjpDNkkAm3Nt3LOHplM= +github.com/vultr/govultr/v3 v3.21.1/go.mod h1:9WwnWGCKnwDlNjHjtt+j+nP+0QWq6hQXzaHgddqrLWY= github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= diff --git a/internal/certapply/applicators/sp_vultr.go b/internal/certapply/applicators/sp_vultr.go new file mode 100644 index 00000000..6101a023 --- /dev/null +++ b/internal/certapply/applicators/sp_vultr.go @@ -0,0 +1,29 @@ +package applicators + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/vultr" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeVultr, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForVultr{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := vultr.NewChallengeProvider(&vultr.ChallengeProviderConfig{ + ApiKey: credentials.ApiKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }); err != nil { + panic(err) + } +} diff --git a/internal/certapply/config.go b/internal/certapply/config.go index 8b36d778..d47753fb 100644 --- a/internal/certapply/config.go +++ b/internal/certapply/config.go @@ -15,6 +15,7 @@ import ( var acmeDirUrls = map[string]string{ string(domain.CAProviderTypeLetsEncrypt): "https://acme-v02.api.letsencrypt.org/directory", string(domain.CAProviderTypeLetsEncryptStaging): "https://acme-staging-v02.api.letsencrypt.org/directory", + string(domain.CAProviderTypeActalisSSL): "https://acme-api.actalis.com/acme/directory", string(domain.CAProviderTypeBuypass): "https://api.buypass.com/acme/directory", string(domain.CAProviderTypeGoogleTrustServices): "https://dv.acme-v02.api.pki.goog/directory", string(domain.CAProviderTypeSSLCom): "https://acme.ssl.com/sslcom-dv-rsa", diff --git a/internal/certdeploy/deployers/sp_aliyun_cdn.go b/internal/certdeploy/deployers/sp_aliyun_cdn.go index cd446f50..3301adaf 100644 --- a/internal/certdeploy/deployers/sp_aliyun_cdn.go +++ b/internal/certdeploy/deployers/sp_aliyun_cdn.go @@ -20,6 +20,7 @@ func init() { AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err diff --git a/internal/certdeploy/deployers/sp_aliyund_cdn.go b/internal/certdeploy/deployers/sp_aliyun_dcdn.go similarity index 92% rename from internal/certdeploy/deployers/sp_aliyund_cdn.go rename to internal/certdeploy/deployers/sp_aliyun_dcdn.go index 2a8279b7..8725d0ef 100644 --- a/internal/certdeploy/deployers/sp_aliyund_cdn.go +++ b/internal/certdeploy/deployers/sp_aliyun_dcdn.go @@ -20,6 +20,7 @@ func init() { AccessKeyId: credentials.AccessKeyId, AccessKeySecret: credentials.AccessKeySecret, ResourceGroupId: credentials.ResourceGroupId, + Region: xmaps.GetString(options.ProviderExtendedConfig, "region"), Domain: xmaps.GetString(options.ProviderExtendedConfig, "domain"), }) return provider, err diff --git a/internal/certdeploy/deployers/sp_kubernetes_secret.go b/internal/certdeploy/deployers/sp_kubernetes_secret.go index 85d6fc05..8fe56574 100644 --- a/internal/certdeploy/deployers/sp_kubernetes_secret.go +++ b/internal/certdeploy/deployers/sp_kubernetes_secret.go @@ -2,6 +2,7 @@ package deployers import ( "fmt" + "strings" "github.com/certimate-go/certimate/internal/domain" "github.com/certimate-go/certimate/pkg/core" @@ -16,6 +17,50 @@ func init() { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } + parseKeyValueMap := func(s string) (map[string]string, error) { + result := make(map[string]string) + + lines := strings.Split(s, "\n") + for i, line := range lines { + if strings.TrimSpace(line) == "" { + continue + } + + pos := strings.Index(line, ":") + if pos == -1 { + return nil, fmt.Errorf("invalid line format at line %d", i+1) + } + + key := strings.TrimSpace(line[:pos]) + value := strings.TrimSpace(line[pos+1:]) + if key == "" { + return nil, fmt.Errorf("invalid key at line %d", i+1) + } + + result[key] = value + } + + return result, nil + } + + secretAnnotations := make(map[string]string) + if secretAnnotationsString := xmaps.GetString(options.ProviderExtendedConfig, "secretAnnotations"); secretAnnotationsString != "" { + temp, err := parseKeyValueMap(secretAnnotationsString) + if err != nil { + return nil, fmt.Errorf("failed to parse kubernetes secret annotations: %w", err) + } + secretAnnotations = temp + } + + secretLabels := make(map[string]string) + if secretLabelsString := xmaps.GetString(options.ProviderExtendedConfig, "secretLabels"); secretLabelsString != "" { + temp, err := parseKeyValueMap(secretLabelsString) + if err != nil { + return nil, fmt.Errorf("failed to parse kubernetes secret labels: %w", err) + } + secretLabels = temp + } + provider, err := k8ssecret.NewSSLDeployerProvider(&k8ssecret.SSLDeployerProviderConfig{ KubeConfig: credentials.KubeConfig, Namespace: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "namespace", "default"), @@ -23,6 +68,8 @@ func init() { SecretType: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretType", "kubernetes.io/tls"), SecretDataKeyForCrt: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretDataKeyForCrt", "tls.crt"), SecretDataKeyForKey: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "secretDataKeyForKey", "tls.key"), + SecretAnnotations: secretAnnotations, + SecretLabels: secretLabels, }) return provider, err }); err != nil { diff --git a/internal/certdeploy/deployers/sp_webhook.go b/internal/certdeploy/deployers/sp_webhook.go index 271fc744..c53ff730 100644 --- a/internal/certdeploy/deployers/sp_webhook.go +++ b/internal/certdeploy/deployers/sp_webhook.go @@ -43,6 +43,7 @@ func init() { WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString), Method: credentials.Method, Headers: mergedHeaders, + Timeout: xmaps.GetInt32(options.ProviderExtendedConfig, "timeout"), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err diff --git a/internal/domain/access.go b/internal/domain/access.go index d43462ed..ceb569b8 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -44,6 +44,10 @@ type AccessConfigForACMEHttpReq struct { Password string `json:"password,omitempty"` } +type AccessConfigForActalisSSL struct { + AccessConfigForACMEExternalAccountBinding +} + type AccessConfigForAliyun struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` @@ -402,6 +406,10 @@ type AccessConfigForVolcEngine struct { SecretAccessKey string `json:"secretAccessKey"` } +type AccessConfigForVultr struct { + ApiKey string `json:"apiKey"` +} + type AccessConfigForWangsu struct { AccessKeyId string `json:"accessKeyId"` AccessKeySecret string `json:"accessKeySecret"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index e35919a7..fec6d3bf 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -13,6 +13,7 @@ const ( AccessProviderTypeACMECA = AccessProviderType("acmeca") AccessProviderTypeACMEDNS = AccessProviderType("acmedns") AccessProviderTypeACMEHttpReq = AccessProviderType("acmehttpreq") + AccessProviderTypeActalisSSL = AccessProviderType("actalisssl") AccessProviderTypeAkamai = AccessProviderType("akamai") // Akamai(预留) AccessProviderTypeAliyun = AccessProviderType("aliyun") AccessProviderTypeAPISIX = AccessProviderType("apisix") @@ -86,6 +87,7 @@ const ( AccessProviderTypeUpyun = AccessProviderType("upyun") AccessProviderTypeVercel = AccessProviderType("vercel") AccessProviderTypeVolcEngine = AccessProviderType("volcengine") + AccessProviderTypeVultr = AccessProviderType("vultr") AccessProviderTypeWangsu = AccessProviderType("wangsu") AccessProviderTypeWebhook = AccessProviderType("webhook") AccessProviderTypeWeComBot = AccessProviderType("wecombot") @@ -104,6 +106,7 @@ NOTICE: If you add new constant, please keep ASCII order. */ const ( CAProviderTypeACMECA = CAProviderType(AccessProviderTypeACMECA) + CAProviderTypeActalisSSL = CAProviderType(AccessProviderTypeActalisSSL) CAProviderTypeBuypass = CAProviderType(AccessProviderTypeBuypass) CAProviderTypeGoogleTrustServices = CAProviderType(AccessProviderTypeGoogleTrustServices) CAProviderTypeLetsEncrypt = CAProviderType(AccessProviderTypeLetsEncrypt) @@ -171,6 +174,7 @@ const ( ACMEDns01ProviderTypeVercel = ACMEDns01ProviderType(AccessProviderTypeVercel) ACMEDns01ProviderTypeVolcEngine = ACMEDns01ProviderType(AccessProviderTypeVolcEngine) // 兼容旧值,等同于 [ACMEDns01ProviderTypeVolcEngineDNS] ACMEDns01ProviderTypeVolcEngineDNS = ACMEDns01ProviderType(AccessProviderTypeVolcEngine + "-dns") + ACMEDns01ProviderTypeVultr = ACMEDns01ProviderType(AccessProviderTypeVultr) ACMEDns01ProviderTypeWestcn = ACMEDns01ProviderType(AccessProviderTypeWestcn) ) diff --git a/internal/notify/notifiers/sp_webhook.go b/internal/notify/notifiers/sp_webhook.go index db0c4412..7cacd0a6 100644 --- a/internal/notify/notifiers/sp_webhook.go +++ b/internal/notify/notifiers/sp_webhook.go @@ -43,6 +43,7 @@ func init() { WebhookData: xmaps.GetOrDefaultString(options.ProviderExtendedConfig, "webhookData", credentials.DataString), Method: credentials.Method, Headers: mergedHeaders, + Timeout: xmaps.GetInt32(options.ProviderExtendedConfig, "timeout"), AllowInsecureConnections: credentials.AllowInsecureConnections, }) return provider, err diff --git a/internal/workflow/service.go b/internal/workflow/service.go index adf2a39e..d869a1c0 100644 --- a/internal/workflow/service.go +++ b/internal/workflow/service.go @@ -111,7 +111,9 @@ func (s *WorkflowService) StartRun(ctx context.Context, req *dtos.WorkflowStartR workflowRun = resp } - s.dispatcher.Start(ctx, workflowRun.Id) + if err := s.dispatcher.Start(ctx, workflowRun.Id); err != nil { + return nil, err + } return &dtos.WorkflowStartRunResp{RunId: workflowRun.Id}, nil } @@ -131,7 +133,9 @@ func (s *WorkflowService) CancelRun(ctx context.Context, req *dtos.WorkflowCance return nil, errors.New("workflow run is not pending or processing") } - s.dispatcher.Cancel(ctx, workflowRun.Id) + if err := s.dispatcher.Cancel(ctx, workflowRun.Id); err != nil { + return nil, err + } return &dtos.WorkflowCancelRunResp{}, nil } diff --git a/pkg/core/notifier/providers/webhook/webhook.go b/pkg/core/notifier/providers/webhook/webhook.go index 09ae91e3..3f57da7d 100644 --- a/pkg/core/notifier/providers/webhook/webhook.go +++ b/pkg/core/notifier/providers/webhook/webhook.go @@ -27,6 +27,9 @@ type NotifierProviderConfig struct { Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` + // 请求超时(单位:秒)。 + // 零值时默认值 30。 + Timeout int32 `json:"timeout,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } @@ -48,6 +51,9 @@ func NewNotifierProvider(config *NotifierProviderConfig) (*NotifierProvider, err SetTimeout(30 * time.Second). SetRetryCount(3). SetRetryWaitTime(5 * time.Second) + if config.Timeout > 0 { + client.SetTimeout(time.Duration(config.Timeout) * time.Second) + } if config.AllowInsecureConnections { client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) } diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/vultr/vultr.go b/pkg/core/ssl-applicator/acme-dns01/providers/vultr/vultr.go new file mode 100644 index 00000000..c3badc34 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/vultr/vultr.go @@ -0,0 +1,38 @@ +package vultr + +import ( + "errors" + "time" + + "github.com/go-acme/lego/v4/providers/dns/vultr" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ChallengeProviderConfig struct { + ApiKey string `json:"apiKey"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := vultr.NewDefaultConfig() + providerConfig.APIKey = config.ApiKey + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := vultr.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/pkg/core/ssl-deployer/providers/aliyun-cdn/aliyun_cdn.go b/pkg/core/ssl-deployer/providers/aliyun-cdn/aliyun_cdn.go index a59924d7..00a541d7 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-cdn/aliyun_cdn.go +++ b/pkg/core/ssl-deployer/providers/aliyun-cdn/aliyun_cdn.go @@ -5,13 +5,16 @@ import ( "errors" "fmt" "log/slog" + "strconv" "strings" - "time" alicdn "github.com/alibabacloud-go/cdn-20180510/v5/client" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/tea" + "github.com/samber/lo" + "github.com/certimate-go/certimate/pkg/core" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" ) type SSLDeployerProviderConfig struct { @@ -21,14 +24,17 @@ type SSLDeployerProviderConfig struct { AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` + // 阿里云地域。 + Region string `json:"region"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type SSLDeployerProvider struct { - config *SSLDeployerProviderConfig - logger *slog.Logger - sdkClient *alicdn.Client + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *alicdn.Client + sslManager core.SSLManager } var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) @@ -43,10 +49,23 @@ func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProv return nil, fmt.Errorf("could not create sdk client: %w", err) } + sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, + Region: lo. + If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). + Else("ap-southeast-1"), + }) + if err != nil { + return nil, fmt.Errorf("could not create ssl manager: %w", err) + } + return &SSLDeployerProvider{ - config: config, - logger: slog.Default(), - sdkClient: client, + config: config, + logger: slog.Default(), + sdkClient: client, + sslManager: sslmgr, }, nil } @@ -59,18 +78,32 @@ func (d *SSLDeployerProvider) SetLogger(logger *slog.Logger) { } func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privkeyPEM string) (*core.SSLDeployResult, error) { + if d.config.Domain == "" { + return nil, errors.New("config `domain` is required") + } + + // 上传证书 + upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + // "*.example.com" → ".example.com",适配阿里云 CDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") // 设置 CDN 域名域名证书 // REF: https://help.aliyun.com/zh/cdn/developer-reference/api-cdn-2018-05-10-setcdndomainsslcertificate + certId, _ := strconv.ParseInt(upres.CertId, 10, 64) setCdnDomainSSLCertificateReq := &alicdn.SetCdnDomainSSLCertificateRequest{ - DomainName: tea.String(domain), - CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), - CertType: tea.String("upload"), + DomainName: tea.String(domain), + CertType: tea.String("cas"), + CertId: tea.Int64(int64(certId)), + CertRegion: lo. + If(d.config.Region == "" || strings.HasPrefix(d.config.Region, "cn-"), tea.String("cn-hangzhou")). + Else(tea.String("ap-southeast-1")), SSLProtocol: tea.String("on"), - SSLPub: tea.String(certPEM), - SSLPri: tea.String(privkeyPEM), } setCdnDomainSSLCertificateResp, err := d.sdkClient.SetCdnDomainSSLCertificate(setCdnDomainSSLCertificateReq) d.logger.Debug("sdk request 'cdn.SetCdnDomainSSLCertificate'", slog.Any("request", setCdnDomainSSLCertificateReq), slog.Any("response", setCdnDomainSSLCertificateResp)) diff --git a/pkg/core/ssl-deployer/providers/aliyun-dcdn/aliyun_dcdn.go b/pkg/core/ssl-deployer/providers/aliyun-dcdn/aliyun_dcdn.go index 6118828f..a599e397 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-dcdn/aliyun_dcdn.go +++ b/pkg/core/ssl-deployer/providers/aliyun-dcdn/aliyun_dcdn.go @@ -5,13 +5,16 @@ import ( "errors" "fmt" "log/slog" + "strconv" "strings" - "time" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" alidcdn "github.com/alibabacloud-go/dcdn-20180115/v3/client" "github.com/alibabacloud-go/tea/tea" + "github.com/samber/lo" + "github.com/certimate-go/certimate/pkg/core" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" ) type SSLDeployerProviderConfig struct { @@ -21,14 +24,17 @@ type SSLDeployerProviderConfig struct { AccessKeySecret string `json:"accessKeySecret"` // 阿里云资源组 ID。 ResourceGroupId string `json:"resourceGroupId,omitempty"` + // 阿里云地域。 + Region string `json:"region"` // 加速域名(支持泛域名)。 Domain string `json:"domain"` } type SSLDeployerProvider struct { - config *SSLDeployerProviderConfig - logger *slog.Logger - sdkClient *alidcdn.Client + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *alidcdn.Client + sslManager core.SSLManager } var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) @@ -43,10 +49,23 @@ func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProv return nil, fmt.Errorf("could not create sdk client: %w", err) } + sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, + Region: lo. + If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). + Else("ap-southeast-1"), + }) + if err != nil { + return nil, fmt.Errorf("could not create ssl manager: %w", err) + } + return &SSLDeployerProvider{ - config: config, - logger: slog.Default(), - sdkClient: client, + config: config, + logger: slog.Default(), + sdkClient: client, + sslManager: sslmgr, }, nil } @@ -63,18 +82,28 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return nil, errors.New("config `domain` is required") } + // 上传证书 + upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + // "*.example.com" → ".example.com",适配阿里云 DCDN 要求的泛域名格式 domain := strings.TrimPrefix(d.config.Domain, "*") // 配置域名证书 // REF: https://help.aliyun.com/zh/edge-security-acceleration/dcdn/developer-reference/api-dcdn-2018-01-15-setdcdndomainsslcertificate + certId, _ := strconv.ParseInt(upres.CertId, 10, 64) setDcdnDomainSSLCertificateReq := &alidcdn.SetDcdnDomainSSLCertificateRequest{ - DomainName: tea.String(domain), - CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), - CertType: tea.String("upload"), + DomainName: tea.String(domain), + CertType: tea.String("cas"), + CertId: tea.Int64(int64(certId)), + CertRegion: lo. + If(d.config.Region == "" || strings.HasPrefix(d.config.Region, "cn-"), tea.String("cn-hangzhou")). + Else(tea.String("ap-southeast-1")), SSLProtocol: tea.String("on"), - SSLPub: tea.String(certPEM), - SSLPri: tea.String(privkeyPEM), } setDcdnDomainSSLCertificateResp, err := d.sdkClient.SetDcdnDomainSSLCertificate(setDcdnDomainSSLCertificateReq) d.logger.Debug("sdk request 'dcdn.SetDcdnDomainSSLCertificate'", slog.Any("request", setDcdnDomainSSLCertificateReq), slog.Any("response", setDcdnDomainSSLCertificateResp)) diff --git a/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go b/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go index 12f8ceec..dec81ee7 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go +++ b/pkg/core/ssl-deployer/providers/aliyun-ddos/aliyun_ddos.go @@ -94,7 +94,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // 为网站业务转发规则关联 SSL 证书 // REF: https://help.aliyun.com/zh/anti-ddos/anti-ddos-pro-and-premium/developer-reference/api-ddoscoo-2020-01-01-associatewebcert - certId, _ := strconv.Atoi(upres.CertId) + certId, _ := strconv.ParseInt(upres.CertId, 10, 32) associateWebCertReq := &aliddos.AssociateWebCertRequest{ Domain: tea.String(d.config.Domain), CertId: tea.Int32(int32(certId)), diff --git a/pkg/core/ssl-deployer/providers/aliyun-vod/aliyun_vod.go b/pkg/core/ssl-deployer/providers/aliyun-vod/aliyun_vod.go index 08173368..07ea9c10 100644 --- a/pkg/core/ssl-deployer/providers/aliyun-vod/aliyun_vod.go +++ b/pkg/core/ssl-deployer/providers/aliyun-vod/aliyun_vod.go @@ -5,12 +5,16 @@ import ( "errors" "fmt" "log/slog" - "time" + "strconv" + "strings" aliopen "github.com/alibabacloud-go/darabonba-openapi/v2/client" "github.com/alibabacloud-go/tea/tea" alivod "github.com/alibabacloud-go/vod-20170321/v4/client" + "github.com/samber/lo" + "github.com/certimate-go/certimate/pkg/core" + sslmgrsp "github.com/certimate-go/certimate/pkg/core/ssl-manager/providers/aliyun-cas" ) type SSLDeployerProviderConfig struct { @@ -27,9 +31,10 @@ type SSLDeployerProviderConfig struct { } type SSLDeployerProvider struct { - config *SSLDeployerProviderConfig - logger *slog.Logger - sdkClient *alivod.Client + config *SSLDeployerProviderConfig + logger *slog.Logger + sdkClient *alivod.Client + sslManager core.SSLManager } var _ core.SSLDeployer = (*SSLDeployerProvider)(nil) @@ -44,10 +49,23 @@ func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProv return nil, fmt.Errorf("could not create sdk client: %w", err) } + sslmgr, err := sslmgrsp.NewSSLManagerProvider(&sslmgrsp.SSLManagerProviderConfig{ + AccessKeyId: config.AccessKeyId, + AccessKeySecret: config.AccessKeySecret, + ResourceGroupId: config.ResourceGroupId, + Region: lo. + If(config.Region == "" || strings.HasPrefix(config.Region, "cn-"), "cn-hangzhou"). + Else("ap-southeast-1"), + }) + if err != nil { + return nil, fmt.Errorf("could not create ssl manager: %w", err) + } + return &SSLDeployerProvider{ - config: config, - logger: slog.Default(), - sdkClient: client, + config: config, + logger: slog.Default(), + sdkClient: client, + sslManager: sslmgr, }, nil } @@ -64,15 +82,25 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke return nil, errors.New("config `domain` is required") } + // 上传证书 + upres, err := d.sslManager.Upload(ctx, certPEM, privkeyPEM) + if err != nil { + return nil, fmt.Errorf("failed to upload certificate file: %w", err) + } else { + d.logger.Info("ssl certificate uploaded", slog.Any("result", upres)) + } + // 设置域名证书 // REF: https://help.aliyun.com/zh/vod/developer-reference/api-vod-2017-03-21-setvoddomainsslcertificate + certId, _ := strconv.ParseInt(upres.CertId, 10, 64) setVodDomainSSLCertificateReq := &alivod.SetVodDomainSSLCertificateRequest{ - DomainName: tea.String(d.config.Domain), - CertName: tea.String(fmt.Sprintf("certimate-%d", time.Now().UnixMilli())), - CertType: tea.String("upload"), + DomainName: tea.String(d.config.Domain), + CertType: tea.String("cas"), + CertId: tea.Int64(int64(certId)), + CertRegion: lo. + If(d.config.Region == "" || strings.HasPrefix(d.config.Region, "cn-"), tea.String("cn-hangzhou")). + Else(tea.String("ap-southeast-1")), SSLProtocol: tea.String("on"), - SSLPub: tea.String(certPEM), - SSLPri: tea.String(privkeyPEM), } setVodDomainSSLCertificateResp, err := d.sdkClient.SetVodDomainSSLCertificate(setVodDomainSSLCertificateReq) d.logger.Debug("sdk request 'live.SetVodDomainSSLCertificate'", slog.Any("request", setVodDomainSSLCertificateReq), slog.Any("response", setVodDomainSSLCertificateResp)) diff --git a/pkg/core/ssl-deployer/providers/baotawaf-site/baotawaf_site.go b/pkg/core/ssl-deployer/providers/baotawaf-site/baotawaf_site.go index 6659c62d..d1408d9e 100644 --- a/pkg/core/ssl-deployer/providers/baotawaf-site/baotawaf_site.go +++ b/pkg/core/ssl-deployer/providers/baotawaf-site/baotawaf_site.go @@ -114,8 +114,8 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke modifySiteReq := &btwafsdk.ModifySiteRequest{ SiteId: lo.ToPtr(siteId), Type: lo.ToPtr("openCert"), - Server: &btwafsdk.SiteServerInfo{ - ListenSSLPorts: lo.ToPtr([]int32{d.config.SitePort}), + Server: &btwafsdk.SiteServerInfoMod{ + ListenSSLPorts: lo.ToPtr([]string{fmt.Sprintf("%d", d.config.SitePort)}), SSL: &btwafsdk.SiteServerSSLInfo{ IsSSL: lo.ToPtr(int32(1)), FullChain: lo.ToPtr(certPEM), diff --git a/pkg/core/ssl-deployer/providers/k8s-secret/k8s_secret.go b/pkg/core/ssl-deployer/providers/k8s-secret/k8s_secret.go index e9a16683..b90db949 100644 --- a/pkg/core/ssl-deployer/providers/k8s-secret/k8s_secret.go +++ b/pkg/core/ssl-deployer/providers/k8s-secret/k8s_secret.go @@ -30,6 +30,10 @@ type SSLDeployerProviderConfig struct { SecretDataKeyForCrt string `json:"secretDataKeyForCrt,omitempty"` // Kubernetes Secret 中用于存放私钥的 Key。 SecretDataKeyForKey string `json:"secretDataKeyForKey,omitempty"` + // Kubernetes Secret 注解。 + SecretAnnotations map[string]string `json:"secretAnnotations,omitempty"` + // Kubernetes Secret 标签。 + SecretLabels map[string]string `json:"secretLabels,omitempty"` } type SSLDeployerProvider struct { @@ -94,6 +98,17 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke "certimate/issuer-sn": certX509.Issuer.SerialNumber, "certimate/issuer-org": strings.Join(certX509.Issuer.Organization, ","), } + secretLabels := map[string]string{} + if d.config.SecretAnnotations != nil { + for k, v := range d.config.SecretAnnotations { + secretAnnotations[k] = v + } + } + if d.config.SecretLabels != nil { + for k, v := range d.config.SecretLabels { + secretLabels[k] = v + } + } // 获取 Secret 实例,如果不存在则创建 secretPayload, err = client.CoreV1().Secrets(d.config.Namespace).Get(context.TODO(), d.config.SecretName, k8smeta.GetOptions{}) @@ -106,6 +121,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke ObjectMeta: k8smeta.ObjectMeta{ Name: d.config.SecretName, Annotations: secretAnnotations, + Labels: secretLabels, }, Type: k8score.SecretType(d.config.SecretType), } @@ -131,6 +147,13 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke secretPayload.ObjectMeta.Annotations[k] = v } } + if secretPayload.ObjectMeta.Labels == nil { + secretPayload.ObjectMeta.Labels = secretLabels + } else { + for k, v := range secretLabels { + secretPayload.ObjectMeta.Labels[k] = v + } + } if secretPayload.Data == nil { secretPayload.Data = make(map[string][]byte) } diff --git a/pkg/core/ssl-deployer/providers/rainyun-rcdn/rainyun_rcdn.go b/pkg/core/ssl-deployer/providers/rainyun-rcdn/rainyun_rcdn.go index 8af0fa06..1accc617 100644 --- a/pkg/core/ssl-deployer/providers/rainyun-rcdn/rainyun_rcdn.go +++ b/pkg/core/ssl-deployer/providers/rainyun-rcdn/rainyun_rcdn.go @@ -80,7 +80,7 @@ func (d *SSLDeployerProvider) Deploy(ctx context.Context, certPEM string, privke // RCDN SSL 绑定域名 // REF: https://apifox.com/apidoc/shared/a4595cc8-44c5-4678-a2a3-eed7738dab03/api-184214120 - certId, _ := strconv.Atoi(upres.CertId) + certId, _ := strconv.ParseInt(upres.CertId, 10, 32) rcdnInstanceSslBindReq := &rainyunsdk.RcdnInstanceSslBindRequest{ CertId: int32(certId), Domains: []string{d.config.Domain}, diff --git a/pkg/core/ssl-deployer/providers/webhook/webhook.go b/pkg/core/ssl-deployer/providers/webhook/webhook.go index 2bf1aa14..9de2d8fc 100644 --- a/pkg/core/ssl-deployer/providers/webhook/webhook.go +++ b/pkg/core/ssl-deployer/providers/webhook/webhook.go @@ -28,6 +28,9 @@ type SSLDeployerProviderConfig struct { Method string `json:"method,omitempty"` // 请求标头。 Headers map[string]string `json:"headers,omitempty"` + // 请求超时(单位:秒)。 + // 零值时默认值 30。 + Timeout int32 `json:"timeout,omitempty"` // 是否允许不安全的连接。 AllowInsecureConnections bool `json:"allowInsecureConnections,omitempty"` } @@ -49,6 +52,9 @@ func NewSSLDeployerProvider(config *SSLDeployerProviderConfig) (*SSLDeployerProv SetTimeout(30 * time.Second). SetRetryCount(3). SetRetryWaitTime(5 * time.Second) + if config.Timeout > 0 { + client.SetTimeout(time.Duration(config.Timeout) * time.Second) + } if config.AllowInsecureConnections { client.SetTLSClientConfig(&tls.Config{InsecureSkipVerify: true}) } diff --git a/pkg/sdk3rd/btwaf/api_modify_site.go b/pkg/sdk3rd/btwaf/api_modify_site.go index 516277ec..a93f8ba8 100644 --- a/pkg/sdk3rd/btwaf/api_modify_site.go +++ b/pkg/sdk3rd/btwaf/api_modify_site.go @@ -6,9 +6,9 @@ import ( ) type ModifySiteRequest struct { - SiteId *string `json:"site_id,omitempty"` - Type *string `json:"types,omitempty"` - Server *SiteServerInfo `json:"server,omitempty"` + SiteId *string `json:"site_id,omitempty"` + Type *string `json:"types,omitempty"` + Server *SiteServerInfoMod `json:"server,omitempty"` } type ModifySiteResponse struct { diff --git a/pkg/sdk3rd/btwaf/types.go b/pkg/sdk3rd/btwaf/types.go index 8c75a44b..b1c26de3 100644 --- a/pkg/sdk3rd/btwaf/types.go +++ b/pkg/sdk3rd/btwaf/types.go @@ -32,6 +32,11 @@ type SiteServerInfo struct { SSL *SiteServerSSLInfo `json:"ssl,omitempty"` } +type SiteServerInfoMod struct { + ListenSSLPorts *[]string `json:"listen_ssl_port,omitempty"` + SSL *SiteServerSSLInfo `json:"ssl,omitempty"` +} + type SiteServerSSLInfo struct { IsSSL *int32 `json:"is_ssl,omitempty"` FullChain *string `json:"full_chain,omitempty"` diff --git a/ui/public/imgs/providers/actalisssl.png b/ui/public/imgs/providers/actalisssl.png new file mode 100644 index 00000000..3897c6b4 Binary files /dev/null and b/ui/public/imgs/providers/actalisssl.png differ diff --git a/ui/public/imgs/providers/vultr.svg b/ui/public/imgs/providers/vultr.svg new file mode 100644 index 00000000..cf49d15d --- /dev/null +++ b/ui/public/imgs/providers/vultr.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 976434ae..69112aa5 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -15,6 +15,7 @@ import AccessConfigFieldsProvider1Panel from "./forms/AccessConfigFieldsProvider import AccessConfigFieldsProviderACMECA from "./forms/AccessConfigFieldsProviderACMECA"; import AccessConfigFieldsProviderACMEDNS from "./forms/AccessConfigFieldsProviderACMEDNS"; import AccessConfigFieldsProviderACMEHttpReq from "./forms/AccessConfigFieldsProviderACMEHttpReq"; +import AccessConfigFieldsProviderActalisSSL from "./forms/AccessConfigFieldsProviderActalisSSL"; import AccessConfigFieldsProviderAliyun from "./forms/AccessConfigFieldsProviderAliyun"; import AccessConfigFieldsProviderAPISIX from "./forms/AccessConfigFieldsProviderAPISIX"; import AccessConfigFieldsProviderAWS from "./forms/AccessConfigFieldsProviderAWS"; @@ -80,6 +81,7 @@ import AccessConfigFieldsProviderUniCloud from "./forms/AccessConfigFieldsProvid import AccessConfigFieldsProviderUpyun from "./forms/AccessConfigFieldsProviderUpyun"; import AccessConfigFieldsProviderVercel from "./forms/AccessConfigFieldsProviderVercel"; import AccessConfigFieldsProviderVolcEngine from "./forms/AccessConfigFieldsProviderVolcEngine"; +import AccessConfigFieldsProviderVultr from "./forms/AccessConfigFieldsProviderVultr"; import AccessConfigFieldsProviderWangsu from "./forms/AccessConfigFieldsProviderWangsu"; import AccessConfigFieldsProviderWebhook from "./forms/AccessConfigFieldsProviderWebhook"; import AccessConfigFieldsProviderWeComBot from "./forms/AccessConfigFieldsProviderWeComBot"; @@ -140,6 +142,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, .. case ACCESS_PROVIDERS.ACMEHTTPREQ: { return ; } + case ACCESS_PROVIDERS.ACTALISSSL: { + return ; + } case ACCESS_PROVIDERS.ALIYUN: { return ; } @@ -335,6 +340,9 @@ const AccessForm = ({ className, style, disabled, initialValues, mode, usage, .. case ACCESS_PROVIDERS.VOLCENGINE: { return ; } + case ACCESS_PROVIDERS.VULTR: { + return ; + } case ACCESS_PROVIDERS.WANGSU: { return ; } diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx new file mode 100644 index 00000000..e0d95932 --- /dev/null +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderActalisSSL.tsx @@ -0,0 +1,70 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const AccessConfigFormFieldsProviderActalisSSL = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + eabKid: "", + eabHmacKey: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + eabKid: z + .string() + .min(1, t("access.form.actalisssl_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + eabHmacKey: z + .string() + .min(1, t("access.form.actalisssl_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderActalisSSL, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/access/forms/AccessConfigFieldsProviderVultr.tsx b/ui/src/components/access/forms/AccessConfigFieldsProviderVultr.tsx new file mode 100644 index 00000000..1ce3e4ea --- /dev/null +++ b/ui/src/components/access/forms/AccessConfigFieldsProviderVultr.tsx @@ -0,0 +1,52 @@ +import { getI18n, useTranslation } from "react-i18next"; +import { Form, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { useFormNestedFieldsContext } from "./_context"; + +const AccessConfigFormFieldsProviderVultr = () => { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const initialValues = getInitialValues(); + + return ( + <> + } + > + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + apiKey: "", + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z.object({ + apiKey: z.string().nonempty(t("access.form.vultr_api_key.placeholder")), + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderVultr, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx index 5ab9cb56..b5609cb9 100644 --- a/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/designer/forms/BizApplyNodeConfigForm.tsx @@ -509,7 +509,7 @@ const BizApplyNodeConfigForm = ({ node, ...props }: BizApplyNodeConfigFormProps) > ({ value }))} + options={["classic", "tlsserver", "shortlived"].map((s) => ({ value: s }))} placeholder={t("workflow_node.apply.form.acme_profile.placeholder")} filterOption={(inputValue, option) => option!.value.toLowerCase().includes(inputValue.toLowerCase())} /> diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx index bb8d68f1..92105b64 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunCDN.tsx @@ -1,5 +1,5 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form, Input } from "antd"; +import { AutoComplete, Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -19,6 +19,20 @@ const BizDeployNodeConfigFieldsProviderAliyunCDN = () => { return ( <> + } + > + ({ value: s }))} + placeholder={t("workflow_node.deploy.form.aliyun_cdn_region.placeholder")} + /> + + { const getInitialValues = (): Nullish>> => { return { + region: "", domain: "", }; }; @@ -42,6 +57,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) const { t } = i18n; return z.object({ + region: z.string().nullish(), domain: z.string().refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx index 4a871f2b..0ec8512e 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderAliyunDCDN.tsx @@ -1,5 +1,5 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form, Input } from "antd"; +import { AutoComplete, Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -19,6 +19,20 @@ const BizDeployNodeConfigFieldsProviderAliyunDCDN = () => { return ( <> + } + > + ({ value: s }))} + placeholder={t("workflow_node.deploy.form.aliyun_dcdn_region.placeholder")} + /> + + { const getInitialValues = (): Nullish>> => { return { + region: "", domain: "", }; }; @@ -42,6 +57,7 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) const { t } = i18n; return z.object({ + region: z.string().nullish(), domain: z.string().refine((v) => validDomainName(v, { allowWildcard: true }), t("common.errmsg.domain_invalid")), }); }; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx index 6730b4d8..c6859b1a 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderKubernetesSecret.tsx @@ -3,6 +3,8 @@ import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; +import CodeInput from "@/components/CodeInput"; + import { useFormNestedFieldsContext } from "./_context"; const BizDeployNodeConfigFieldsProviderKubernetesSecret = () => { @@ -13,8 +15,23 @@ const BizDeployNodeConfigFieldsProviderKubernetesSecret = () => { [parentNamePath]: getSchema({ i18n }), }); const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); const initialValues = getInitialValues(); + const handleSecretAnnotationsBlur = () => { + let value = formInst.getFieldValue([parentNamePath, "secretAnnotations"]); + value = value.trim(); + value = value.replace(/(? { + let value = formInst.getFieldValue([parentNamePath, "secretLabels"]); + value = value.trim(); + value = value.replace(/(? { > + + } + > + + + + } + > + + ); }; @@ -88,6 +139,34 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) secretType: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_type.placeholder")), secretDataKeyForCrt: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_crt.placeholder")), secretDataKeyForKey: z.string().nonempty(t("workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder")), + secretAnnotations: z + .string() + .nullish() + .refine((v) => { + if (!v) return true; + + const lines = v.split(/\r?\n/); + for (const line of lines) { + if (line.split(":").length < 2) { + return false; + } + } + return true; + }, t("workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid")), + secretLabels: z + .string() + .nullish() + .refine((v) => { + if (!v) return true; + + const lines = v.split(/\r?\n/); + for (const line of lines) { + if (line.split(":").length < 2) { + return false; + } + } + return true; + }, t("workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid")), }); }; diff --git a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx index d6d111b9..7c288dca 100644 --- a/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx +++ b/ui/src/components/workflow/designer/forms/BizDeployNodeConfigFieldsProviderWebhook.tsx @@ -1,5 +1,5 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form } from "antd"; +import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -51,6 +51,17 @@ const BizDeployNodeConfigFieldsProviderWebhook = () => { } /> + + + + ); }; @@ -76,6 +87,10 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) return false; } }, t("workflow_node.deploy.form.webhook_data.errmsg.json_invalid")), + timeout: z.preprocess( + (v) => (v == null || v === "" ? void 0 : Number(v)), + z.number().int(t("workflow_node.deploy.form.webhook_timeout.placeholder")).gte(1, t("workflow_node.deploy.form.webhook_timeout.placeholder")).nullish() + ), }); }; diff --git a/ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx b/ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx index 247f568f..0f88205a 100644 --- a/ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx +++ b/ui/src/components/workflow/designer/forms/BizNotifyNodeConfigFieldsProviderWebhook.tsx @@ -1,5 +1,5 @@ import { getI18n, useTranslation } from "react-i18next"; -import { Form } from "antd"; +import { Form, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { z } from "zod"; @@ -51,6 +51,17 @@ const BizNotifyNodeConfigFieldsProviderWebhook = () => { } /> + + + + ); }; @@ -76,6 +87,10 @@ const getSchema = ({ i18n = getI18n() }: { i18n?: ReturnType }) return false; } }, t("workflow_node.notify.form.webhook_data.errmsg.json_invalid")), + timeout: z.preprocess( + (v) => (v == null || v === "" ? void 0 : Number(v)), + z.number().int(t("workflow_node.notify.form.webhook_timeout.placeholder")).gte(1, t("workflow_node.notify.form.webhook_timeout.placeholder")).nullish() + ), }); }; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index aeb875dd..8a934aa9 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -19,6 +19,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ ACMECA: "acmeca", ACMEDNS: "acmedns", ACMEHTTPREQ: "acmehttpreq", + ACTALISSSL: "actalisssl", ALIYUN: "aliyun", APISIX: "apisix", AWS: "aws", @@ -88,6 +89,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ UPYUN: "upyun", VERCEL: "vercel", VOLCENGINE: "volcengine", + VULTR: "vultr", WANGSU: "wangsu", WEBHOOK: "webhook", WECOMBOT: "wecombot", @@ -178,6 +180,7 @@ export const accessProvidersMap: Map = new [ [CA_PROVIDERS.LETSENCRYPT, "builtin"], [CA_PROVIDERS.LETSENCRYPTSTAGING, "builtin"], + [CA_PROVIDERS.ACTALISSSL], [CA_PROVIDERS.BUYPASS], [CA_PROVIDERS.GOOGLETRUSTSERVICES], [CA_PROVIDERS.SSLCOM], @@ -316,6 +322,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({ VERCEL: `${ACCESS_PROVIDERS.VERCEL}`, VOLCENGINE: `${ACCESS_PROVIDERS.VOLCENGINE}`, // 兼容旧值,等同于 `VOLCENGINE_DNS` VOLCENGINE_DNS: `${ACCESS_PROVIDERS.VOLCENGINE}-dns`, + VULTR: `${ACCESS_PROVIDERS.VULTR}`, WESTCN: `${ACCESS_PROVIDERS.WESTCN}`, } as const); @@ -362,6 +369,7 @@ export const acmeDns01ProvidersMap: Maphttps://github.com/joohoi/acme-dns", - "access.form.acmedns_credentials.errmsg.json_invalid": "Please enter a valiod JSON string", + "access.form.acmedns_credentials.errmsg.json_invalid": "Please enter a valid JSON string", "access.form.acmehttpreq_endpoint.label": "Endpoint", "access.form.acmehttpreq_endpoint.placeholder": "Please enter endpoint", "access.form.acmehttpreq_endpoint.tooltip": "For more information, see https://go-acme.github.io/lego/dns/httpreq/", @@ -78,6 +78,12 @@ "access.form.acmehttpreq_password.label": "HTTP Basic Auth password (Optional)", "access.form.acmehttpreq_password.placeholder": "Please enter HTTP Basic Auth password", "access.form.acmehttpreq_password.tooltip": "For more information, see https://go-acme.github.io/lego/dns/httpreq/", + "access.form.actalisssl_eab_kid.label": "ACME EAB KID", + "access.form.actalisssl_eab_kid.placeholder": "Please enter ACME EAB KID", + "access.form.actalisssl_eab_kid.tooltip": "For more information, see https://www.actalis.com/manage-with-acme", + "access.form.actalisssl_eab_hmac_key.label": "ACME EAB HMAC key", + "access.form.actalisssl_eab_hmac_key.placeholder": "Please enter ACME EAB HMAC key", + "access.form.actalisssl_eab_hmac_key.tooltip": "For more information, see https://www.actalis.com/manage-with-acme", "access.form.aliyun_access_key_id.label": "Aliyun AccessKeyId", "access.form.aliyun_access_key_id.placeholder": "Please enter Aliyun AccessKeyId", "access.form.aliyun_access_key_id.tooltip": "For more information, see https://www.alibabacloud.com/help/en/acr/create-and-obtain-an-accesskey-pair", @@ -492,6 +498,9 @@ "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.vultr_api_key.label": "Vultr API key", + "access.form.vultr_api_key.placeholder": "Please enter Vultr API key", + "access.form.vultr_api_key.tooltip": "For more information, see https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key", "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", @@ -509,7 +518,7 @@ "access.form.webhook_headers.placeholder": "Please enter Webhook request headers", "access.form.webhook_headers.errmsg.invalid": "Please enter a valid request headers", "access.form.webhook_headers.tooltip": "Example:
Content-Type: application/json
User-Agent: certimate
", - "access.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string", + "access.form.webhook_data.errmsg.json_invalid": "Please enter a valid JSON string", "access.form.webhook_data.label": "Webhook data (Optional)", "access.form.webhook_data.placeholder": "Please enter the default Webhook data", "access.form.webhook_data.help": "Notes: It can be overrided in the workflows.", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index 593cda3d..49cf3367 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -5,6 +5,7 @@ "provider.acmeca": "ACME Custom CA Endpoint", "provider.acmedns": "ACME-DNS", "provider.acmehttpreq": "ACME Custom HTTP Endpoint", + "provider.actalisssl": "Actalis SSL", "provider.aliyun": "Alibaba Cloud", "provider.aliyun.alb": "Alibaba Cloud - ALB (Application Load Balancer)", "provider.aliyun.apigw": "Alibaba Cloud - API Gateway", @@ -171,6 +172,7 @@ "provider.volcengine.imagex": "Volcengine - ImageX", "provider.volcengine.live": "Volcengine - Live", "provider.volcengine.tos": "Volcengine - TOS (Tinder Object Storage)", + "provider.vultr": "Vultr", "provider.wangsu": "Wangsu Cloud", "provider.wangsu.cdn": "Wangsu Cloud - CDN (Content Delivery Network)", "provider.wangsu.cdnpro": "Wangsu Cloud - CDN Pro (CDN 360)", diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 60ed9ca9..07c523da 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -92,8 +92,8 @@ "workflow_node.apply.form.validity_lifetime.placeholder": "Please enter certificate's validity lifetime", "workflow_node.apply.form.validity_lifetime.help": "Notes: Not all CAs support this feature.", "workflow_node.apply.form.validity_lifetime.tooltip": "It determines the NotAfter field of the certificate in the ACME protocol. If you don't understand this option, just keep it by default.", - "workflow_node.apply.form.validity_lifetime.units.h": "Hour(s)", - "workflow_node.apply.form.validity_lifetime.units.d": "Day(s)", + "workflow_node.apply.form.validity_lifetime.units.h": "hours", + "workflow_node.apply.form.validity_lifetime.units.d": "days", "workflow_node.apply.form.acme_profile.label": "Certificate ACME profile (Optional)", "workflow_node.apply.form.acme_profile.placeholder": "Please enter certificate's ACME profile", "workflow_node.apply.form.acme_profile.help": "Notes: Not all CAs support this feature.", @@ -256,9 +256,15 @@ "workflow_node.deploy.form.aliyun_clb_snidomain.placeholder": "Please enter Alibaba Cloud CLB SNI domain name", "workflow_node.deploy.form.aliyun_clb_snidomain.help": "", "workflow_node.deploy.form.aliyun_clb_snidomain.tooltip": "For more information, see https://slb.console.aliyun.com/clb", + "workflow_node.deploy.form.aliyun_cdn_region.label": "Alibaba Cloud CDN region", + "workflow_node.deploy.form.aliyun_cdn_region.placeholder": "Please enter Alibaba Cloud CDN region (e.g. cn-hangzhou)", + "workflow_node.deploy.form.aliyun_cdn_region.tooltip": "
  • ap-southeast-1 for Alibaba Cloud International
  • cn-hangzhou for Alibaba Cloud in China
", "workflow_node.deploy.form.aliyun_cdn_domain.label": "Alibaba Cloud CDN domain", "workflow_node.deploy.form.aliyun_cdn_domain.placeholder": "Please enter Alibaba Cloud CDN domain name", "workflow_node.deploy.form.aliyun_cdn_domain.tooltip": "For more information, see https://cdn.console.aliyun.com", + "workflow_node.deploy.form.aliyun_dcdn_region.label": "Alibaba Cloud DCDN region", + "workflow_node.deploy.form.aliyun_dcdn_region.placeholder": "Please enter Alibaba Cloud DCDN region (e.g. cn-hangzhou)", + "workflow_node.deploy.form.aliyun_dcdn_region.tooltip": "
  • ap-southeast-1 for Alibaba Cloud International
  • cn-hangzhou for Alibaba Cloud in China
", "workflow_node.deploy.form.aliyun_dcdn_domain.label": "Alibaba Cloud DCDN domain", "workflow_node.deploy.form.aliyun_dcdn_domain.placeholder": "Please enter Alibaba Cloud DCDN domain name", "workflow_node.deploy.form.aliyun_dcdn_domain.tooltip": "For more information, see https://dcdn.console.aliyun.com", @@ -588,6 +594,16 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "Please enter Kubernetes Secret data key for private key", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "For more information, see https://kubernetes.io/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.k8s_secret_annotations.label": "Kubernetes Secret annotations (Optional)", + "workflow_node.deploy.form.k8s_secret_annotations.placeholder": "Please enter Kubernetes Secret annotations", + "workflow_node.deploy.form.k8s_secret_annotations.help": "Notes: One key value pair per line, separated by colon.", + "workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid": "Please enter a valid annotations", + "workflow_node.deploy.form.k8s_secret_annotations.tooltip": "Example:
environment: production
app: nginx
", + "workflow_node.deploy.form.k8s_secret_labels.label": "Kubernetes Secret labels (Optional)", + "workflow_node.deploy.form.k8s_secret_labels.placeholder": "Please enter Kubernetes Secret labels", + "workflow_node.deploy.form.k8s_secret_labels.help": "Notes: One key value pair per line, separated by colon.", + "workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid": "Please enter a valid labels", + "workflow_node.deploy.form.k8s_secret_labels.tooltip": "Example:
environment: production
app: nginx
", "workflow_node.deploy.form.kong_resource_type.label": "Resource type", "workflow_node.deploy.form.kong_resource_type.placeholder": "Please select resource type", "workflow_node.deploy.form.kong_resource_type.option.certificate.label": "SSL certificate", @@ -996,7 +1012,10 @@ "workflow_node.deploy.form.webhook_data.placeholder": "Please enter Webhook data", "workflow_node.deploy.form.webhook_data.help": "Notes: Leave it blank to use the default Webhook data provided by the credential.", "workflow_node.deploy.form.webhook_data.guide": "
Supported variables:
  1. ${DOMAIN}: The primary domain of the certificate (CommonName).
  2. ${DOMAINS}: The domain list of the certificate (SubjectAltNames).
  3. ${CERTIFICATE}: The PEM format content of the certificate file.
  4. ${SERVER_CERTIFICATE}: The PEM format content of the server certificate file.
  5. ${INTERMEDIA_CERTIFICATE}: The PEM format content of the intermediate CA certificate file.
  6. ${PRIVATE_KEY}: The PEM format content of the private key file.

Please visit the credentials page for addtional notes.", - "workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string", + "workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "Please enter a valid JSON string", + "workflow_node.deploy.form.webhook_timeout.label": "Webhook timeout (Optional)", + "workflow_node.deploy.form.webhook_timeout.placeholder": "Please enter Webhook timeout", + "workflow_node.deploy.form.webhook_timeout.unit": "seconds", "workflow_node.deploy.form.skip_on_last_succeeded.label": "Repeated deployment", "workflow_node.deploy.form.skip_on_last_succeeded.prefix": "If the last deployment was successful, ", "workflow_node.deploy.form.skip_on_last_succeeded.suffix": " to re-deploy.", @@ -1041,7 +1060,10 @@ "workflow_node.notify.form.webhook_data.placeholder": "Please enter Webhook data", "workflow_node.notify.form.webhook_data.help": "Notes: Leave it blank to use the default Webhook data provided by the credential.", "workflow_node.notify.form.webhook_data.guide": "
Supported variables:
  1. ${SUBJECT}: The subject of notification.
  2. ${MESSAGE}: The message of notification.

Please visit the credentials page for addtional notes.", - "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valiod JSON string", + "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "Please enter a valid JSON string", + "workflow_node.notify.form.webhook_timeout.label": "Webhook timeout (Optional)", + "workflow_node.notify.form.webhook_timeout.placeholder": "Please enter Webhook timeout", + "workflow_node.notify.form.webhook_timeout.unit": "seconds", "workflow_node.notify.form.skip_on_all_prev_skipped.label": "Silent behavior", "workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "If all the previous nodes were skipped, ", "workflow_node.notify.form.skip_on_all_prev_skipped.suffix": " to notify.", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 9aef0305..1cda5e6b 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -77,6 +77,12 @@ "access.form.acmehttpreq_password.label": "HTTP 基本认证密码(可选)", "access.form.acmehttpreq_password.placeholder": "请输入 HTTP 基本认证密码", "access.form.acmehttpreq_password.tooltip": "这是什么?请参阅 https://go-acme.github.io/lego/dns/httpreq/", + "access.form.actalisssl_eab_kid.label": "ACME EAB KID", + "access.form.actalisssl_eab_kid.placeholder": "请输入 ACME EAB KID", + "access.form.actalisssl_eab_kid.tooltip": "这是什么?请参阅 https://www.actalis.com/manage-with-acme", + "access.form.actalisssl_eab_hmac_key.label": "ACME EAB HMAC Key", + "access.form.actalisssl_eab_hmac_key.placeholder": "请输入 ACME EAB HMAC Key", + "access.form.actalisssl_eab_hmac_key.tooltip": "这是什么?请参阅 https://www.actalis.com/manage-with-acme", "access.form.aliyun_access_key_id.label": "阿里云 AccessKeyId", "access.form.aliyun_access_key_id.placeholder": "请输入阿里云 AccessKeyId", "access.form.aliyun_access_key_id.tooltip": "这是什么?请参阅 https://help.aliyun.com/zh/ram/user-guide/create-an-accesskey-pair", @@ -491,6 +497,9 @@ "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.vultr_api_key.label": "Vultr API Key", + "access.form.vultr_api_key.placeholder": "请输入 Vultr API Key", + "access.form.vultr_api_key.tooltip": "这是什么?请参阅 https://docs.vultr.com/platform/other/users/manage-users/api-access/regenerate-user-api-key", "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", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 4c39a384..85f1e578 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -5,6 +5,7 @@ "provider.acmeca": "ACME 自定义 CA 端点", "provider.acmedns": "ACME-DNS", "provider.acmehttpreq": "ACME 自定义 HTTP 端点", + "provider.actalisssl": "Actalis SSL", "provider.aliyun": "阿里云", "provider.aliyun.alb": "阿里云 - 应用型负载均衡 ALB", "provider.aliyun.apigw": "阿里云 - API 网关", @@ -171,6 +172,7 @@ "provider.volcengine.imagex": "火山引擎 - 图片服务 ImageX", "provider.volcengine.live": "火山引擎 - 视频直播 Live", "provider.volcengine.tos": "火山引擎 - 对象存储 TOS", + "provider.vultr": "Vultr", "provider.wangsu": "网宿云", "provider.wangsu.cdn": "网宿云 - 内容分发网络 CDN", "provider.wangsu.cdnpro": "网宿云 - CDN Pro (CDN 360)", diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index e214f6e9..19bdbcaf 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -255,9 +255,15 @@ "workflow_node.deploy.form.aliyun_clb_snidomain.placeholder": "请输入阿里云 CLB 扩展域名(支持泛域名)", "workflow_node.deploy.form.aliyun_clb_snidomain.help": "提示:不填写时,将替换监听器的默认证书;否则,将替换扩展域名证书。", "workflow_node.deploy.form.aliyun_clb_snidomain.tooltip": "这是什么?请参阅 https://slb.console.aliyun.com/clb", + "workflow_node.deploy.form.aliyun_cdn_region.label": "阿里云 CDN 服务地域", + "workflow_node.deploy.form.aliyun_cdn_region.placeholder": "请输入阿里云 CDN 服务地域(例如:cn-hangzhou)", + "workflow_node.deploy.form.aliyun_cdn_region.tooltip": "中国站请填写 cn-hangzhou
国际站请填写 ap-southeast-1。", "workflow_node.deploy.form.aliyun_cdn_domain.label": "阿里云 CDN 加速域名", "workflow_node.deploy.form.aliyun_cdn_domain.placeholder": "请输入阿里云 CDN 加速域名(支持泛域名)", "workflow_node.deploy.form.aliyun_cdn_domain.tooltip": "这是什么?请参阅 https://cdn.console.aliyun.com", + "workflow_node.deploy.form.aliyun_dcdn_region.label": "阿里云 DCDN 服务地域", + "workflow_node.deploy.form.aliyun_dcdn_region.placeholder": "请输入阿里云 DCDN 服务地域(例如:cn-hangzhou)", + "workflow_node.deploy.form.aliyun_dcdn_region.tooltip": "中国站请填写 cn-hangzhou
国际站请填写 ap-southeast-1。", "workflow_node.deploy.form.aliyun_dcdn_domain.label": "阿里云 DCDN 加速域名", "workflow_node.deploy.form.aliyun_dcdn_domain.placeholder": "请输入阿里云 DCDN 加速域名(支持泛域名)", "workflow_node.deploy.form.aliyun_dcdn_domain.tooltip": "这是什么?请参阅 https://dcdn.console.aliyun.com", @@ -586,6 +592,16 @@ "workflow_node.deploy.form.k8s_secret_data_key_for_key.label": "Kubernetes Secret 数据键(用于存放私钥的字段)", "workflow_node.deploy.form.k8s_secret_data_key_for_key.placeholder": "请输入 Kubernetes Secret 中用于存放私钥的数据键", "workflow_node.deploy.form.k8s_secret_data_key_for_key.tooltip": "这是什么?请参阅 https://kubernetes.io/zh-cn/docs/concepts/configuration/secret/", + "workflow_node.deploy.form.k8s_secret_annotations.label": "Kubernetes Secret 注解(可选)", + "workflow_node.deploy.form.k8s_secret_annotations.placeholder": "请输入 Kubernetes Secret 注解", + "workflow_node.deploy.form.k8s_secret_annotations.help": "提示:每行一个键值对,以分号分隔。", + "workflow_node.deploy.form.k8s_secret_annotations.errmsg.invalid": "请输入有效的注解键值对", + "workflow_node.deploy.form.k8s_secret_annotations.tooltip": "示例:
environment: production
app: nginx
", + "workflow_node.deploy.form.k8s_secret_labels.label": "Kubernetes Secret 标签(可选)", + "workflow_node.deploy.form.k8s_secret_labels.placeholder": "请输入 Kubernetes Secret 标签", + "workflow_node.deploy.form.k8s_secret_labels.help": "提示:每行一个键值对,以分号分隔。", + "workflow_node.deploy.form.k8s_secret_labels.errmsg.invalid": "请输入有效的标签键值对", + "workflow_node.deploy.form.k8s_secret_labels.tooltip": "示例:
environment: production
app: nginx
", "workflow_node.deploy.form.kong_resource_type.label": "证书部署方式", "workflow_node.deploy.form.kong_resource_type.placeholder": "请选择证书部署方式", "workflow_node.deploy.form.kong_resource_type.option.certificate.label": "替换指定证书", @@ -995,6 +1011,9 @@ "workflow_node.deploy.form.webhook_data.help": "提示:不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。", "workflow_node.deploy.form.webhook_data.guide": "
支持的变量:
  1. ${DOMAIN}:证书的主域名(即 CommonName)。
  2. ${DOMAINS}:证书的多域名列表(即 SubjectAltNames)。
  3. ${CERTIFICATE}:证书文件 PEM 格式内容。
  4. ${SERVER_CERTIFICATE}:证书文件(仅含服务器证书)PEM 格式内容。
  5. ${INTERMEDIA_CERTIFICATE}:证书文件(仅含中间证书)PEM 格式内容。
  6. ${PRIVATE_KEY}:私钥文件 PEM 格式内容。

其他注意事项请前往授权凭据页面查看。", "workflow_node.deploy.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串", + "workflow_node.deploy.form.webhook_timeout.label": "Webhook 超时时间(可选)", + "workflow_node.deploy.form.webhook_timeout.placeholder": "请输入 Webhook 超时时间", + "workflow_node.deploy.form.webhook_timeout.unit": "秒", "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.suffix": "此部署节点。", @@ -1040,6 +1059,9 @@ "workflow_node.notify.form.webhook_data.help": "提示:不填写时,将使用所选部署目标授权的默认 Webhook 回调数据。", "workflow_node.notify.form.webhook_data.guide": "
支持的变量:
  1. ${SUBJECT}:通知主题。
  2. ${MESSAGE}:通知内容。

其他注意事项请前往授权凭据页面查看。", "workflow_node.notify.form.webhook_data.errmsg.json_invalid": "请输入有效的 JSON 格式字符串", + "workflow_node.notify.form.webhook_timeout.label": "Webhook 超时时间(可选)", + "workflow_node.notify.form.webhook_timeout.placeholder": "请输入 Webhook 超时时间", + "workflow_node.notify.form.webhook_timeout.unit": "秒", "workflow_node.notify.form.skip_on_all_prev_skipped.label": "静默行为", "workflow_node.notify.form.skip_on_all_prev_skipped.prefix": "当前序申请、上传、部署等节点均已跳过执行时,", "workflow_node.notify.form.skip_on_all_prev_skipped.suffix": "此通知节点。", diff --git a/ui/src/pages/settings/SettingsSSLProvider.tsx b/ui/src/pages/settings/SettingsSSLProvider.tsx index 4170af57..5d97aa49 100644 --- a/ui/src/pages/settings/SettingsSSLProvider.tsx +++ b/ui/src/pages/settings/SettingsSSLProvider.tsx @@ -108,6 +108,76 @@ const SSLProviderEditFormLetsEncryptStagingConfig = () => { ); }; +const SSLProviderEditFormActalisSSLConfig = () => { + const { t } = useTranslation(); + + const { pending, settings, updateSettings } = useContext(SSLProviderContext); + + const formSchema = z.object({ + eabKid: z + .string(t("access.form.actalisssl_eab_kid.placeholder")) + .min(1, t("access.form.actalisssl_eab_kid.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + eabHmacKey: z + .string(t("access.form.actalisssl_eab_hmac_key.placeholder")) + .min(1, t("access.form.actalisssl_eab_hmac_key.placeholder")) + .max(256, t("common.errmsg.string_max", { max: 256 })), + }); + const formRule = createSchemaFieldRule(formSchema); + const { form: formInst, formProps } = useAntdForm>({ + initialValues: settings?.content?.config?.[CA_PROVIDERS.ACTALISSSL], + onSubmit: async (values) => { + const newSettings = produce(settings, (draft) => { + draft.content ??= {} as SSLProviderSettingsContent; + draft.content.provider = CA_PROVIDERS.ACTALISSSL; + + draft.content.config ??= {} as SSLProviderSettingsContent["config"]; + draft.content.config[CA_PROVIDERS.ACTALISSSL] = values; + }); + await updateSettings(newSettings); + + setFormChanged(false); + }, + }); + + const [formChanged, setFormChanged] = useState(false); + useEffect(() => { + setFormChanged(settings?.content?.provider !== CA_PROVIDERS.ACTALISSSL); + }, [settings?.content?.provider]); + + const handleFormChange = () => { + setFormChanged(true); + }; + + return ( +
+ } + > + + + + } + > + + + + + + +
+ ); +}; + const SSLProviderEditFormBuypassConfig = () => { const { t } = useTranslation(); @@ -466,6 +536,7 @@ const SettingsSSLProvider = () => { const providers = [ [CA_PROVIDERS.LETSENCRYPT, "provider.letsencrypt", "letsencrypt.org", "/imgs/providers/letsencrypt.svg"], [CA_PROVIDERS.LETSENCRYPTSTAGING, "provider.letsencryptstaging", "letsencrypt.org", "/imgs/providers/letsencrypt.svg"], + [CA_PROVIDERS.ACTALISSSL, "provider.actalisssl", "actalis.com", "/imgs/providers/actalisssl.png"], [CA_PROVIDERS.BUYPASS, "provider.buypass", "buypass.com", "/imgs/providers/buypass.png"], [CA_PROVIDERS.GOOGLETRUSTSERVICES, "provider.googletrustservices", "pki.goog", "/imgs/providers/google.svg"], [CA_PROVIDERS.SSLCOM, "provider.sslcom", "ssl.com", "/imgs/providers/sslcom.svg"], @@ -486,6 +557,8 @@ const SettingsSSLProvider = () => { return ; case CA_PROVIDERS.LETSENCRYPTSTAGING: return ; + case CA_PROVIDERS.ACTALISSSL: + return ; case CA_PROVIDERS.BUYPASS: return ; case CA_PROVIDERS.GOOGLETRUSTSERVICES: