diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index af4aa234..90e8a972 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -30,6 +30,7 @@ import ( pPowerDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/powerdns" pRainYun "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/rainyun" pTencentCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud" + pTencentCloudEO "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo" pVercel "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/vercel" pVolcEngine "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/volcengine" pWestcn "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/westcn" @@ -294,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, "region_id"), + RegionId: maputil.GetString(options.ProviderApplyConfig, "regionId"), DnsPropagationTimeout: options.DnsPropagationTimeout, DnsTTL: options.DnsTTL, }) @@ -410,20 +411,36 @@ func createApplicant(options *applicantOptions) (challenge.Provider, error) { return applicant, err } - case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: + case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS, domain.ApplyDNSProviderTypeTencentCloudEO: { access := domain.AccessConfigForTencentCloud{} if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { return nil, fmt.Errorf("failed to populate provider access config: %w", err) } - applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{ - SecretId: access.SecretId, - SecretKey: access.SecretKey, - DnsPropagationTimeout: options.DnsPropagationTimeout, - DnsTTL: options.DnsTTL, - }) - return applicant, err + switch options.Provider { + case domain.ApplyDNSProviderTypeTencentCloud, domain.ApplyDNSProviderTypeTencentCloudDNS: + applicant, err := pTencentCloud.NewChallengeProvider(&pTencentCloud.ChallengeProviderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + case domain.ApplyDNSProviderTypeTencentCloudEO: + applicant, err := pTencentCloudEO.NewChallengeProvider(&pTencentCloudEO.ChallengeProviderConfig{ + SecretId: access.SecretId, + SecretKey: access.SecretKey, + ZoneId: maputil.GetString(options.ProviderApplyConfig, "zoneId"), + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + + default: + break + } } case domain.ApplyDNSProviderTypeVercel: diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 029405d9..0e4bcce3 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -101,6 +101,7 @@ const ( ApplyDNSProviderTypeRainYun = ApplyDNSProviderType("rainyun") ApplyDNSProviderTypeTencentCloud = ApplyDNSProviderType("tencentcloud") // 兼容旧值,等同于 [ApplyDNSProviderTypeTencentCloudDNS] ApplyDNSProviderTypeTencentCloudDNS = ApplyDNSProviderType("tencentcloud-dns") + ApplyDNSProviderTypeTencentCloudEO = ApplyDNSProviderType("tencentcloud-eo") ApplyDNSProviderTypeVercel = ApplyDNSProviderType("vercel") ApplyDNSProviderTypeVolcEngine = ApplyDNSProviderType("volcengine") // 兼容旧值,等同于 [ApplyDNSProviderTypeVolcEngineDNS] ApplyDNSProviderTypeVolcEngineDNS = ApplyDNSProviderType("volcengine-dns") diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go index 92ef6dfe..0329d18d 100644 --- a/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud/internal/lego.go @@ -106,6 +106,7 @@ func (d *DNSProvider) Present(domain, token, keyAuth string) error { if err != nil { return err } + if record == nil { // add new record resp, err := d.client.CreateRecordOpenapi(&model.CreateRecordOpenapiRequest{ diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go new file mode 100644 index 00000000..57f74193 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal/lego.go @@ -0,0 +1,207 @@ +package lego_tencentcloudeo + +import ( + "errors" + "fmt" + "math" + "strings" + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + teo "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/teo/v20220901" +) + +const ( + envNamespace = "TENCENTCLOUDEO_" + + EnvSecretID = envNamespace + "SECRET_ID" + EnvSecretKey = envNamespace + "SECRET_KEY" + EnvZoneId = envNamespace + "ZONE_ID" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" + EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT" +) + +var _ challenge.ProviderTimeout = (*DNSProvider)(nil) + +type Config struct { + SecretID string + SecretKey string + ZoneId string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int32 + HTTPTimeout time.Duration +} + +type DNSProvider struct { + client *teo.Client + config *Config +} + +func NewDefaultConfig() *Config { + return &Config{ + TTL: int32(env.GetOrDefaultInt(EnvTTL, 300)), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 2*time.Minute), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second), + } +} + +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvSecretID, EnvSecretKey, EnvZoneId) + if err != nil { + return nil, fmt.Errorf("tencentcloud-eo: %w", err) + } + + config := NewDefaultConfig() + config.SecretID = values[EnvSecretID] + config.SecretKey = values[EnvSecretKey] + config.ZoneId = values[EnvSecretKey] + + return NewDNSProviderConfig(config) +} + +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("tencentcloud-eo: the configuration of the DNS provider is nil") + } + + credential := common.NewCredential(config.SecretID, config.SecretKey) + cpf := profile.NewClientProfile() + cpf.HttpProfile.ReqTimeout = int(math.Round(config.HTTPTimeout.Seconds())) + client, err := teo.NewClient(credential, "", cpf) + if err != nil { + return nil, err + } + + return &DNSProvider{ + client: client, + config: config, + }, nil +} + +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + if err := d.addOrUpdateDNSRecord(strings.TrimRight(info.EffectiveFQDN, "."), info.Value); err != nil { + return fmt.Errorf("tencentcloud-eo: %w", err) + } + + return nil +} + +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + info := dns01.GetChallengeInfo(domain, keyAuth) + + if err := d.removeDNSRecord(strings.TrimRight(info.EffectiveFQDN, ".")); err != nil { + return fmt.Errorf("tencentcloud-eo: %w", err) + } + + return nil +} + +func (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +func (d *DNSProvider) getDNSRecord(effectiveFQDN string) (*teo.DnsRecord, error) { + pageOffset := 0 + pageLimit := 1000 + for { + request := teo.NewDescribeDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.Offset = common.Int64Ptr(int64(pageOffset)) + request.Limit = common.Int64Ptr(int64(pageLimit)) + request.Filters = []*teo.AdvancedFilter{ + { + Name: common.StringPtr("type"), + Values: []*string{common.StringPtr("TXT")}, + }, + } + + response, err := d.client.DescribeDnsRecords(request) + if err != nil { + return nil, err + } + + if response.Response == nil { + break + } else { + for _, record := range response.Response.DnsRecords { + if *record.Name == effectiveFQDN { + return record, nil + } + } + + if len(response.Response.DnsRecords) < int(pageLimit) { + break + } + + pageOffset += len(response.Response.DnsRecords) + } + } + + return nil, nil +} + +func (d *DNSProvider) addOrUpdateDNSRecord(effectiveFQDN, value string) error { + record, err := d.getDNSRecord(effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + request := teo.NewCreateDnsRecordRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.Name = common.StringPtr(effectiveFQDN) + request.Type = common.StringPtr("TXT") + request.Content = common.StringPtr(value) + request.TTL = common.Int64Ptr(int64(d.config.TTL)) + _, err := d.client.CreateDnsRecord(request) + return err + } else { + record.Content = common.StringPtr(value) + request := teo.NewModifyDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.DnsRecords = []*teo.DnsRecord{record} + if _, err := d.client.ModifyDnsRecords(request); err != nil { + return err + } + + if *record.Status == "disable" { + request := teo.NewModifyDnsRecordsStatusRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.RecordsToEnable = []*string{record.RecordId} + if _, err = d.client.ModifyDnsRecordsStatus(request); err != nil { + return err + } + } + + return nil + } +} + +func (d *DNSProvider) removeDNSRecord(effectiveFQDN string) error { + record, err := d.getDNSRecord(effectiveFQDN) + if err != nil { + return err + } + + if record == nil { + return nil + } else { + request := teo.NewDeleteDnsRecordsRequest() + request.ZoneId = common.StringPtr(d.config.ZoneId) + request.RecordIds = []*string{record.RecordId} + _, err = d.client.DeleteDnsRecords(request) + return err + } +} diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go new file mode 100644 index 00000000..33552ecf --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/tencentcloud_eo.go @@ -0,0 +1,41 @@ +package tencentcloudeo + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + + internal "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/tencentcloud-eo/internal" +) + +type ChallengeProviderConfig struct { + SecretId string `json:"secretId"` + SecretKey string `json:"secretKey"` + ZoneId string `json:"zoneId"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (challenge.Provider, error) { + if config == nil { + panic("config is nil") + } + + providerConfig := internal.NewDefaultConfig() + providerConfig.SecretID = config.SecretId + providerConfig.SecretKey = config.SecretKey + providerConfig.ZoneId = config.ZoneId + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = config.DnsTTL + } + + provider, err := internal.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/internal/workflow/node-processor/apply_node.go b/internal/workflow/node-processor/apply_node.go index a7d6a101..a1ca977d 100644 --- a/internal/workflow/node-processor/apply_node.go +++ b/internal/workflow/node-processor/apply_node.go @@ -45,7 +45,7 @@ func (n *applyNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this application, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to apply, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-apply, because %s", skipReason)) } // 初始化申请器 diff --git a/internal/workflow/node-processor/deploy_node.go b/internal/workflow/node-processor/deploy_node.go index 42bc9ca6..edb3c53d 100644 --- a/internal/workflow/node-processor/deploy_node.go +++ b/internal/workflow/node-processor/deploy_node.go @@ -58,7 +58,7 @@ func (n *deployNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this deployment, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to deploy, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-deploy, because %s", skipReason)) } } diff --git a/internal/workflow/node-processor/upload_node.go b/internal/workflow/node-processor/upload_node.go index 6c46e90f..891f2978 100644 --- a/internal/workflow/node-processor/upload_node.go +++ b/internal/workflow/node-processor/upload_node.go @@ -43,7 +43,7 @@ func (n *uploadNode) Process(ctx context.Context) error { n.logger.Info(fmt.Sprintf("skip this upload, because %s", skipReason)) return nil } else if skipReason != "" { - n.logger.Info(fmt.Sprintf("continue to upload, because %s", skipReason)) + n.logger.Info(fmt.Sprintf("re-upload, because %s", skipReason)) } // 生成证书实体 diff --git a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx index c714e197..6d219b49 100644 --- a/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeConfigForm.tsx @@ -36,6 +36,7 @@ import { validDomainName, validIPv4Address, validIPv6Address } from "@/utils/val import ApplyNodeConfigFormAWSRoute53Config from "./ApplyNodeConfigFormAWSRoute53Config"; import ApplyNodeConfigFormHuaweiCloudDNSConfig from "./ApplyNodeConfigFormHuaweiCloudDNSConfig"; import ApplyNodeConfigFormJDCloudDNSConfig from "./ApplyNodeConfigFormJDCloudDNSConfig"; +import ApplyNodeConfigFormTencentCloudEOConfig from "./ApplyNodeConfigFormTencentCloudEOConfig"; type ApplyNodeConfigFormFieldValues = Partial; @@ -125,6 +126,19 @@ const ApplyNodeConfigForm = forwardRef("domains", formInst); const fieldNameservers = Form.useWatch("nameservers", formInst); + const [showProvider, setShowProvider] = useState(false); + useEffect(() => { + // 通常情况下每个授权信息只对应一个 DNS 提供商,此时无需显示 DNS 提供商字段; + // 如果对应多个(如 AWS 的 Route53、Lightsail,腾讯云的 DNS、EdgeOne 等),则显示。 + if (fieldProviderAccessId) { + const access = accesses.find((e) => e.id === fieldProviderAccessId); + const providers = Array.from(applyDNSProvidersMap.values()).filter((e) => e.provider === access?.provider); + setShowProvider(providers.length > 1); + } else { + setShowProvider(false); + } + }, [accesses, fieldProviderAccessId]); + const [nestedFormInst] = Form.useForm(); const nestedFormName = useAntdFormName({ form: nestedFormInst, name: "workflowNodeApplyConfigFormProviderConfigForm" }); const nestedFormEl = useMemo(() => { @@ -149,12 +163,15 @@ const ApplyNodeConfigForm = forwardRef; + case APPLY_DNS_PROVIDERS.TENCENTCLOUD_EO: + return ; } }, [disabled, initialValues?.providerConfig, fieldProvider, nestedFormInst, nestedFormName]); const handleProviderSelect = (value: string) => { if (fieldProvider === value) return; + // 切换 DNS 提供商时联动授权信息 if (initialValues?.provider === value) { formInst.setFieldValue("providerAccessId", initialValues?.providerAccessId); onValuesChange?.(formInst.getFieldsValue(true)); @@ -169,10 +186,13 @@ const ApplyNodeConfigForm = forwardRef { if (fieldProviderAccessId === value) return; - // DNS 提供商和授权提供商目前一一对应,因此切换授权时,自动切换到相应提供商 + // 切换授权信息时联动 DNS 提供商 const access = accesses.find((access) => access.id === value); - formInst.setFieldValue("provider", Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider)?.type); - onValuesChange?.(formInst.getFieldsValue(true)); + const provider = Array.from(applyDNSProvidersMap.values()).find((provider) => provider.provider === access?.provider); + if (fieldProvider !== provider?.type) { + formInst.setFieldValue("provider", provider?.type); + onValuesChange?.(formInst.getFieldsValue(true)); + } }; const handleFormProviderChange = (name: string) => { @@ -252,10 +272,16 @@ const ApplyNodeConfigForm = forwardRef -