From 03a9644307484c37063c33d04749cec3f1b3b2fc Mon Sep 17 00:00:00 2001 From: Fu Diwei Date: Wed, 5 Nov 2025 23:04:53 +0800 Subject: [PATCH] feat(provider): new acme dns-01 provider: ovhcloud --- go.mod | 1 + go.sum | 4 + internal/certapply/applicators/sp_ovhcloud.go | 35 ++++ internal/domain/access.go | 18 +- internal/domain/provider.go | 2 +- .../acme-dns01/providers/ovhcloud/consts.go | 6 + .../acme-dns01/providers/ovhcloud/ovhcloud.go | 58 ++++++ ui/public/imgs/providers/ovhcloud.svg | 1 + .../forms/AccessConfigFieldsProvider.tsx | 2 + .../AccessConfigFieldsProviderOVHcloud.tsx | 183 ++++++++++++++++++ ui/src/domain/provider.ts | 2 + ui/src/i18n/locales/en/nls.access.json | 21 ++ ui/src/i18n/locales/zh/nls.access.json | 21 ++ 13 files changed, 349 insertions(+), 5 deletions(-) create mode 100644 internal/certapply/applicators/sp_ovhcloud.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/consts.go create mode 100644 pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/ovhcloud.go create mode 100644 ui/public/imgs/providers/ovhcloud.svg create mode 100644 ui/src/components/access/forms/AccessConfigFieldsProviderOVHcloud.tsx diff --git a/go.mod b/go.mod index 88635d103..f9742529b 100644 --- a/go.mod +++ b/go.mod @@ -128,6 +128,7 @@ require ( github.com/nrdcg/desec v0.11.1 // indirect github.com/nrdcg/goacmedns v0.2.0 // indirect github.com/nrdcg/porkbun v0.4.0 // indirect + github.com/ovh/go-ovh v1.9.0 // indirect github.com/peterhellberg/link v1.2.0 // indirect github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect github.com/qiniu/dyn v1.3.0 // indirect diff --git a/go.sum b/go.sum index 02794a994..3613e26b2 100644 --- a/go.sum +++ b/go.sum @@ -649,6 +649,8 @@ github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27k github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/maxatome/go-testdeep v1.12.0 h1:Ql7Go8Tg0C1D/uMMX59LAoYK7LffeJQ6X2T04nTH68g= +github.com/maxatome/go-testdeep v1.12.0/go.mod h1:lPZc/HAcJMP92l7yI6TRz1aZN5URwUBUAfUNvrclaNM= github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= @@ -717,6 +719,8 @@ github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7y github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/opentracing/opentracing-go v1.2.0/go.mod h1:GxEUsuufX4nBwe+T+Wl9TAgYrxe9dPLANfrWvHYVTgc= github.com/openzipkin/zipkin-go v0.2.5/go.mod h1:KpXfKdgRDnnhsxw4pNIH9Md5lyFqKUa4YDFlwRYAMyE= +github.com/ovh/go-ovh v1.9.0 h1:6K8VoL3BYjVV3In9tPJUdT7qMx9h0GExN9EXx1r2kKE= +github.com/ovh/go-ovh v1.9.0/go.mod h1:cTVDnl94z4tl8pP1uZ/8jlVxntjSIf09bNcQ5TJSC7c= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 h1:2nosf3P75OZv2/ZO/9Px5ZgZ5gbKrzA3joN1QMfOGMQ= diff --git a/internal/certapply/applicators/sp_ovhcloud.go b/internal/certapply/applicators/sp_ovhcloud.go new file mode 100644 index 000000000..5fe9d80db --- /dev/null +++ b/internal/certapply/applicators/sp_ovhcloud.go @@ -0,0 +1,35 @@ +package applicators + +import ( + "fmt" + + "github.com/go-acme/lego/v4/challenge" + + "github.com/certimate-go/certimate/internal/domain" + "github.com/certimate-go/certimate/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud" + xmaps "github.com/certimate-go/certimate/pkg/utils/maps" +) + +func init() { + if err := ACMEDns01Registries.Register(domain.ACMEDns01ProviderTypeOVHcloud, func(options *ProviderFactoryOptions) (challenge.Provider, error) { + credentials := domain.AccessConfigForOVHcloud{} + if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil { + return nil, fmt.Errorf("failed to populate provider access config: %w", err) + } + + provider, err := ovhcloud.NewChallengeProvider(&ovhcloud.ChallengeProviderConfig{ + Endpoint: credentials.Endpoint, + AuthMethod: credentials.AuthMethod, + ApplicationKey: credentials.ApplicationKey, + ApplicationSecret: credentials.ApplicationSecret, + ConsumerKey: credentials.ConsumerKey, + ClientId: credentials.ClientId, + ClientSecret: credentials.ClientSecret, + DnsPropagationTimeout: options.DnsPropagationTimeout, + DnsTTL: options.DnsTTL, + }) + return provider, err + }); err != nil { + panic(err) + } +} diff --git a/internal/domain/access.go b/internal/domain/access.go index 20207c419..972c7fd37 100644 --- a/internal/domain/access.go +++ b/internal/domain/access.go @@ -351,6 +351,16 @@ type AccessConfigForNS1 struct { ApiKey string `json:"apiKey"` } +type AccessConfigForOVHcloud struct { + Endpoint string `json:"endpoint"` + AuthMethod string `json:"authMethod"` + ApplicationKey string `json:"applicationKey,omitempty"` + ApplicationSecret string `json:"applicationSecret,omitempty"` + ConsumerKey string `json:"consumerKey,omitempty"` + ClientId string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` +} + type AccessConfigForPorkbun struct { ApiKey string `json:"apiKey"` SecretApiKey string `json:"secretApiKey"` @@ -412,16 +422,16 @@ type AccessConfigForSpaceship struct { type AccessConfigForSSH struct { Host string `json:"host"` Port int32 `json:"port"` - AuthMethod string `json:"authMethod,omitempty"` - Username string `json:"username,omitempty"` + AuthMethod string `json:"authMethod"` + Username string `json:"username"` Password string `json:"password,omitempty"` Key string `json:"key,omitempty"` KeyPassphrase string `json:"keyPassphrase,omitempty"` JumpServers []struct { Host string `json:"host"` Port int32 `json:"port"` - AuthMethod string `json:"authMethod,omitempty"` - Username string `json:"username,omitempty"` + AuthMethod string `json:"authMethod"` + Username string `json:"username"` Password string `json:"password,omitempty"` Key string `json:"key,omitempty"` KeyPassphrase string `json:"keyPassphrase,omitempty"` diff --git a/internal/domain/provider.go b/internal/domain/provider.go index aa1c63556..c640c55b4 100644 --- a/internal/domain/provider.go +++ b/internal/domain/provider.go @@ -79,7 +79,7 @@ const ( AccessProviderTypeNetcup = AccessProviderType("netcup") AccessProviderTypeNetlify = AccessProviderType("netlify") AccessProviderTypeNS1 = AccessProviderType("ns1") - AccessProviderTypeOVHcloud = AccessProviderType("ovhcloud") // OVHcloud(预留) + AccessProviderTypeOVHcloud = AccessProviderType("ovhcloud") AccessProviderTypePorkbun = AccessProviderType("porkbun") AccessProviderTypePowerDNS = AccessProviderType("powerdns") AccessProviderTypeProxmoxVE = AccessProviderType("proxmoxve") diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/consts.go b/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/consts.go new file mode 100644 index 000000000..dd32fa900 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/consts.go @@ -0,0 +1,6 @@ +package ovhcloud + +const ( + AUTH_METHOD_APPLICATION = "application" + AUTH_METHOD_OAUTH2 = "oauth2" +) diff --git a/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/ovhcloud.go b/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/ovhcloud.go new file mode 100644 index 000000000..4d2b22c17 --- /dev/null +++ b/pkg/core/ssl-applicator/acme-dns01/providers/ovhcloud/ovhcloud.go @@ -0,0 +1,58 @@ +package ovhcloud + +import ( + "errors" + "fmt" + "time" + + "github.com/go-acme/lego/v4/providers/dns/ovh" + + "github.com/certimate-go/certimate/pkg/core" +) + +type ChallengeProviderConfig struct { + Endpoint string `json:"endpoint"` + AuthMethod string `json:"authMethod"` + ApplicationKey string `json:"applicationKey,omitempty"` + ApplicationSecret string `json:"applicationSecret,omitempty"` + ConsumerKey string `json:"consumerKey,omitempty"` + ClientId string `json:"clientId,omitempty"` + ClientSecret string `json:"clientSecret,omitempty"` + DnsPropagationTimeout int32 `json:"dnsPropagationTimeout,omitempty"` + DnsTTL int32 `json:"dnsTTL,omitempty"` +} + +func NewChallengeProvider(config *ChallengeProviderConfig) (core.ACMEChallenger, error) { + if config == nil { + return nil, errors.New("the configuration of the acme challenge provider is nil") + } + + providerConfig := ovh.NewDefaultConfig() + providerConfig.APIEndpoint = config.Endpoint + switch config.AuthMethod { + case AUTH_METHOD_APPLICATION: + providerConfig.ApplicationKey = config.ApplicationKey + providerConfig.ApplicationSecret = config.ApplicationSecret + providerConfig.ConsumerKey = config.ConsumerKey + case AUTH_METHOD_OAUTH2: + providerConfig.OAuth2Config = &ovh.OAuth2Config{ + ClientID: config.ClientId, + ClientSecret: config.ClientSecret, + } + default: + return nil, fmt.Errorf("unsupported auth method '%s'", config.AuthMethod) + } + if config.DnsPropagationTimeout != 0 { + providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second + } + if config.DnsTTL != 0 { + providerConfig.TTL = int(config.DnsTTL) + } + + provider, err := ovh.NewDNSProviderConfig(providerConfig) + if err != nil { + return nil, err + } + + return provider, nil +} diff --git a/ui/public/imgs/providers/ovhcloud.svg b/ui/public/imgs/providers/ovhcloud.svg new file mode 100644 index 000000000..b29b804d5 --- /dev/null +++ b/ui/public/imgs/providers/ovhcloud.svg @@ -0,0 +1 @@ + diff --git a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx index 2d59bf3a0..135c7a80d 100644 --- a/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx +++ b/ui/src/components/access/forms/AccessConfigFieldsProvider.tsx @@ -65,6 +65,7 @@ import AccessConfigFieldsProviderNameSilo from "./AccessConfigFieldsProviderName import AccessConfigFieldsProviderNetcup from "./AccessConfigFieldsProviderNetcup"; import AccessConfigFieldsProviderNetlify from "./AccessConfigFieldsProviderNetlify"; import AccessConfigFieldsProviderNS1 from "./AccessConfigFieldsProviderNS1"; +import AccessConfigFieldsProviderOVHcloud from "./AccessConfigFieldsProviderOVHcloud"; import AccessConfigFieldsProviderPorkbun from "./AccessConfigFieldsProviderPorkbun"; import AccessConfigFieldsProviderPowerDNS from "./AccessConfigFieldsProviderPowerDNS"; import AccessConfigFieldsProviderProxmoxVE from "./AccessConfigFieldsProviderProxmoxVE"; @@ -161,6 +162,7 @@ const providerComponentMap: Partial { + const { i18n, t } = useTranslation(); + + const { parentNamePath } = useFormNestedFieldsContext(); + const formSchema = z.object({ + [parentNamePath]: getSchema({ i18n }), + }); + const formRule = createSchemaFieldRule(formSchema); + const formInst = Form.useFormInstance(); + const initialValues = getInitialValues(); + + const fieldAuthMethod = Form.useWatch([parentNamePath, "authMethod"], formInst); + + return ( + <> + + ({ value }))} + placeholder={t("access.form.ovhcloud_endpoint.placeholder")} + filterOption={(inputValue, option) => option!.value.toLowerCase().includes(inputValue.toLowerCase())} + /> + + + + + {t("access.form.ovhcloud_auth_method.option.application.label")} + {t("access.form.ovhcloud_auth_method.option.oauth2.label")} + + + + + } + > + + + + } + > + + + + } + > + + + + + + } + > + + + + } + > + + + + + ); +}; + +const getInitialValues = (): Nullish>> => { + return { + endpoint: "ovh-eu", + authMethod: AUTH_METHOD_APPLICATION, + }; +}; + +const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType }) => { + const { t } = i18n; + + return z + .object({ + endpoint: z.string().nonempty(t("access.form.ovhcloud_endpoint.placeholder")), + authMethod: z.literal([AUTH_METHOD_APPLICATION, AUTH_METHOD_OAUTH2], t("access.form.ovhcloud_auth_method.placeholder")), + applicationKey: z.string().nullish(), + applicationSecret: z.string().nullish(), + consumerKey: z.string().nullish(), + clientId: z.string().nullish(), + clientSecret: z.string().nullish(), + }) + .superRefine((values, ctx) => { + switch (values.authMethod) { + case AUTH_METHOD_APPLICATION: + { + if (!values.applicationKey?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("access.form.ovhcloud_application_key.placeholder"), + path: ["applicationKey"], + }); + } + + if (!values.applicationSecret?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("access.form.ovhcloud_application_secret.placeholder"), + path: ["applicationSecret"], + }); + } + + if (!values.consumerKey?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("access.form.ovhcloud_consumer_key.placeholder"), + path: ["consumerKey"], + }); + } + } + break; + + case AUTH_METHOD_OAUTH2: + { + if (!values.clientId?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("access.form.ovhcloud_client_id.placeholder"), + path: ["clientId"], + }); + } + + if (!values.clientSecret?.trim()) { + ctx.addIssue({ + code: "custom", + message: t("access.form.ovhcloud_client_secret.placeholder"), + path: ["clientSecret"], + }); + } + } + break; + } + }); +}; + +const _default = Object.assign(AccessConfigFormFieldsProviderOVHcloud, { + getInitialValues, + getSchema, +}); + +export default _default; diff --git a/ui/src/domain/provider.ts b/ui/src/domain/provider.ts index ddc9904f2..ab93f5573 100644 --- a/ui/src/domain/provider.ts +++ b/ui/src/domain/provider.ts @@ -201,6 +201,7 @@ export const accessProvidersMap: Maphttps://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api", + "access.form.ovhcloud_endpoint.label": "OVHcloud API endpoint", + "access.form.ovhcloud_endpoint.placeholder": "Please enter OVHcloud API endpoint", + "access.form.ovhcloud_auth_method.label": "OVHcloud API authentication method", + "access.form.ovhcloud_auth_method.placeholder": "Please select OVHcloud API authentication method", + "access.form.ovhcloud_auth_method.option.application.label": "Application key & secret", + "access.form.ovhcloud_auth_method.option.oauth2.label": "OAuth2 client credentials", + "access.form.ovhcloud_application_key.label": "OVHcloud application key", + "access.form.ovhcloud_application_key.placeholder": "Please enter OVHcloud application key", + "access.form.ovhcloud_application_key.tooltip": "For more information, see https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_application_secret.label": "OVHcloud application secret", + "access.form.ovhcloud_application_secret.placeholder": "Please enter OVHcloud application secret", + "access.form.ovhcloud_application_secret.tooltip": "For more information, see https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_consumer_key.label": "OVHcloud consumer key", + "access.form.ovhcloud_consumer_key.placeholder": "Please enter OVHcloud consumer key", + "access.form.ovhcloud_consumer_key.tooltip": "For more information, see https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_client_id.label": "OVHcloud client ID", + "access.form.ovhcloud_client_id.placeholder": "Please enter OVHcloud client ID", + "access.form.ovhcloud_client_id.tooltip": "For more information, see https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343", + "access.form.ovhcloud_client_secret.label": "OVHcloud client secret", + "access.form.ovhcloud_client_secret.placeholder": "Please enter OVHcloud client secret", + "access.form.ovhcloud_client_secret.tooltip": "For more information, see https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343", "access.form.porkbun_api_key.label": "Porkbun API key", "access.form.porkbun_api_key.placeholder": "Please enter Porkbun API key", "access.form.porkbun_api_key.tooltip": "For more information, see https://porkbun.com/api/json/v3/documentation", diff --git a/ui/src/i18n/locales/zh/nls.access.json b/ui/src/i18n/locales/zh/nls.access.json index 37f8eb381..dbf7cb237 100644 --- a/ui/src/i18n/locales/zh/nls.access.json +++ b/ui/src/i18n/locales/zh/nls.access.json @@ -407,6 +407,27 @@ "access.form.ns1_api_key.label": "NS1 API Key", "access.form.ns1_api_key.placeholder": "请输入 NS1 API Key", "access.form.ns1_api_key.tooltip": "这是什么?请参阅 https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api", + "access.form.ovhcloud_endpoint.label": "OVHcloud API 端点", + "access.form.ovhcloud_endpoint.placeholder": "请输入 OVHcloud API 端点", + "access.form.ovhcloud_auth_method.label": "OVHcloud API 认证方式", + "access.form.ovhcloud_auth_method.placeholder": "请选择 OVHcloud API 认证方式", + "access.form.ovhcloud_auth_method.option.application.label": "Application Key & Secret", + "access.form.ovhcloud_auth_method.option.oauth2.label": "OAuth2 Client Credentials", + "access.form.ovhcloud_application_key.label": "OVHcloud Application Key", + "access.form.ovhcloud_application_key.placeholder": "请输入 OVHcloud Application Key", + "access.form.ovhcloud_application_key.tooltip": "这是什么?请参阅 https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_application_secret.label": "OVHcloud Application Secret", + "access.form.ovhcloud_application_secret.placeholder": "请输入 OVHcloud Application Secret", + "access.form.ovhcloud_application_secret.tooltip": "这是什么?请参阅 https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_consumer_key.label": "OVHcloud Consumer Key", + "access.form.ovhcloud_consumer_key.placeholder": "请输入 OVHcloud Consumer Key", + "access.form.ovhcloud_consumer_key.tooltip": "这是什么?请参阅 https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/", + "access.form.ovhcloud_client_id.label": "OVHcloud Client ID", + "access.form.ovhcloud_client_id.placeholder": "请输入 OVHcloud Client ID", + "access.form.ovhcloud_client_id.tooltip": "这是什么?请参阅 https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343", + "access.form.ovhcloud_client_secret.label": "OVHcloud Client Cecret", + "access.form.ovhcloud_client_secret.placeholder": "请输入 OVHcloud Client Secret", + "access.form.ovhcloud_client_secret.tooltip": "这是什么?请参阅 https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343", "access.form.porkbun_api_key.label": "Porkbun API Key", "access.form.porkbun_api_key.placeholder": "请输入 Porkbun API Key", "access.form.porkbun_api_key.tooltip": "这是什么?请参阅 https://porkbun.com/api/json/v3/documentation",