From 6ad0d8e42fe309848cd2c0734ec6967006a41074 Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Sun, 30 Mar 2025 13:09:18 +0800 Subject: [PATCH] feat: support configuring independent ca in workflows --- internal/applicant/acme_ca.go | 14 +- internal/applicant/acme_user.go | 67 +++++---- internal/applicant/applicant.go | 119 ++++++++------- internal/applicant/providers.go | 10 +- internal/domain/workflow.go | 100 ++++++------- internal/pkg/utils/maputil/getter.go | 22 +++ .../workflow/node-processor/apply_node.go | 12 ++ .../components/provider/CAProviderSelect.tsx | 83 +++++++++++ ...oviderPicker.tsx => DNSProviderPicker.tsx} | 6 +- ...oviderSelect.tsx => DNSProviderSelect.tsx} | 6 +- ...erPicker.tsx => HostingProviderPicker.tsx} | 6 +- ...erSelect.tsx => HostingProviderSelect.tsx} | 6 +- .../workflow/node/ApplyNodeConfigForm.tsx | 135 ++++++++++++++++-- .../workflow/node/DeployNodeConfigForm.tsx | 19 +-- ...loyNodeConfigFormAliyunCASDeployConfig.tsx | 6 + ...ployNodeConfigFormBaotaPanelSiteConfig.tsx | 3 + ...eConfigFormTencentCloudSSLDeployConfig.tsx | 3 + ui/src/domain/provider.ts | 42 +++++- ui/src/domain/workflow.ts | 3 + ui/src/i18n/locales/en/nls.provider.json | 4 +- .../i18n/locales/en/nls.workflow.nodes.json | 6 + ui/src/i18n/locales/zh/nls.provider.json | 4 +- .../i18n/locales/zh/nls.workflow.nodes.json | 6 + 23 files changed, 496 insertions(+), 186 deletions(-) create mode 100644 ui/src/components/provider/CAProviderSelect.tsx rename ui/src/components/provider/{ApplyDNSProviderPicker.tsx => DNSProviderPicker.tsx} (91%) rename ui/src/components/provider/{ApplyDNSProviderSelect.tsx => DNSProviderSelect.tsx} (91%) rename ui/src/components/provider/{DeployProviderPicker.tsx => HostingProviderPicker.tsx} (94%) rename ui/src/components/provider/{DeployProviderSelect.tsx => HostingProviderSelect.tsx} (91%) diff --git a/internal/applicant/acme_ca.go b/internal/applicant/acme_ca.go index afd86bdf..074b2a18 100644 --- a/internal/applicant/acme_ca.go +++ b/internal/applicant/acme_ca.go @@ -19,16 +19,6 @@ var sslProviderUrls = map[string]string{ } type acmeSSLProviderConfig struct { - Config acmeSSLProviderConfigContent `json:"config"` - Provider string `json:"provider"` -} - -type acmeSSLProviderConfigContent struct { - ZeroSSL acmeSSLProviderEabConfig `json:"zerossl"` - GoogleTrustServices acmeSSLProviderEabConfig `json:"googletrustservices"` -} - -type acmeSSLProviderEabConfig struct { - EabHmacKey string `json:"eabHmacKey"` - EabKid string `json:"eabKid"` + Config map[domain.ApplyCAProviderType]map[string]any `json:"config"` + Provider string `json:"provider"` } diff --git a/internal/applicant/acme_user.go b/internal/applicant/acme_user.go index 107e417c..75afa708 100644 --- a/internal/applicant/acme_user.go +++ b/internal/applicant/acme_user.go @@ -14,6 +14,7 @@ import ( "github.com/usual2970/certimate/internal/domain" "github.com/usual2970/certimate/internal/pkg/utils/certutil" + "github.com/usual2970/certimate/internal/pkg/utils/maputil" "github.com/usual2970/certimate/internal/repository" ) @@ -76,16 +77,11 @@ func (u *acmeUser) getPrivateKeyPEM() string { return u.privkey } -type acmeAccountRepository interface { - GetByCAAndEmail(ca, email string) (*domain.AcmeAccount, error) - Save(ca, email, key string, resource *registration.Resource) error -} - var registerGroup singleflight.Group -func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { - resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", sslProviderConfig.Provider, user.GetEmail()), func() (interface{}, error) { - return registerAcmeUser(client, sslProviderConfig, user) +func registerAcmeUserWithSingleFlight(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) { + resp, err, _ := registerGroup.Do(fmt.Sprintf("register_acme_user_%s_%s", user.CA, user.Email), func() (interface{}, error) { + return registerAcmeUser(client, user, userRegisterOptions) }) if err != nil { @@ -95,45 +91,62 @@ func registerAcmeUserWithSingleFlight(client *lego.Client, sslProviderConfig *ac return resp.(*registration.Resource), nil } -func registerAcmeUser(client *lego.Client, sslProviderConfig *acmeSSLProviderConfig, user *acmeUser) (*registration.Resource, error) { +func registerAcmeUser(client *lego.Client, user *acmeUser, userRegisterOptions map[string]any) (*registration.Resource, error) { var reg *registration.Resource var err error - switch sslProviderConfig.Provider { - case sslProviderZeroSSL: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProviderConfig.Config.ZeroSSL.EabKid, - HmacEncoded: sslProviderConfig.Config.ZeroSSL.EabHmacKey, - }) - case sslProviderGoogleTrustServices: - reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ - TermsOfServiceAgreed: true, - Kid: sslProviderConfig.Config.GoogleTrustServices.EabKid, - HmacEncoded: sslProviderConfig.Config.GoogleTrustServices.EabHmacKey, - }) + switch user.CA { case sslProviderLetsEncrypt, sslProviderLetsEncryptStaging: reg, err = client.Registration.Register(registration.RegisterOptions{TermsOfServiceAgreed: true}) + + case sslProviderGoogleTrustServices: + { + access := domain.AccessConfigForGoogleTrustServices{} + if err := maputil.Populate(userRegisterOptions, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: access.EabKid, + HmacEncoded: access.EabHmacKey, + }) + } + + case sslProviderZeroSSL: + { + access := domain.AccessConfigForZeroSSL{} + if err := maputil.Populate(userRegisterOptions, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + reg, err = client.Registration.RegisterWithExternalAccountBinding(registration.RegisterEABOptions{ + TermsOfServiceAgreed: true, + Kid: access.EabKid, + HmacEncoded: access.EabHmacKey, + }) + } + default: - err = fmt.Errorf("unsupported ssl provider: %s", sslProviderConfig.Provider) + err = fmt.Errorf("unsupported ca provider: %s", user.CA) } if err != nil { return nil, err } repo := repository.NewAcmeAccountRepository() - resp, err := repo.GetByCAAndEmail(sslProviderConfig.Provider, user.GetEmail()) + resp, err := repo.GetByCAAndEmail(user.CA, user.Email) if err == nil { user.privkey = resp.Key return resp.Resource, nil } if _, err := repo.Save(context.Background(), &domain.AcmeAccount{ - CA: sslProviderConfig.Provider, - Email: user.GetEmail(), + CA: user.CA, + Email: user.Email, Key: user.getPrivateKeyPEM(), Resource: reg, }); err != nil { - return nil, fmt.Errorf("failed to save registration: %w", err) + return nil, fmt.Errorf("failed to save acme account registration: %w", err) } return reg, nil diff --git a/internal/applicant/applicant.go b/internal/applicant/applicant.go index 8b452aab..1e996976 100644 --- a/internal/applicant/applicant.go +++ b/internal/applicant/applicant.go @@ -37,18 +37,21 @@ type Applicant interface { } type applicantOptions struct { - Domains []string - ContactEmail string - Provider domain.ApplyDNSProviderType - ProviderAccessConfig map[string]any - ProviderApplyConfig map[string]any - KeyAlgorithm string - Nameservers []string - DnsPropagationTimeout int32 - DnsTTL int32 - DisableFollowCNAME bool - ReplacedARIAcctId string - ReplacedARICertId string + Domains []string + ContactEmail string + Provider domain.ApplyDNSProviderType + ProviderAccessConfig map[string]any + ProviderExtendedConfig map[string]any + CAProvider domain.ApplyCAProviderType + CAProviderAccessConfig map[string]any + CAProviderExtendedConfig map[string]any + KeyAlgorithm string + Nameservers []string + DnsPropagationTimeout int32 + DnsTTL int32 + DisableFollowCNAME bool + ReplacedARIAcctId string + ReplacedARICertId string } func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { @@ -58,22 +61,55 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { nodeConfig := node.GetConfigForApply() options := &applicantOptions{ - Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), - ContactEmail: nodeConfig.ContactEmail, - Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), - ProviderApplyConfig: nodeConfig.ProviderConfig, - KeyAlgorithm: nodeConfig.KeyAlgorithm, - Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), - DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, - DnsTTL: nodeConfig.DnsTTL, - DisableFollowCNAME: nodeConfig.DisableFollowCNAME, + Domains: sliceutil.Filter(strings.Split(nodeConfig.Domains, ";"), func(s string) bool { return s != "" }), + ContactEmail: nodeConfig.ContactEmail, + Provider: domain.ApplyDNSProviderType(nodeConfig.Provider), + ProviderAccessConfig: make(map[string]any), + ProviderExtendedConfig: nodeConfig.ProviderConfig, + CAProvider: domain.ApplyCAProviderType(nodeConfig.CAProvider), + CAProviderAccessConfig: make(map[string]any), + CAProviderExtendedConfig: nodeConfig.CAProviderConfig, + KeyAlgorithm: nodeConfig.KeyAlgorithm, + Nameservers: sliceutil.Filter(strings.Split(nodeConfig.Nameservers, ";"), func(s string) bool { return s != "" }), + DnsPropagationTimeout: nodeConfig.DnsPropagationTimeout, + DnsTTL: nodeConfig.DnsTTL, + DisableFollowCNAME: nodeConfig.DisableFollowCNAME, } accessRepo := repository.NewAccessRepository() - if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { - return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) - } else { - options.ProviderAccessConfig = access.Config + if nodeConfig.ProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeConfig.ProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.ProviderAccessId, err) + } else { + options.ProviderAccessConfig = access.Config + } + } + if nodeConfig.CAProviderAccessId != "" { + if access, err := accessRepo.GetById(context.Background(), nodeConfig.CAProviderAccessId); err != nil { + return nil, fmt.Errorf("failed to get access #%s record: %w", nodeConfig.CAProviderAccessId, err) + } else { + options.CAProviderAccessConfig = access.Config + } + } + + settingsRepo := repository.NewSettingsRepository() + if string(options.CAProvider) == "" { + settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") + + sslProviderConfig := &acmeSSLProviderConfig{ + Config: make(map[domain.ApplyCAProviderType]map[string]any), + Provider: sslProviderDefault, + } + if settings != nil { + if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { + return nil, err + } else if sslProviderConfig.Provider == "" { + sslProviderConfig.Provider = sslProviderDefault + } + } + + options.CAProvider = domain.ApplyCAProviderType(sslProviderConfig.Provider) + options.CAProviderAccessConfig = sslProviderConfig.Config[options.CAProvider] } certRepo := repository.NewCertificateRepository() @@ -106,24 +142,7 @@ func NewWithApplyNode(node *domain.WorkflowNode) (Applicant, error) { } func apply(challengeProvider challenge.Provider, options *applicantOptions) (*ApplyCertResult, error) { - settingsRepo := repository.NewSettingsRepository() - settings, _ := settingsRepo.GetByName(context.Background(), "sslProvider") - - sslProviderConfig := &acmeSSLProviderConfig{ - Config: acmeSSLProviderConfigContent{}, - Provider: sslProviderDefault, - } - if settings != nil { - if err := json.Unmarshal([]byte(settings.Content), sslProviderConfig); err != nil { - return nil, err - } - } - - if sslProviderConfig.Provider == "" { - sslProviderConfig.Provider = sslProviderDefault - } - - acmeUser, err := newAcmeUser(sslProviderConfig.Provider, options.ContactEmail) + user, err := newAcmeUser(string(options.CAProvider), options.ContactEmail) if err != nil { return nil, err } @@ -133,8 +152,8 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap os.Setenv("LEGO_DISABLE_CNAME_SUPPORT", strconv.FormatBool(options.DisableFollowCNAME)) // Create an ACME client config - config := lego.NewConfig(acmeUser) - config.CADirURL = sslProviderUrls[sslProviderConfig.Provider] + config := lego.NewConfig(user) + config.CADirURL = sslProviderUrls[user.CA] config.Certificate.KeyType = parseKeyAlgorithm(domain.CertificateKeyAlgorithmType(options.KeyAlgorithm)) // Create an ACME client @@ -152,12 +171,12 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap client.Challenge.SetDNS01Provider(challengeProvider, challengeOptions...) // New users need to register first - if !acmeUser.hasRegistration() { - reg, err := registerAcmeUserWithSingleFlight(client, sslProviderConfig, acmeUser) + if !user.hasRegistration() { + reg, err := registerAcmeUserWithSingleFlight(client, user, options.CAProviderAccessConfig) if err != nil { return nil, fmt.Errorf("failed to register: %w", err) } - acmeUser.Registration = reg + user.Registration = reg } // Obtain a certificate @@ -165,7 +184,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap Domains: options.Domains, Bundle: true, } - if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != acmeUser.Registration.URI { + if options.ReplacedARICertId != "" && options.ReplacedARIAcctId != user.Registration.URI { certRequest.ReplacesCertID = options.ReplacedARICertId } certResource, err := client.Certificate.Obtain(certRequest) @@ -177,7 +196,7 @@ func apply(challengeProvider challenge.Provider, options *applicantOptions) (*Ap CertificateFullChain: strings.TrimSpace(string(certResource.Certificate)), IssuerCertificate: strings.TrimSpace(string(certResource.IssuerCertificate)), PrivateKey: strings.TrimSpace(string(certResource.PrivateKey)), - ACMEAccountUrl: acmeUser.Registration.URI, + ACMEAccountUrl: user.Registration.URI, ACMECertUrl: certResource.CertURL, ACMECertStableUrl: certResource.CertStableURL, CSR: strings.TrimSpace(string(certResource.CSR)), diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index 90e8a972..55164eeb 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -86,8 +86,8 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pAWSRoute53.NewChallengeProvider(&pAWSRoute53.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderApplyConfig, "region"), - HostedZoneId: maputil.GetString(options.ProviderApplyConfig, "hostedZoneId"), + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), + HostedZoneId: maputil.GetString(options.ProviderExtendedConfig, "hostedZoneId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -278,7 +278,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pHuaweiCloud.NewChallengeProvider(&pHuaweiCloud.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, SecretAccessKey: access.SecretAccessKey, - Region: maputil.GetString(options.ProviderApplyConfig, "region"), + Region: maputil.GetString(options.ProviderExtendedConfig, "region"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -295,7 +295,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pJDCloud.NewChallengeProvider(&pJDCloud.ChallengeProviderConfig{ AccessKeyId: access.AccessKeyId, AccessKeySecret: access.AccessKeySecret, - RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"), + RegionId: maputil.GetString(options.ProviderExtendedConfig, "regionId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -432,7 +432,7 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{ SecretId: access.SecretId, SecretKey: access.SecretKey, - ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"), + ZoneId: maputil.GetString(options.ProviderExtendedConfig, "zoneId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) diff --git a/internal/domain/workflow.go b/internal/domain/workflow.go index 50069865..068920ae 100644 --- a/internal/domain/workflow.go +++ b/internal/domain/workflow.go @@ -62,19 +62,22 @@ type WorkflowNode struct { } type WorkflowNodeConfigForApply struct { - Domains string `json:"domains"` // 域名列表,以半角分号分隔 - ContactEmail string `json:"contactEmail"` // 联系邮箱 - ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 - Provider string `json:"provider"` // DNS 提供商 - ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID - ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 - KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 - Nameservers string `json:"nameservers"` // DNS 服务器列表,以半角分号分隔 - DnsPropagationTimeout int32 `json:"dnsPropagationTimeout"` // DNS 传播超时时间(零值取决于提供商的默认值) - DnsTTL int32 `json:"dnsTTL"` // DNS TTL(零值取决于提供商的默认值) - DisableFollowCNAME bool `json:"disableFollowCNAME"` // 是否关闭 CNAME 跟随 - DisableARI bool `json:"disableARI"` // 是否关闭 ARI - SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) + Domains string `json:"domains"` // 域名列表,以半角分号分隔 + ContactEmail string `json:"contactEmail"` // 联系邮箱 + ChallengeType string `json:"challengeType"` // TODO: 验证方式。目前仅支持 dns-01 + Provider string `json:"provider"` // DNS 提供商 + ProviderAccessId string `json:"providerAccessId"` // DNS 提供商授权记录 ID + ProviderConfig map[string]any `json:"providerConfig"` // DNS 提供商额外配置 + CAProvider string `json:"caProvider,omitempty"` // CA 提供商(零值将使用全局配置) + CAProviderAccessId string `json:"caProviderAccessId,omitempty"` // CA 提供商授权记录 ID + CAProviderConfig map[string]any `json:"caProviderConfig,omitempty"` // CA 提供商额外配置 + KeyAlgorithm string `json:"keyAlgorithm"` // 密钥算法 + Nameservers string `json:"nameservers,omitempty"` // DNS 服务器列表,以半角分号分隔 + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` // DNS 传播超时时间(零值取决于提供商的默认值) + DnsTTL int32 `json:"dnsTTL,omitempty"` // DNS TTL(零值取决于提供商的默认值) + DisableFollowCNAME bool `json:"disableFollowCNAME,omitempty"` // 是否关闭 CNAME 跟随 + DisableARI bool `json:"disableARI,omitempty"` // 是否关闭 ARI + SkipBeforeExpiryDays int32 `json:"skipBeforeExpiryDays,omitempty"` // 证书到期前多少天前跳过续期(零值将使用默认值 30) } type WorkflowNodeConfigForUpload struct { @@ -97,73 +100,54 @@ type WorkflowNodeConfigForNotify struct { Message string `json:"message"` // 通知内容 } -func (n *WorkflowNode) getConfigString(key string) string { - return maputil.GetString(n.Config, key) -} - -func (n *WorkflowNode) getConfigBool(key string) bool { - return maputil.GetBool(n.Config, key) -} - -func (n *WorkflowNode) getConfigInt32(key string) int32 { - return maputil.GetInt32(n.Config, key) -} - -func (n *WorkflowNode) getConfigMap(key string) map[string]any { - if val, ok := n.Config[key]; ok { - if result, ok := val.(map[string]any); ok { - return result - } - } - - return make(map[string]any) -} - func (n *WorkflowNode) GetConfigForApply() WorkflowNodeConfigForApply { - skipBeforeExpiryDays := n.getConfigInt32("skipBeforeExpiryDays") + skipBeforeExpiryDays := maputil.GetInt32(n.Config, "skipBeforeExpiryDays") if skipBeforeExpiryDays == 0 { skipBeforeExpiryDays = 30 } return WorkflowNodeConfigForApply{ - Domains: n.getConfigString("domains"), - ContactEmail: n.getConfigString("contactEmail"), - Provider: n.getConfigString("provider"), - ProviderAccessId: n.getConfigString("providerAccessId"), - ProviderConfig: n.getConfigMap("providerConfig"), - KeyAlgorithm: n.getConfigString("keyAlgorithm"), - Nameservers: n.getConfigString("nameservers"), - DnsPropagationTimeout: n.getConfigInt32("dnsPropagationTimeout"), - DnsTTL: n.getConfigInt32("dnsTTL"), - DisableFollowCNAME: n.getConfigBool("disableFollowCNAME"), - DisableARI: n.getConfigBool("disableARI"), + Domains: maputil.GetString(n.Config, "domains"), + ContactEmail: maputil.GetString(n.Config, "contactEmail"), + Provider: maputil.GetString(n.Config, "provider"), + ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), + ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"), + CAProvider: maputil.GetString(n.Config, "caProvider"), + CAProviderAccessId: maputil.GetString(n.Config, "caProviderAccessId"), + CAProviderConfig: maputil.GetAnyMap(n.Config, "caProviderConfig"), + KeyAlgorithm: maputil.GetString(n.Config, "keyAlgorithm"), + Nameservers: maputil.GetString(n.Config, "nameservers"), + DnsPropagationTimeout: maputil.GetInt32(n.Config, "dnsPropagationTimeout"), + DnsTTL: maputil.GetInt32(n.Config, "dnsTTL"), + DisableFollowCNAME: maputil.GetBool(n.Config, "disableFollowCNAME"), + DisableARI: maputil.GetBool(n.Config, "disableARI"), SkipBeforeExpiryDays: skipBeforeExpiryDays, } } func (n *WorkflowNode) GetConfigForUpload() WorkflowNodeConfigForUpload { return WorkflowNodeConfigForUpload{ - Certificate: n.getConfigString("certificate"), - PrivateKey: n.getConfigString("privateKey"), - Domains: n.getConfigString("domains"), + Certificate: maputil.GetString(n.Config, "certificate"), + PrivateKey: maputil.GetString(n.Config, "privateKey"), + Domains: maputil.GetString(n.Config, "domains"), } } func (n *WorkflowNode) GetConfigForDeploy() WorkflowNodeConfigForDeploy { return WorkflowNodeConfigForDeploy{ - Certificate: n.getConfigString("certificate"), - Provider: n.getConfigString("provider"), - ProviderAccessId: n.getConfigString("providerAccessId"), - ProviderConfig: n.getConfigMap("providerConfig"), - SkipOnLastSucceeded: n.getConfigBool("skipOnLastSucceeded"), + Certificate: maputil.GetString(n.Config, "certificate"), + Provider: maputil.GetString(n.Config, "provider"), + ProviderAccessId: maputil.GetString(n.Config, "providerAccessId"), + ProviderConfig: maputil.GetAnyMap(n.Config, "providerConfig"), + SkipOnLastSucceeded: maputil.GetBool(n.Config, "skipOnLastSucceeded"), } } func (n *WorkflowNode) GetConfigForNotify() WorkflowNodeConfigForNotify { return WorkflowNodeConfigForNotify{ - Channel: n.getConfigString("channel"), - Subject: n.getConfigString("subject"), - Message: n.getConfigString("message"), + Channel: maputil.GetString(n.Config, "channel"), + Subject: maputil.GetString(n.Config, "subject"), + Message: maputil.GetString(n.Config, "message"), } } diff --git a/internal/pkg/utils/maputil/getter.go b/internal/pkg/utils/maputil/getter.go index 36561381..9ba22875 100644 --- a/internal/pkg/utils/maputil/getter.go +++ b/internal/pkg/utils/maputil/getter.go @@ -180,3 +180,25 @@ func GetOrDefaultBool(dict map[string]any, key string, defaultValue bool) bool { return defaultValue } + +// 以 `map[string]any` 形式从字典中获取指定键的值。 +// +// 入参: +// - dict: 字典。 +// - key: 键。 +// +// 出参: +// - 字典中键对应的 `map[string]any` 对象。 +func GetAnyMap(dict map[string]any, key string) map[string]any { + if dict == nil { + return make(map[string]any) + } + + if val, ok := dict[key]; ok { + if result, ok := val.(map[string]any); ok { + return result + } + } + + return make(map[string]any) +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index a1ca977d..b9769f3d 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -109,12 +109,24 @@ func (n *applyNode) checkCanSkip(ctx context.Context, lastOutput *domain.Workflo if currentNodeConfig.ContactEmail != lastNodeConfig.ContactEmail { return false, "the configuration item 'ContactEmail' changed" } + if currentNodeConfig.Provider != lastNodeConfig.Provider { + return false, "the configuration item 'Provider' changed" + } if currentNodeConfig.ProviderAccessId != lastNodeConfig.ProviderAccessId { return false, "the configuration item 'ProviderAccessId' changed" } if !maps.Equal(currentNodeConfig.ProviderConfig, lastNodeConfig.ProviderConfig) { return false, "the configuration item 'ProviderConfig' changed" } + if currentNodeConfig.CAProvider != lastNodeConfig.CAProvider { + return false, "the configuration item 'CAProvider' changed" + } + if currentNodeConfig.CAProviderAccessId != lastNodeConfig.CAProviderAccessId { + return false, "the configuration item 'CAProviderAccessId' changed" + } + if !maps.Equal(currentNodeConfig.CAProviderConfig, lastNodeConfig.CAProviderConfig) { + return false, "the configuration item 'CAProviderConfig' changed" + } if currentNodeConfig.KeyAlgorithm != lastNodeConfig.KeyAlgorithm { return false, "the configuration item 'KeyAlgorithm' changed" } diff --git a/ui/src/components/provider/CAProviderSelect.tsx b/ui/src/components/provider/CAProviderSelect.tsx new file mode 100644 index 00000000..5d9637ac --- /dev/null +++ b/ui/src/components/provider/CAProviderSelect.tsx @@ -0,0 +1,83 @@ +import { memo, useEffect, useState } from "react"; +import { useTranslation } from "react-i18next"; +import { Avatar, Select, type SelectProps, Space, Typography } from "antd"; + +import { type ApplyCAProvider, applyCAProvidersMap } from "@/domain/provider"; + +export type CAProviderSelectProps = Omit< + SelectProps, + "filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender" +> & { + filter?: (record: ApplyCAProvider) => boolean; +}; + +const CAProviderSelect = ({ filter, ...props }: CAProviderSelectProps) => { + const { t } = useTranslation(); + + const [options, setOptions] = useState>([]); + useEffect(() => { + const allItems = Array.from(applyCAProvidersMap.values()); + const filteredItems = filter != null ? allItems.filter(filter) : allItems; + setOptions([ + { + key: "", + value: "", + label: "provider.default_ca_provider.label", + data: {} as ApplyCAProvider, + }, + ...filteredItems.map((item) => ({ + key: item.type, + value: item.type, + label: t(item.name), + data: item, + })), + ]); + }, [filter]); + + const renderOption = (key: string) => { + if (key === "") { + return ( + + + {t("provider.default_ca_provider.label")} + + + ); + } + + const provider = applyCAProvidersMap.get(key); + return ( + + + + {t(provider?.name ?? "")} + + + ); + }; + + return ( + ({ @@ -364,6 +476,9 @@ const ApplyNodeConfigForm = forwardRef { formInst.setFieldValue("nameservers", e.target.value); }} + onClear={() => { + formInst.setFieldValue("nameservers", undefined); + }} /> { + const handleProviderSelect = (value?: string | undefined) => { if (fieldProvider === value) return; // 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标 @@ -310,7 +310,7 @@ const DeployNodeConfigForm = forwardRef } + fallback={} > - @@ -384,13 +385,13 @@ const DeployNodeConfigForm = forwardRef - {t("workflow_node.deploy.form.provider_access.button")} + } afterSubmit={(record) => { const provider = accessProvidersMap.get(record.provider); - if (provider?.usages?.includes(ACCESS_USAGES.DEPLOY)) { + if (provider?.usages?.includes(ACCESS_USAGES.HOSTING)) { formInst.setFieldValue("providerAccessId", record.id); } }} @@ -406,7 +407,7 @@ const DeployNodeConfigForm = forwardRef diff --git a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx index 065f752c..f6182f28 100644 --- a/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx +++ b/ui/src/components/workflow/node/DeployNodeConfigFormAliyunCASDeployConfig.tsx @@ -100,6 +100,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({ onChange={(e) => { formInst.setFieldValue("resourceIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("resourceIds", ""); + }} /> { formInst.setFieldValue("contactIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("contactIds", ""); + }} /> { formInst.setFieldValue("siteNames", e.target.value); }} + onClear={() => { + formInst.setFieldValue("siteNames", ""); + }} /> { formInst.setFieldValue("resourceIds", e.target.value); }} + onClear={() => { + formInst.setFieldValue("resourceIds", ""); + }} /> = new Map( + /* + 注意:此处的顺序决定显示在前端的顺序。 + NOTICE: The following order determines the order displayed at the frontend. + */ + [[APPLY_CA_PROVIDERS.LETSENCRYPT], [APPLY_CA_PROVIDERS.LETSENCRYPTSTAGING], [APPLY_CA_PROVIDERS.ZEROSSL], [APPLY_CA_PROVIDERS.GOOGLETRUSTSERVICES]].map( + ([type]) => [ + type, + { + type: type as ApplyCAProviderType, + name: accessProvidersMap.get(type.split("-")[0])!.name, + icon: accessProvidersMap.get(type.split("-")[0])!.icon, + provider: type.split("-")[0] as AccessProviderType, + }, + ] + ) +); +// #endregion + +// #region ApplyDNSProvider /* 注意:如果追加新的常量值,请保持以 ASCII 排序。 NOTICE: If you add new constant, please keep ASCII order. diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 07841902..77f4ba12 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -126,6 +126,9 @@ export type WorkflowNodeConfigForApply = { provider: string; providerAccessId: string; providerConfig?: Record; + caProvider?: string; + caProviderAccessId?: string; + caProviderConfig?: Record; keyAlgorithm: string; nameservers?: string; dnsPropagationTimeout?: number; diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index f2e286a9..89bf68e6 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -134,5 +134,7 @@ "provider.category.av": "Audio/Video", "provider.category.serverless": "Serverless", "provider.category.website": "Website", - "provider.category.other": "Other" + "provider.category.other": "Other", + + "provider.default_ca_provider.label": "Follow global settings" } diff --git a/ui/src/i18n/locales/en/nls.workflow.nodes.json b/ui/src/i18n/locales/en/nls.workflow.nodes.json index 39c0361c..594575c8 100644 --- a/ui/src/i18n/locales/en/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/en/nls.workflow.nodes.json @@ -55,6 +55,12 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "Please enter Tencent Cloud EdgeOne zone ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "For more information, see https://console.tencentcloud.com/edgeone", "workflow_node.apply.form.advanced_config.label": "Advanced settings", + "workflow_node.apply.form.ca_provider.label": "Certificate authority", + "workflow_node.apply.form.ca_provider.placeholder": "Follow global settings", + "workflow_node.apply.form.ca_provider.button": "Configure", + "workflow_node.apply.form.ca_provider_access.label": "Certificate authority authorization", + "workflow_node.apply.form.ca_provider_access.placeholder": "Please select an authorization of the certificate authority", + "workflow_node.apply.form.ca_provider_access.button": "Create", "workflow_node.apply.form.key_algorithm.label": "Certificate key algorithm", "workflow_node.apply.form.key_algorithm.placeholder": "Please select certificate key algorithm", "workflow_node.apply.form.nameservers.label": "DNS recursive nameservers (Optional)", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index 341ceb1b..a164e360 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -134,5 +134,7 @@ "provider.category.av": "音视频", "provider.category.serverless": "Serverless", "provider.category.website": "网站托管", - "provider.category.other": "其他" + "provider.category.other": "其他", + + "provider.default_ca_provider.label": "跟随全局设置" } diff --git a/ui/src/i18n/locales/zh/nls.workflow.nodes.json b/ui/src/i18n/locales/zh/nls.workflow.nodes.json index 99382f80..4e7450e0 100644 --- a/ui/src/i18n/locales/zh/nls.workflow.nodes.json +++ b/ui/src/i18n/locales/zh/nls.workflow.nodes.json @@ -55,6 +55,12 @@ "workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID", "workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 https://console.cloud.tencent.com/edgeone", "workflow_node.apply.form.advanced_config.label": "高级设置", + "workflow_node.apply.form.ca_provider.label": "证书颁发机构", + "workflow_node.apply.form.ca_provider.placeholder": "跟随全局设置", + "workflow_node.apply.form.ca_provider.button": "去配置", + "workflow_node.apply.form.ca_provider_access.label": "证书颁发机构授权", + "workflow_node.apply.form.ca_provider_access.placeholder": "请选择证书颁发机构授权", + "workflow_node.apply.form.ca_provider_access.button": "新建", "workflow_node.apply.form.key_algorithm.label": "数字证书算法", "workflow_node.apply.form.key_algorithm.placeholder": "请选择数字证书算法", "workflow_node.apply.form.nameservers.label": "DNS 递归服务器(可选)",