From 40f4488009db60b768f35026a9dffcbb615c1b0a Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Mon, 26 May 2025 13:29:48 +0800 Subject: [PATCH] feat: new acme dns-01 provider: digitalocean --- internal/applicant/providers.go | 16 ++++++ internal/domain/access.go | 4 ++ internal/domain/provider.go | 2 + .../digitalocean/digitalocean.go | 36 ++++++++++++ ui/public/imgs/providers/digitalocean.svg | 1 + ui/src/components/access/AccessForm.tsx | 3 + .../access/AccessFormDigitalOceanConfig.tsx | 57 +++++++++++++++++++ ui/src/domain/access.ts | 4 ++ ui/src/domain/provider.ts | 4 ++ ui/src/i18n/locales/en/nls.access.json | 3 + ui/src/i18n/locales/en/nls.provider.json | 1 + ui/src/i18n/locales/zh/nls.access.json | 3 + ui/src/i18n/locales/zh/nls.provider.json | 1 + 13 files changed, 135 insertions(+) create mode 100644 internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go create mode 100644 ui/public/imgs/providers/digitalocean.svg create mode 100644 ui/src/components/access/AccessFormDigitalOceanConfig.tsx diff --git a/internal/applicant/providers.go b/internal/applicant/providers.go index f967aef9..e35aee3e 100644 --- a/internal/applicant/providers.go +++ b/internal/applicant/providers.go @@ -17,6 +17,7 @@ import ( pClouDNS "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cloudns" pCMCCCloud "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/cmcccloud" pDeSEC "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/desec" + pDigitalOcean "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean" pDNSLA "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dnsla" pDynv6 "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/dynv6" pGcore "github.com/usual2970/certimate/internal/pkg/core/applicant/acme-dns-01/lego-providers/gcore" @@ -247,6 +248,21 @@ func createApplicantProvider(options *applicantProviderOptions) (challenge.Provi return applicant, err } + case domain.ACMEDns01ProviderTypeDigitalOcean: + { + access := domain.AccessConfigForDigitalOcean{} + if err := maputil.Populate(options.ProviderAccessConfig, &access); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + applicant, err := pDigitalOcean.NewChallengeProvider(&pDigitalOcean.ChallengeProviderConfig{ + AccessToken: access.AccessToken, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return applicant, err + } + case domain.ACMEDns01ProviderTypeDNSLA: { access := domain.AccessConfigForDNSLA{} diff --git a/internal/domain/access.go b/internal/domain/access.go index 30088bdf..c3530a4f 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -112,6 +112,10 @@ type AccessConfigForDeSEC struct { Token string `json:"token"` } +type AccessConfigForDigitalOcean struct { + AccessToken string `json:"accessToken"` +} + type AccessConfigForDingTalkBot struct { WebhookUrl string `json:"webhookUrl"` Secret string `json:"secret"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index 4552553d..52a71e64 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -31,6 +31,7 @@ const ( AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud") // 天翼云(预留) AccessProviderTypeCUCCCloud = AccessProviderType("cucccloud") // 联通云(预留) AccessProviderTypeDeSEC = AccessProviderType("desec") + AccessProviderTypeDigitalOcean = AccessProviderType("digitalocean") AccessProviderTypeDingTalkBot = AccessProviderType("dingtalkbot") AccessProviderTypeDNSLA = AccessProviderType("dnsla") AccessProviderTypeDogeCloud = AccessProviderType("dogecloud") @@ -127,6 +128,7 @@ const ( ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS) ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) ACMEDns01ProviderTypeDeSEC = ACMEDns01ProviderType(AccessProviderTypeDeSEC) + ACMEDns01ProviderTypeDigitalOcean = ACMEDns01ProviderType(AccessProviderTypeDigitalOcean) ACMEDns01ProviderTypeDNSLA = ACMEDns01ProviderType(AccessProviderTypeDNSLA) ACMEDns01ProviderTypeDynv6 = ACMEDns01ProviderType(AccessProviderTypeDynv6) ACMEDns01ProviderTypeGcore = ACMEDns01ProviderType(AccessProviderTypeGcore) diff --git a/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go b/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go new file mode 100644 index 00000000..0e3cb358 --- /dev/null +++ b/internal/pkg/core/applicant/acme-dns-01/lego-providers/digitalocean/digitalocean.go @@ -0,0 +1,36 @@ +package namedotcom + +import ( + "time" + + "github.com/go-acme/lego/v4/challenge" + "github.com/go-acme/lego/v4/providers/dns/digitalocean" +) + +type ChallengeProviderConfig struct { + AccessToken string `json:"accessToken"` + 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 := digitalocean.NewDefaultConfig() + providerConfig.AuthToken = config.AccessToken + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := digitalocean.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/digitalocean.svg b/ui/public/imgs/providers/digitalocean.svg new file mode 100644 index 00000000..94b90bf9 --- /dev/null +++ b/ui/public/imgs/providers/digitalocean.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/ui/src/components/access/AccessForm.tsx b/ui/src/components/access/AccessForm.tsx index 6f4d5086..4fd547e8 100644 --- a/ui/src/components/access/AccessForm.tsx +++ b/ui/src/components/access/AccessForm.tsx @@ -29,6 +29,7 @@ import AccessFormCloudflareConfig from "./AccessFormCloudflareConfig"; import AccessFormClouDNSConfig from "./AccessFormClouDNSConfig"; import AccessFormCMCCCloudConfig from "./AccessFormCMCCCloudConfig"; import AccessFormDeSECConfig from "./AccessFormDeSECConfig"; +import AccessFormDigitalOceanConfig from "./AccessFormDigitalOceanConfig"; import AccessFormDingTalkBotConfig from "./AccessFormDingTalkBotConfig"; import AccessFormDNSLAConfig from "./AccessFormDNSLAConfig"; import AccessFormDogeCloudConfig from "./AccessFormDogeCloudConfig"; @@ -216,6 +217,8 @@ const AccessForm = forwardRef(({ className, return ; case ACCESS_PROVIDERS.DESEC: return ; + case ACCESS_PROVIDERS.DIGITALOCEAN: + return ; case ACCESS_PROVIDERS.DINGTALKBOT: return ; case ACCESS_PROVIDERS.DNSLA: diff --git a/ui/src/components/access/AccessFormDigitalOceanConfig.tsx b/ui/src/components/access/AccessFormDigitalOceanConfig.tsx new file mode 100644 index 00000000..f4aafc4f --- /dev/null +++ b/ui/src/components/access/AccessFormDigitalOceanConfig.tsx @@ -0,0 +1,57 @@ +import { useTranslation } from "react-i18next"; +import { Form, type FormInstance, Input } from "antd"; +import { createSchemaFieldRule } from "antd-zod"; +import { z } from "zod"; + +import { type AccessConfigForDigitalOcean } from "@/domain/access"; + +type AccessFormDigitalOceanConfigFieldValues = Nullish; + +export type AccessFormDigitalOceanConfigProps = { + form: FormInstance; + formName: string; + disabled?: boolean; + initialValues?: AccessFormDigitalOceanConfigFieldValues; + onValuesChange?: (values: AccessFormDigitalOceanConfigFieldValues) => void; +}; + +const initFormModel = (): AccessFormDigitalOceanConfigFieldValues => { + return { + accessToken: "", + }; +}; + +const AccessFormDigitalOceanConfig = ({ form: formInst, formName, disabled, initialValues, onValuesChange }: AccessFormDigitalOceanConfigProps) => { + const { t } = useTranslation(); + + const formSchema = z.object({ + accessToken: z.string().nonempty(t("access.form.digitalocean_access_token.placeholder")).trim(), + }); + const formRule = createSchemaFieldRule(formSchema); + + const handleFormChange = (_: unknown, values: z.infer) => { + onValuesChange?.(values); + }; + + return ( +
+ } + > + + +
+ ); +}; + +export default AccessFormDigitalOceanConfig; diff --git a/ui/src/domain/access.ts b/ui/src/domain/access.ts index 1576fd67..32a1b128 100644 --- a/ui/src/domain/access.ts +++ b/ui/src/domain/access.ts @@ -170,6 +170,10 @@ export type AccessConfigForDeSEC = { token: string; }; +export type AccessConfigForDigitalOcean = { + accessToken: string; +}; + export type AccessConfigForDingTalkBot = { webhookUrl: string; secret?: string; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index 712530d7..6d1ad303 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -23,6 +23,7 @@ export const ACCESS_PROVIDERS = Object.freeze({ CLOUDNS: "cloudns", CMCCCLOUD: "cmcccloud", DESEC: "desec", + DIGITALOCEAN: "digitalocean", DINGTALKBOT: "dingtalkbot", DNSLA: "dnsla", DOGECLOUD: "dogecloud", @@ -139,6 +140,7 @@ export const accessProvidersMap: Maphttps://desec.readthedocs.io/en/latest/auth/tokens.html", + "access.form.digitalocean_access_token.label": "DigitalOcean access token", + "access.form.digitalocean_access_token.placeholder": "Please enter DigitalOcean access token", + "access.form.digitalocean_access_token.tooltip": "For more information, see https://docs.digitalocean.com/reference/api/create-personal-access-token/", "access.form.dingtalkbot_webhook_url.label": "DingTalk bot Webhook URL", "access.form.dingtalkbot_webhook_url.placeholder": "Please enter DingTalk bot Webhook URL", "access.form.dingtalkbot_webhook_url.tooltip": "For more information, see https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot", diff --git a/ui/src/i18n/locales/en/nls.provider.json b/ui/src/i18n/locales/en/nls.provider.json index e9122149..c623bb27 100644 --- a/ui/src/i18n/locales/en/nls.provider.json +++ b/ui/src/i18n/locales/en/nls.provider.json @@ -58,6 +58,7 @@ "provider.ctcccloud": "China Telecom Cloud (State Cloud)", "provider.cucccloud": "China Unicom Cloud", "provider.desec": "deSEC", + "provider.digitalocean": "DigitalOcean", "provider.dingtalkbot": "DingTalk Bot", "provider.dnsla": "DNS.LA", "provider.dogecloud": "Doge Cloud", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index e2cacc1f..f3a39ec2 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -146,6 +146,9 @@ "access.form.desec_token.label": "deSEC Token", "access.form.desec_token.placeholder": "请输入 deSEC Token", "access.form.desec_token.tooltip": "这是什么?请参阅 https://desec.readthedocs.io/en/latest/auth/tokens.html", + "access.form.digitalocean_access_token.label": "DigitalOcean Access Token", + "access.form.digitalocean_access_token.placeholder": "请输入 DigitalOcean Access Token", + "access.form.digitalocean_access_token.tooltip": "这是什么?请参阅 https://docs.digitalocean.com/reference/api/create-personal-access-token/", "access.form.dingtalkbot_webhook_url.label": "钉钉群机器人 Webhook 地址", "access.form.dingtalkbot_webhook_url.placeholder": "请输入钉钉群机器人 Webhook 地址", "access.form.dingtalkbot_webhook_url.tooltip": "这是什么?请参阅 https://open.dingtalk.com/document/orgapp/obtain-the-webhook-address-of-a-custom-robot", diff --git a/ui/src/i18n/locales/zh/nls.provider.json b/ui/src/i18n/locales/zh/nls.provider.json index a6b94779..a68a1a09 100644 --- a/ui/src/i18n/locales/zh/nls.provider.json +++ b/ui/src/i18n/locales/zh/nls.provider.json @@ -58,6 +58,7 @@ "provider.ctcccloud": "联通云", "provider.cucccloud": "天翼云", "provider.desec": "deSEC", + "provider.digitalocean": "DigitalOcean", "provider.dingtalkbot": "钉钉群机器人", "provider.dnsla": "DNS.LA", "provider.dogecloud": "多吉云",