feat(provider): new acme dns-01 provider: ovhcloud

This commit is contained in:
Fu Diwei
2025-11-05 23:04:53 +08:00
parent ab61cadd37
commit 03a9644307
13 changed files with 349 additions and 5 deletions
+1
View File
@@ -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
+4
View File
@@ -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=
@@ -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)
}
}
+14 -4
View File
@@ -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"`
+1 -1
View File
@@ -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")
@@ -0,0 +1,6 @@
package ovhcloud
const (
AUTH_METHOD_APPLICATION = "application"
AUTH_METHOD_OAUTH2 = "oauth2"
)
@@ -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
}
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1505 909" width="200" height="200"><path id="Layer" fill-rule="evenodd" fill="#000e9c" d="M1407.08 52.27L1246.84 335.62L1078.68 335.62L880.85 684.34L1049.01 684.34L922.39 908.24L1335.86 908.24C1529.74 664.53 1559.41 325.71 1407.08 52.27ZM592.01 908.24L1116.27 0.76L673.13 0.76L372.42 523.85L99.41 50.29C-54.9 323.73 -27.2 664.53 172.61 908.24L592.01 908.24Z" /></svg>

After

Width:  |  Height:  |  Size: 421 B

@@ -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<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.NETCUP]: AccessConfigFieldsProviderNetcup,
[ACCESS_PROVIDERS.NETLIFY]: AccessConfigFieldsProviderNetlify,
[ACCESS_PROVIDERS.NS1]: AccessConfigFieldsProviderNS1,
[ACCESS_PROVIDERS.OVHCLOUD]: AccessConfigFieldsProviderOVHcloud,
[ACCESS_PROVIDERS.PORKBUN]: AccessConfigFieldsProviderPorkbun,
[ACCESS_PROVIDERS.POWERDNS]: AccessConfigFieldsProviderPowerDNS,
[ACCESS_PROVIDERS.PROXMOXVE]: AccessConfigFieldsProviderProxmoxVE,
@@ -0,0 +1,183 @@
import { getI18n, useTranslation } from "react-i18next";
import { AutoComplete, Form, Input, Radio } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { useFormNestedFieldsContext } from "./_context";
const AUTH_METHOD_APPLICATION = "application" as const;
const AUTH_METHOD_OAUTH2 = "oauth2" as const;
const AccessConfigFormFieldsProviderOVHcloud = () => {
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 (
<>
<Form.Item name={[parentNamePath, "endpoint"]} initialValue={initialValues.endpoint} label={t("access.form.ovhcloud_endpoint.label")} rules={[formRule]}>
<AutoComplete
options={["ovh-eu", "ovh-us", "ovh-ca"].map((value) => ({ value }))}
placeholder={t("access.form.ovhcloud_endpoint.placeholder")}
filterOption={(inputValue, option) => option!.value.toLowerCase().includes(inputValue.toLowerCase())}
/>
</Form.Item>
<Form.Item
name={[parentNamePath, "authMethod"]}
initialValue={initialValues.authMethod}
label={t("access.form.ovhcloud_auth_method.label")}
rules={[formRule]}
>
<Radio.Group block>
<Radio.Button value={AUTH_METHOD_APPLICATION}>{t("access.form.ovhcloud_auth_method.option.application.label")}</Radio.Button>
<Radio.Button value={AUTH_METHOD_OAUTH2}>{t("access.form.ovhcloud_auth_method.option.oauth2.label")}</Radio.Button>
</Radio.Group>
</Form.Item>
<Show when={fieldAuthMethod === AUTH_METHOD_APPLICATION}>
<Form.Item
name={[parentNamePath, "applicationKey"]}
initialValue={initialValues.applicationKey}
label={t("access.form.ovhcloud_application_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ovhcloud_application_key.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.ovhcloud_application_key.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "applicationSecret"]}
initialValue={initialValues.applicationSecret}
label={t("access.form.ovhcloud_application_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ovhcloud_application_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ovhcloud_application_secret.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "consumerKey"]}
initialValue={initialValues.consumerKey}
label={t("access.form.ovhcloud_consumer_key.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ovhcloud_consumer_key.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ovhcloud_consumer_key.placeholder")} />
</Form.Item>
</Show>
<Show when={fieldAuthMethod === AUTH_METHOD_OAUTH2}>
<Form.Item
name={[parentNamePath, "clientId"]}
initialValue={initialValues.clientId}
label={t("access.form.ovhcloud_client_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ovhcloud_client_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.ovhcloud_client_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "clientSecret"]}
initialValue={initialValues.clientSecret}
label={t("access.form.ovhcloud_client_secret.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.ovhcloud_client_secret.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.ovhcloud_client_secret.placeholder")} />
</Form.Item>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
endpoint: "ovh-eu",
authMethod: AUTH_METHOD_APPLICATION,
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
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;
+2
View File
@@ -201,6 +201,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.NAMESILO, "provider.namesilo", "/imgs/providers/namesilo.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NETCUP, "provider.netcup", "/imgs/providers/netcup.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.NS1, "provider.ns1", "/imgs/providers/ns1.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.OVHCLOUD, "provider.ovhcloud", "/imgs/providers/ovhcloud.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.PORKBUN, "provider.porkbun", "/imgs/providers/porkbun.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.SPACESHIP, "provider.spaceship", "/imgs/providers/spaceship.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.VERCEL, "provider.vercel", "/imgs/providers/vercel.svg", [ACCESS_USAGES.DNS]],
@@ -418,6 +419,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.NETCUP, "provider.netcup"],
[ACME_DNS01_PROVIDERS.NETLIFY, "provider.netlify"],
[ACME_DNS01_PROVIDERS.NS1, "provider.ns1"],
[ACME_DNS01_PROVIDERS.OVHCLOUD, "provider.ovhcloud"],
[ACME_DNS01_PROVIDERS.PORKBUN, "provider.porkbun"],
[ACME_DNS01_PROVIDERS.SPACESHIP, "provider.spaceship"],
[ACME_DNS01_PROVIDERS.VERCEL, "provider.vercel"],
+21
View File
@@ -408,6 +408,27 @@
"access.form.ns1_api_key.label": "NS1 API key",
"access.form.ns1_api_key.placeholder": "Please enter NS1 API key",
"access.form.ns1_api_key.tooltip": "For more information, see <a href=\"https://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api\" target=\"_blank\">https://www.ibm.com/docs/en/ns1-connect?topic=introduction-using-api</a>",
"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 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"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 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"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 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"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 <a href=\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\" target=\"_blank\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>",
"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 <a href=\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\" target=\"_blank\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>",
"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 <a href=\"https://porkbun.com/api/json/v3/documentation#Authentication\" target=\"_blank\">https://porkbun.com/api/json/v3/documentation</a>",
+21
View File
@@ -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": "这是什么?请参阅 <a href=\"https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api\" target=\"_blank\">https://www.ibm.com/docs/zh/ns1-connect?topic=introduction-using-api</a>",
"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": "这是什么?请参阅 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"access.form.ovhcloud_application_secret.label": "OVHcloud Application Secret",
"access.form.ovhcloud_application_secret.placeholder": "请输入 OVHcloud Application Secret",
"access.form.ovhcloud_application_secret.tooltip": "这是什么?请参阅 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"access.form.ovhcloud_consumer_key.label": "OVHcloud Consumer Key",
"access.form.ovhcloud_consumer_key.placeholder": "请输入 OVHcloud Consumer Key",
"access.form.ovhcloud_consumer_key.tooltip": "这是什么?请参阅 <a href=\"https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/\" target=\"_blank\">https://docs.ovh.com/gb/en/customer/first-steps-with-ovh-api/</a>",
"access.form.ovhcloud_client_id.label": "OVHcloud Client ID",
"access.form.ovhcloud_client_id.placeholder": "请输入 OVHcloud Client ID",
"access.form.ovhcloud_client_id.tooltip": "这是什么?请参阅 <a href=\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\" target=\"_blank\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>",
"access.form.ovhcloud_client_secret.label": "OVHcloud Client Cecret",
"access.form.ovhcloud_client_secret.placeholder": "请输入 OVHcloud Client Secret",
"access.form.ovhcloud_client_secret.tooltip": "这是什么?请参阅 <a href=\"https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343\" target=\"_blank\">https://help.ovhcloud.com/csm/en-manage-service-account?id=kb_article_view&sysparm_article=KB0059343</a>",
"access.form.porkbun_api_key.label": "Porkbun API Key",
"access.form.porkbun_api_key.placeholder": "请输入 Porkbun API Key",
"access.form.porkbun_api_key.tooltip": "这是什么?请参阅 <a href=\"https://porkbun.com/api/json/v3/documentation#Authentication\" target=\"_blank\">https://porkbun.com/api/json/v3/documentation</a>",