feat: support configuring independent ca in workflows

This commit is contained in:
Fu Diwei 2025-03-30 13:09:18 +08:00
parent deb3b2f412
commit 6ad0d8e42f
23 changed files with 496 additions and 186 deletions

View File

@ -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"`
}

View File

@ -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

View File

@ -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)),

View File

@ -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,
})

View File

@ -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"),
}
}

View File

@ -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)
}

View File

@ -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"
}

View File

@ -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<Array<{ key: string; value: string; label: string; data: ApplyCAProvider }>>([]);
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 (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Typography.Text className="italic leading-loose" type="secondary" ellipsis italic>
{t("provider.default_ca_provider.label")}
</Typography.Text>
</Space>
);
}
const provider = applyCAProvidersMap.get(key);
return (
<Space className="max-w-full grow overflow-hidden truncate" size={4}>
<Avatar src={provider?.icon} size="small" />
<Typography.Text className="leading-loose" ellipsis>
{t(provider?.name ?? "")}
</Typography.Text>
</Space>
);
};
return (
<Select
{...props}
filterOption={(inputValue, option) => {
if (!option) return false;
const value = inputValue.toLowerCase();
return option.value.toLowerCase().includes(value) || option.label.toLowerCase().includes(value);
}}
labelRender={({ label, value }) => {
if (!label) {
return <Typography.Text type="secondary">{props.placeholder || t("provider.default_ca_provider.label")}</Typography.Text>;
}
return renderOption(value as string);
}}
options={options}
optionFilterProp={undefined}
optionLabelProp={undefined}
optionRender={(option) => renderOption(option.data.value)}
/>
);
};
export default memo(CAProviderSelect);

View File

@ -5,7 +5,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Typography }
import Show from "@/components/Show";
import { applyDNSProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderPickerProps = {
export type DNSProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@ -13,7 +13,7 @@ export type ApplyDNSProviderPickerProps = {
onSelect?: (value: string) => void;
};
const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: ApplyDNSProviderPickerProps) => {
const DNSProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DNSProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
@ -71,4 +71,4 @@ const ApplyDNSProviderPicker = ({ className, style, autoFocus, placeholder, onSe
);
};
export default memo(ApplyDNSProviderPicker);
export default memo(DNSProviderPicker);

View File

@ -4,14 +4,14 @@ import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type ApplyDNSProvider, applyDNSProvidersMap } from "@/domain/provider";
export type ApplyDNSProviderSelectProps = Omit<
export type DNSProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: ApplyDNSProvider) => boolean;
};
const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProps) => {
const DNSProviderSelect = ({ filter, ...props }: DNSProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: ApplyDNSProvider }>>([]);
@ -64,4 +64,4 @@ const ApplyDNSProviderSelect = ({ filter, ...props }: ApplyDNSProviderSelectProp
);
};
export default memo(ApplyDNSProviderSelect);
export default memo(DNSProviderSelect);

View File

@ -5,7 +5,7 @@ import { Avatar, Card, Col, Empty, Flex, Input, type InputRef, Row, Tabs, Toolti
import Show from "@/components/Show";
import { DEPLOY_CATEGORIES, deployProvidersMap } from "@/domain/provider";
export type DeployProviderPickerProps = {
export type HostingProviderPickerProps = {
className?: string;
style?: React.CSSProperties;
autoFocus?: boolean;
@ -13,7 +13,7 @@ export type DeployProviderPickerProps = {
onSelect?: (value: string) => void;
};
const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: DeployProviderPickerProps) => {
const HostingProviderPicker = ({ className, style, autoFocus, placeholder, onSelect }: HostingProviderPickerProps) => {
const { t } = useTranslation();
const [keyword, setKeyword] = useState<string>();
@ -110,4 +110,4 @@ const DeployProviderPicker = ({ className, style, autoFocus, placeholder, onSele
);
};
export default memo(DeployProviderPicker);
export default memo(HostingProviderPicker);

View File

@ -4,14 +4,14 @@ import { Avatar, Select, type SelectProps, Space, Typography } from "antd";
import { type DeployProvider, deployProvidersMap } from "@/domain/provider";
export type DeployProviderSelectProps = Omit<
export type HostingProviderSelectProps = Omit<
SelectProps,
"filterOption" | "filterSort" | "labelRender" | "options" | "optionFilterProp" | "optionLabelProp" | "optionRender"
> & {
filter?: (record: DeployProvider) => boolean;
};
const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) => {
const HostingProviderSelect = ({ filter, ...props }: HostingProviderSelectProps) => {
const { t } = useTranslation();
const [options, setOptions] = useState<Array<{ key: string; value: string; label: string; data: DeployProvider }>>([]);
@ -64,4 +64,4 @@ const DeployProviderSelect = ({ filter, ...props }: DeployProviderSelectProps) =
);
};
export default memo(DeployProviderSelect);
export default memo(HostingProviderSelect);

View File

@ -1,6 +1,12 @@
import { forwardRef, memo, useEffect, useImperativeHandle, useMemo, useState } from "react";
import { useTranslation } from "react-i18next";
import { FormOutlined as FormOutlinedIcon, PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import { Link } from "react-router";
import {
FormOutlined as FormOutlinedIcon,
PlusOutlined as PlusOutlinedIcon,
QuestionCircleOutlined as QuestionCircleOutlinedIcon,
RightOutlined as RightOutlinedIcon,
} from "@ant-design/icons";
import { useControllableValue } from "ahooks";
import {
AutoComplete,
@ -25,8 +31,9 @@ import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import ModalForm from "@/components/ModalForm";
import MultipleInput from "@/components/MultipleInput";
import ApplyDNSProviderSelect from "@/components/provider/ApplyDNSProviderSelect";
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
import CAProviderSelect from "@/components/provider/CAProviderSelect";
import DNSProviderSelect from "@/components/provider/DNSProviderSelect";
import { ACCESS_USAGES, APPLY_DNS_PROVIDERS, accessProvidersMap, applyCAProvidersMap, applyDNSProvidersMap } from "@/domain/provider";
import { type WorkflowNodeConfigForApply } from "@/domain/workflow";
import { useAntdForm, useAntdFormName, useZustandShallowSelector } from "@/hooks";
import { useAccessesStore } from "@/stores/access";
@ -60,7 +67,7 @@ const initFormModel = (): ApplyNodeConfigFormFieldValues => {
return {
challengeType: "dns-01",
keyAlgorithm: "RSA2048",
skipBeforeExpiryDays: 20,
skipBeforeExpiryDays: 30,
};
};
@ -83,7 +90,16 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
providerAccessId: z
.string({ message: t("workflow_node.apply.form.provider_access.placeholder") })
.min(1, t("workflow_node.apply.form.provider_access.placeholder")),
providerConfig: z.any(),
providerConfig: z.any().nullish(),
caProvider: z.string({ message: t("workflow_node.apply.form.ca_provider.placeholder") }).nullish(),
caProviderAccessId: z
.string({ message: t("workflow_node.apply.form.ca_provider_access.placeholder") })
.nullish()
.refine((v) => {
if (!fieldCAProvider) return true;
return !!v;
}, t("workflow_node.apply.form.ca_provider_access.placeholder")),
caProviderConfig: z.any().nullish(),
keyAlgorithm: z
.string({ message: t("workflow_node.apply.form.key_algorithm.placeholder") })
.nonempty(t("workflow_node.apply.form.key_algorithm.placeholder")),
@ -121,9 +137,10 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
initialValues: initialValues ?? initFormModel(),
});
const fieldDomains = Form.useWatch<string>("domains", formInst);
const fieldProvider = Form.useWatch<string>("provider", { form: formInst, preserve: true });
const fieldProviderAccessId = Form.useWatch<string>("providerAccessId", formInst);
const fieldDomains = Form.useWatch<string>("domains", formInst);
const fieldCAProvider = Form.useWatch<string>("caProvider", formInst);
const fieldNameservers = Form.useWatch<string>("nameservers", formInst);
const [showProvider, setShowProvider] = useState(false);
@ -139,6 +156,15 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
}
}, [accesses, fieldProviderAccessId]);
const [showCAProviderAccess, setShowCAProviderAccess] = useState(false);
useEffect(() => {
if (fieldCAProvider) {
setShowCAProviderAccess(true);
} else {
setShowCAProviderAccess(false);
}
}, [fieldCAProvider]);
const [nestedFormInst] = Form.useForm();
const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" });
const nestedFormEl = useMemo(() => {
@ -195,6 +221,27 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
}
};
const handleCAProviderSelect = (value?: string | undefined) => {
if (fieldCAProvider === value) return;
// 切换 CA 提供商时联动授权信息
if (value === "") {
setTimeout(() => {
formInst.setFieldValue("caProvider", undefined);
formInst.setFieldValue("caProviderAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}, 1);
} else if (initialValues?.caProvider === value) {
formInst.setFieldValue("caProviderAccessId", initialValues?.caProviderAccessId);
onValuesChange?.(formInst.getFieldsValue(true));
} else {
if (applyCAProvidersMap.get(fieldCAProvider)?.provider !== applyCAProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("caProviderAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
}
};
const handleFormProviderChange = (name: string) => {
if (name === nestedFormName) {
formInst.setFieldValue("providerConfig", nestedFormInst.getFieldsValue());
@ -273,7 +320,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Form.Item>
<Form.Item name="provider" label={t("workflow_node.apply.form.provider.label")} hidden={!showProvider} rules={[formRule]}>
<ApplyDNSProviderSelect
<DNSProviderSelect
disabled={!showProvider}
filter={(record) => {
if (fieldProviderAccessId) {
@ -304,13 +351,13 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.apply.form.provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.APPLY)) {
if (provider?.usages?.includes(ACCESS_USAGES.DNS)) {
formInst.setFieldValue("providerAccessId", record.id);
}
}}
@ -322,7 +369,7 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
<AccessSelect
filter={(record) => {
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.APPLY);
return !!provider?.usages?.includes(ACCESS_USAGES.DNS);
}}
placeholder={t("workflow_node.apply.form.provider_access.placeholder")}
onChange={handleProviderAccessSelect}
@ -340,6 +387,71 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
</Divider>
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Form.Item className="mb-0">
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">{t("workflow_node.apply.form.ca_provider.label")}</div>
<div className="text-right">
<Link className="ant-typography" to="/settings/ssl-provider" target="_blank">
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider.button")}
<RightOutlinedIcon className="text-xs" />
</Button>
</Link>
</div>
</div>
</label>
<Form.Item name="caProvider" rules={[formRule]}>
<CAProviderSelect
allowClear
placeholder={t("workflow_node.apply.form.ca_provider.placeholder")}
showSearch
onSelect={handleCAProviderSelect}
onClear={handleCAProviderSelect}
/>
</Form.Item>
</Form.Item>
<Form.Item className="mb-0" hidden={!showCAProviderAccess}>
<label className="mb-1 block">
<div className="flex w-full items-center justify-between gap-4">
<div className="max-w-full grow truncate">
<span>{t("workflow_node.apply.form.ca_provider_access.label")}</span>
</div>
<div className="text-right">
<AccessEditModal
preset="add"
trigger={
<Button size="small" type="link">
{t("workflow_node.apply.form.ca_provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</Button>
}
afterSubmit={(record) => {
const provider = accessProvidersMap.get(record.provider);
if (provider?.usages?.includes(ACCESS_USAGES.CA)) {
formInst.setFieldValue("caProviderAccessId", record.id);
}
}}
/>
</div>
</div>
</label>
<Form.Item name="caProviderAccessId" rules={[formRule]}>
<AccessSelect
filter={(record) => {
if (fieldCAProvider) {
return applyCAProvidersMap.get(fieldCAProvider)?.provider === record.provider;
}
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.CA);
}}
placeholder={t("workflow_node.apply.form.ca_provider_access.placeholder")}
/>
</Form.Item>
</Form.Item>
<Form.Item name="keyAlgorithm" label={t("workflow_node.apply.form.key_algorithm.label")} rules={[formRule]}>
<Select
options={["RSA2048", "RSA3072", "RSA4096", "RSA8192", "EC256", "EC384"].map((e) => ({
@ -364,6 +476,9 @@ const ApplyNodeConfigForm = forwardRef<ApplyNodeConfigFormInstance, ApplyNodeCon
onChange={(e) => {
formInst.setFieldValue("nameservers", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("nameservers", undefined);
}}
/>
</Form.Item>
<NameserversModalInput

View File

@ -7,8 +7,8 @@ import { z } from "zod";
import AccessEditModal from "@/components/access/AccessEditModal";
import AccessSelect from "@/components/access/AccessSelect";
import DeployProviderPicker from "@/components/provider/DeployProviderPicker";
import DeployProviderSelect from "@/components/provider/DeployProviderSelect";
import HostingProviderPicker from "@/components/provider/HostingProviderPicker.tsx";
import HostingProviderSelect from "@/components/provider/HostingProviderSelect.tsx";
import Show from "@/components/Show";
import { ACCESS_USAGES, DEPLOY_PROVIDERS, accessProvidersMap, deployProvidersMap } from "@/domain/provider";
import { type WorkflowNode, type WorkflowNodeConfigForDeploy } from "@/domain/workflow";
@ -292,7 +292,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
onValuesChange?.(formInst.getFieldsValue(true));
};
const handleProviderSelect = (value: string) => {
const handleProviderSelect = (value?: string | undefined) => {
if (fieldProvider === value) return;
// 切换部署目标时重置表单,避免其他部署目标的配置字段影响当前部署目标
@ -310,7 +310,7 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
}
formInst.setFieldsValue(newValues);
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value)?.provider) {
if (deployProvidersMap.get(fieldProvider)?.provider !== deployProvidersMap.get(value!)?.provider) {
formInst.setFieldValue("providerAccessId", undefined);
onValuesChange?.(formInst.getFieldsValue(true));
}
@ -355,15 +355,16 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
<Form className={className} style={style} {...formProps} disabled={disabled} layout="vertical" scrollToFirstError onValuesChange={handleFormChange}>
<Show
when={!!fieldProvider}
fallback={<DeployProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
fallback={<HostingProviderPicker autoFocus placeholder={t("workflow_node.deploy.search.provider.placeholder")} onSelect={handleProviderPick} />}
>
<Form.Item name="provider" label={t("workflow_node.deploy.form.provider.label")} rules={[formRule]}>
<DeployProviderSelect
<HostingProviderSelect
allowClear
disabled={!!initialValues?.provider}
placeholder={t("workflow_node.deploy.form.provider.placeholder")}
showSearch
onSelect={handleProviderSelect}
onClear={handleProviderSelect}
/>
</Form.Item>
@ -384,13 +385,13 @@ const DeployNodeConfigForm = forwardRef<DeployNodeConfigFormInstance, DeployNode
preset="add"
trigger={
<Button size="small" type="link">
<PlusOutlinedIcon />
{t("workflow_node.deploy.form.provider_access.button")}
<PlusOutlinedIcon className="text-xs" />
</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<DeployNodeConfigFormInstance, DeployNode
}
const provider = accessProvidersMap.get(record.provider);
return !!provider?.usages?.includes(ACCESS_USAGES.DEPLOY);
return !!provider?.usages?.includes(ACCESS_USAGES.HOSTING);
}}
placeholder={t("workflow_node.deploy.form.provider_access.placeholder")}
/>

View File

@ -100,6 +100,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("resourceIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("resourceIds", "");
}}
/>
</Form.Item>
<ResourceIdsModalInput
@ -130,6 +133,9 @@ const DeployNodeConfigFormAliyunCASDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("contactIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("contactIds", "");
}}
/>
</Form.Item>
<ContactIdsModalInput

View File

@ -123,6 +123,9 @@ const DeployNodeConfigFormBaotaPanelSiteConfig = ({
onChange={(e) => {
formInst.setFieldValue("siteNames", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("siteNames", "");
}}
/>
</Form.Item>
<SiteNamesModalInput

View File

@ -107,6 +107,9 @@ const DeployNodeConfigFormTencentCloudSSLDeployConfig = ({
onChange={(e) => {
formInst.setFieldValue("resourceIds", e.target.value);
}}
onClear={() => {
formInst.setFieldValue("resourceIds", "");
}}
/>
</Form.Item>
<ResourceIdsModalInput

View File

@ -140,7 +140,47 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
);
// #endregion
// #region ApplyProvider
// #region ApplyCAProvider
/*
ASCII
NOTICE: If you add new constant, please keep ASCII order.
*/
export const APPLY_CA_PROVIDERS = Object.freeze({
GOOGLETRUSTSERVICES: `${ACCESS_PROVIDERS.GOOGLETRUSTSERVICES}`,
LETSENCRYPT: `${ACCESS_PROVIDERS.LETSENCRYPT}`,
LETSENCRYPTSTAGING: `${ACCESS_PROVIDERS.LETSENCRYPTSTAGING}`,
ZEROSSL: `${ACCESS_PROVIDERS.ZEROSSL}`,
} as const);
export type ApplyCAProviderType = (typeof APPLY_CA_PROVIDERS)[keyof typeof APPLY_CA_PROVIDERS];
export type ApplyCAProvider = {
type: ApplyCAProviderType;
name: string;
icon: string;
provider: AccessProviderType;
};
export const applyCAProvidersMap: Map<ApplyCAProvider["type"] | string, ApplyCAProvider> = 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.

View File

@ -126,6 +126,9 @@ export type WorkflowNodeConfigForApply = {
provider: string;
providerAccessId: string;
providerConfig?: Record<string, unknown>;
caProvider?: string;
caProviderAccessId?: string;
caProviderConfig?: Record<string, unknown>;
keyAlgorithm: string;
nameservers?: string;
dnsPropagationTimeout?: number;

View File

@ -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"
}

View File

@ -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 <a href=\"https://console.tencentcloud.com/edgeone\" target=\"_blank\">https://console.tencentcloud.com/edgeone</a>",
"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)",

View File

@ -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": "跟随全局设置"
}

View File

@ -55,6 +55,12 @@
"workflow_node.apply.form.tencentcloud_eo_zone_id.placeholder": "请输入腾讯云 EdgeOne 站点 ID",
"workflow_node.apply.form.tencentcloud_eo_zone_id.tooltip": "这是什么?请参阅 <a href=\"https://console.cloud.tencent.com/edgeone\" target=\"_blank\">https://console.cloud.tencent.com/edgeone</a>",
"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 递归服务器(可选)",