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

This commit is contained in:
Fu Diwei
2026-05-29 23:47:06 +08:00
committed by RHQYZ
parent c89da616c6
commit 795e8df4ad
24 changed files with 1113 additions and 14 deletions
@@ -0,0 +1,43 @@
package certifiers
import (
"fmt"
"github.com/certimate-go/certimate/internal/domain"
"github.com/certimate-go/certimate/pkg/core"
conohavpsv2 "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/conohavpsv2"
conohavpsv3 "github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/conohavpsv3"
xmaps "github.com/certimate-go/certimate/pkg/utils/maps"
)
func init() {
ACMEDns01Registries.MustRegister(domain.ACMEDns01ProviderTypeConoHaVPS, func(options *ProviderFactoryOptions) (core.ACMEChallenger, error) {
credentials := domain.AccessConfigForConoHaVPS{}
if err := xmaps.Populate(options.ProviderAccessConfig, &credentials); err != nil {
return nil, fmt.Errorf("failed to populate provider access config: %w", err)
}
switch credentials.ApiVersion {
case "2", "2.0", "v2", "v2.0":
return conohavpsv2.NewChallenger(&conohavpsv2.ChallengerConfig{
ApiUserName: credentials.ApiUserName,
ApiPassword: credentials.ApiPassword,
TenantId: credentials.TenantId,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
case "3", "3.0", "v3", "v3.0":
return conohavpsv3.NewChallenger(&conohavpsv3.ChallengerConfig{
ApiUserId: credentials.ApiUserId,
ApiUserName: credentials.ApiUserName,
ApiPassword: credentials.ApiPassword,
TenantId: credentials.TenantId,
TenantName: credentials.TenantName,
DnsPropagationTimeout: options.DnsPropagationTimeout,
DnsTTL: options.DnsTTL,
})
default:
return nil, fmt.Errorf("conohavps: unsupported api version: '%s'", credentials.ApiVersion)
}
})
}
+9
View File
@@ -160,6 +160,15 @@ type AccessConfigForCMCCCloud struct {
AccessKeySecret string `json:"accessKeySecret"`
}
type AccessConfigForConoHaVPS struct {
ApiVersion string `json:"apiVersion"`
ApiUserId string `json:"apiUserId,omitempty"`
ApiUserName string `json:"apiUserName"`
ApiPassword string `json:"apiPassword"`
TenantId string `json:"tenantId"`
TenantName string `json:"tenantName,omitempty"`
}
type AccessConfigForConstellix struct {
ApiKey string `json:"apiKey"`
SecretKey string `json:"secretKey"`
+2
View File
@@ -39,6 +39,7 @@ const (
AccessProviderTypeCloudflare = AccessProviderType("cloudflare")
AccessProviderTypeClouDNS = AccessProviderType("cloudns")
AccessProviderTypeCMCCCloud = AccessProviderType("cmcccloud")
AccessProviderTypeConoHaVPS = AccessProviderType("conohavps")
AccessProviderTypeConstellix = AccessProviderType("constellix")
AccessProviderTypeCPanel = AccessProviderType("cpanel")
AccessProviderTypeCTCCCloud = AccessProviderType("ctcccloud")
@@ -202,6 +203,7 @@ const (
ACMEDns01ProviderTypeClouDNS = ACMEDns01ProviderType(AccessProviderTypeClouDNS)
ACMEDns01ProviderTypeCMCCCloud = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCMCCCloudDNS]
ACMEDns01ProviderTypeCMCCCloudDNS = ACMEDns01ProviderType(AccessProviderTypeCMCCCloud + "-dns")
ACMEDns01ProviderTypeConoHaVPS = ACMEDns01ProviderType(AccessProviderTypeConoHaVPS)
ACMEDns01ProviderTypeConstellix = ACMEDns01ProviderType(AccessProviderTypeConstellix)
ACMEDns01ProviderTypeCPanel = ACMEDns01ProviderType(AccessProviderTypeCPanel)
ACMEDns01ProviderTypeCTCCCloud = ACMEDns01ProviderType(AccessProviderTypeCTCCCloud) // 兼容旧值,等同于 [ACMEDns01ProviderTypeCTCCCloudSmartDNS]
@@ -0,0 +1,42 @@
package conohavpsv2
import (
"fmt"
"time"
"github.com/go-acme/lego/v5/providers/dns/conoha"
"github.com/certimate-go/certimate/pkg/core/certifier"
)
type ChallengerConfig struct {
ApiUserName string `json:"apiUserName"`
ApiPassword string `json:"apiPassword"`
TenantId string `json:"tenantId"`
DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int `json:"dnsTTL,omitempty"`
}
func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
}
providerConfig := conoha.NewDefaultConfig()
providerConfig.Username = config.ApiUserName
providerConfig.Password = config.ApiPassword
providerConfig.TenantID = config.TenantId
if config.DnsPropagationTimeout != 0 {
providerConfig.PropagationTimeout = time.Duration(config.DnsPropagationTimeout) * time.Second
}
if config.DnsTTL != 0 {
providerConfig.TTL = config.DnsTTL
}
provider, err := conoha.NewDNSProviderConfig(providerConfig)
if err != nil {
return nil, err
}
return provider, nil
}
@@ -0,0 +1,45 @@
package conohavpsv2
import (
"fmt"
"time"
"github.com/certimate-go/certimate/pkg/core/certifier"
"github.com/certimate-go/certimate/pkg/core/certifier/challengers/dns01/conohavpsv3/internal"
)
type ChallengerConfig struct {
ApiUserId string `json:"apiUserId"`
ApiUserName string `json:"apiUserName"`
ApiPassword string `json:"apiPassword"`
TenantId string `json:"tenantId"`
TenantName string `json:"tenantName"`
DnsPropagationTimeout int `json:"dnsPropagationTimeout,omitempty"`
DnsTTL int `json:"dnsTTL,omitempty"`
}
func NewChallenger(config *ChallengerConfig) (certifier.ACMEChallenger, error) {
if config == nil {
return nil, fmt.Errorf("the configuration of the acme challenge provider is nil")
}
providerConfig := internal.NewDefaultConfig()
providerConfig.UserID = config.ApiUserId
providerConfig.UserName = config.ApiUserName
providerConfig.Password = config.ApiPassword
providerConfig.TenantID = config.TenantId
providerConfig.TenantName = config.TenantName
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
}
@@ -0,0 +1,200 @@
package internal
import (
"context"
"fmt"
"sync"
"time"
"github.com/go-acme/lego/v5/challenge"
"github.com/go-acme/lego/v5/challenge/dns01"
"github.com/go-acme/lego/v5/platform/env"
"github.com/samber/lo"
conohavpssdk "github.com/certimate-go/certimate/pkg/sdk3rd/conoha/vps/v3"
)
const (
envNamespace = "CONOHAV3_"
EnvAPIUserID = envNamespace + "API_USER_ID"
EnvAPIUserName = envNamespace + "API_USER_NAME"
EnvAPIPassword = envNamespace + "API_PASSWORD"
EnvTenantID = envNamespace + "TENANT_ID"
EnvTenantName = envNamespace + "TENANT_NAME"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
UserID string
UserName string
Password string
TenantID string
TenantName string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPTimeout time.Duration
}
type DNSProvider struct {
config *Config
client *conohavpssdk.Client
zoneIDs map[string]string // Key: ZoneFQDN; Value: ZoneUUID
zoneIDsMu sync.Mutex
recordIDs map[string]string // Key: ChallengeToken; Value: RecordUUID
recordIDsMu sync.Mutex
}
func NewDefaultConfig() *Config {
return &Config{
TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
}
}
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvAPIUserID, EnvAPIUserName, EnvAPIPassword, EnvTenantID, EnvTenantName)
if err != nil {
return nil, fmt.Errorf("conohavpsv3: %w", err)
}
config := NewDefaultConfig()
config.UserID = values[EnvAPIUserID]
config.UserName = values[EnvAPIUserName]
config.Password = values[EnvAPIPassword]
config.TenantID = values[EnvTenantID]
config.TenantName = values[EnvTenantName]
return NewDNSProviderConfig(config)
}
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, fmt.Errorf("conohavpsv3: the configuration of the DNS provider is nil")
}
client, err := conohavpssdk.NewClient(config.UserID, config.UserName, config.Password, config.TenantID, config.TenantName)
if err != nil {
return nil, fmt.Errorf("conohavpsv3: %w", err)
} else {
client.SetTimeout(config.HTTPTimeout)
}
return &DNSProvider{
config: config,
client: client,
zoneIDs: make(map[string]string),
zoneIDsMu: sync.Mutex{},
recordIDs: make(map[string]string),
recordIDsMu: sync.Mutex{},
}, nil
}
func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("conohavpsv3: could not find zone for domain %q: %w", domain, err)
}
zoneInfo, err := d.findZone(ctx, authZone)
if err != nil {
return fmt.Errorf("conohavpsv3: error when list zones: %w", err)
}
// REF: https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-create_record-v3/
response, err := d.client.DnsCreateRecordWithContext(ctx, zoneInfo.UUID, &conohavpssdk.DnsCreateRecordRequest{
Name: lo.ToPtr(info.EffectiveFQDN),
Type: lo.ToPtr("TXT"),
Data: lo.ToPtr(info.Value),
TTL: lo.ToPtr(d.config.TTL),
})
if err != nil {
return fmt.Errorf("conohavpsv3: error when create record: %w", err)
}
d.zoneIDsMu.Lock()
d.zoneIDs[authZone] = zoneInfo.UUID
d.zoneIDsMu.Unlock()
d.recordIDsMu.Lock()
d.recordIDs[token] = response.UUID
d.recordIDsMu.Unlock()
return nil
}
func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string) error {
info := dns01.GetChallengeInfo(ctx, domain, keyAuth)
authZone, err := dns01.DefaultClient().FindZoneByFqdn(ctx, info.EffectiveFQDN)
if err != nil {
return fmt.Errorf("conohavpsv3: could not find zone for domain %q: %w", domain, err)
}
d.zoneIDsMu.Lock()
zoneId, ok := d.zoneIDs[authZone]
d.zoneIDsMu.Unlock()
if !ok {
return fmt.Errorf("conohavpsv3: unknown zone ID for '%s'", authZone)
}
d.recordIDsMu.Lock()
recordId, ok := d.recordIDs[token]
d.recordIDsMu.Unlock()
if !ok {
return fmt.Errorf("conohavpsv3: unknown record ID for '%s'", info.EffectiveFQDN)
}
if _, err := d.client.DnsDeleteRecordWithContext(ctx, zoneId, recordId); err != nil {
return fmt.Errorf("conohavpsv3: error when delete record: %w", err)
}
return nil
}
func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
return d.config.PropagationTimeout, d.config.PollingInterval
}
func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*conohavpssdk.DnsDomainRecord, error) {
offset := 0
limit := 10
for {
// REF: https://doc.conoha.jp/reference/api-vps3/api-dns-vps3/dnsaas-get_domains_list-v3/
request := &conohavpssdk.DnsGetDomainsListRequest{
Offset: lo.ToPtr(offset),
Limit: lo.ToPtr(limit),
}
response, err := d.client.DnsGetDomainsListWithContext(ctx, request)
if err != nil {
return nil, err
}
for _, domainItem := range response.Domains {
if dns01.UnFqdn(domainItem.Name) == dns01.UnFqdn(zoneName) {
return domainItem, nil
}
}
if len(response.Domains) < limit || offset+limit >= response.TotalCount {
break
}
offset += limit
}
return nil, fmt.Errorf("could not find zone '%s'", zoneName)
}
@@ -32,9 +32,9 @@ type Config struct {
AccessKeyId string
SecretAccessKey string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
@@ -30,9 +30,9 @@ var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
type Config struct {
HTTPToken string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
@@ -40,7 +40,7 @@ type DNSProvider struct {
config *Config
client *dynv6sdk.Client
zoneIDs map[string]int64 // Key: ZoneName; Value: ZoneID
zoneIDs map[string]int64 // Key: ZoneFQDN; Value: ZoneID
zoneIDsMu sync.Mutex
recordIDs map[string]int64 // Key: ChallengeToken; Value: RecordID
recordIDsMu sync.Mutex
@@ -102,13 +102,13 @@ func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string
return fmt.Errorf("dynv6: %w", err)
}
zone, err := d.findZone(ctx, dns01.UnFqdn(authZone))
zoneInfo, err := d.findZone(ctx, authZone)
if err != nil {
return fmt.Errorf("dynv6: error when list zones: %w", err)
}
// REF: https://dynv6.github.io/api-spec/#tag/records/operation/addRecord
response, err := d.client.AddRecordWithContext(ctx, zone.ID, &dynv6sdk.AddRecordRequest{
response, err := d.client.AddRecordWithContext(ctx, zoneInfo.ID, &dynv6sdk.AddRecordRequest{
Type: lo.ToPtr("TXT"),
Name: lo.ToPtr(subDomain),
Data: lo.ToPtr(info.Value),
@@ -118,7 +118,7 @@ func (d *DNSProvider) Present(ctx context.Context, domain, token, keyAuth string
}
d.zoneIDsMu.Lock()
d.zoneIDs[zone.Name] = zone.ID
d.zoneIDs[authZone] = zoneInfo.ID
d.zoneIDsMu.Unlock()
d.recordIDsMu.Lock()
@@ -137,10 +137,10 @@ func (d *DNSProvider) CleanUp(ctx context.Context, domain, token, keyAuth string
}
d.zoneIDsMu.Lock()
zoneId, ok := d.zoneIDs[dns01.UnFqdn(authZone)]
zoneId, ok := d.zoneIDs[authZone]
d.zoneIDsMu.Unlock()
if !ok {
return fmt.Errorf("dynv6: unknown zone ID for '%s'", dns01.UnFqdn(authZone))
return fmt.Errorf("dynv6: unknown zone ID for '%s'", authZone)
}
d.recordIDsMu.Lock()
@@ -169,7 +169,7 @@ func (d *DNSProvider) findZone(ctx context.Context, zoneName string) (*dynv6sdk.
}
for _, zone := range *zones {
if zone.Name == zoneName {
if dns01.UnFqdn(zone.Name) == dns01.UnFqdn(zoneName) {
return zone, nil
}
}
@@ -32,9 +32,9 @@ type Config struct {
AccessKey string
AccessSecret string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
TTL int
HTTPTimeout time.Duration
}
@@ -0,0 +1,57 @@
package v3
import (
"context"
"fmt"
"net/http"
"net/url"
)
type DnsCreateRecordRequest struct {
Name *string `json:"name,omitempty"`
Type *string `json:"type,omitempty"`
Data *string `json:"data,omitempty"`
TTL *int `json:"ttl,omitempty"`
}
type DnsCreateRecordResponse struct {
sdkResponseBase
UUID string `json:"uuid"`
DomainUUID string `json:"domain_uuid"`
Name string `json:"name"`
Type string `json:"type"`
Data string `json:"data"`
TTL int `json:"ttl"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
func (c *Client) DnsCreateRecord(domainId string, req *DnsCreateRecordRequest) (*DnsCreateRecordResponse, error) {
return c.DnsCreateRecordWithContext(context.Background(), domainId, req)
}
func (c *Client) DnsCreateRecordWithContext(ctx context.Context, domainId string, req *DnsCreateRecordRequest) (*DnsCreateRecordResponse, error) {
if domainId == "" {
return nil, fmt.Errorf("sdkerr: unset domainId")
}
if err := c.ensureAccessTokenExists(); err != nil {
return nil, err
}
httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("%s/v1/domains/%s/records", dnsBaseURL, url.PathEscape(domainId)))
if err != nil {
return nil, err
} else {
httpreq.SetBody(req)
httpreq.SetContext(ctx)
}
result := &DnsCreateRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,43 @@
package v3
import (
"context"
"fmt"
"net/http"
"net/url"
)
type DnsDeleteRecordResponse struct {
sdkResponseBase
}
func (c *Client) DnsDeleteRecord(domainId string, recordId string) (*DnsDeleteRecordResponse, error) {
return c.DnsDeleteRecordWithContext(context.Background(), domainId, recordId)
}
func (c *Client) DnsDeleteRecordWithContext(ctx context.Context, domainId string, recordId string) (*DnsDeleteRecordResponse, error) {
if domainId == "" {
return nil, fmt.Errorf("sdkerr: unset domainId")
}
if recordId == "" {
return nil, fmt.Errorf("sdkerr: unset recordId")
}
if err := c.ensureAccessTokenExists(); err != nil {
return nil, err
}
httpreq, err := c.newRequest(http.MethodDelete, fmt.Sprintf("%s/v1/domains/%s/records/%s", dnsBaseURL, url.PathEscape(domainId), url.PathEscape(recordId)))
if err != nil {
return nil, err
} else {
httpreq.SetContext(ctx)
}
result := &DnsDeleteRecordResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
@@ -0,0 +1,53 @@
package v3
import (
"context"
"fmt"
"net/http"
qs "github.com/google/go-querystring/query"
)
type DnsGetDomainsListRequest struct {
Limit *int `json:"limit,omitempty" url:"limit,omitempty"`
Offset *int `json:"offset,omitempty" url:"offset,omitempty"`
SortType *string `json:"sort_type,omitempty" url:"sort_type,omitempty"`
SortKey *string `json:"sort_key,omitempty" url:"sort_key,omitempty"`
}
type DnsGetDomainsListResponse struct {
sdkResponseBase
Domains []*DnsDomainRecord `json:"domains,omitempty"`
TotalCount int `json:"total_count,omitempty"`
}
func (c *Client) DnsGetDomainsList(req *DnsGetDomainsListRequest) (*DnsGetDomainsListResponse, error) {
return c.DnsGetDomainsListWithContext(context.Background(), req)
}
func (c *Client) DnsGetDomainsListWithContext(ctx context.Context, req *DnsGetDomainsListRequest) (*DnsGetDomainsListResponse, error) {
if err := c.ensureAccessTokenExists(); err != nil {
return nil, err
}
httpreq, err := c.newRequest(http.MethodGet, fmt.Sprintf("%s/v1/domains", dnsBaseURL))
if err != nil {
return nil, err
} else {
values, err := qs.Values(req)
if err != nil {
return nil, err
}
httpreq.SetQueryParamsFromValues(values)
httpreq.SetContext(ctx)
}
result := &DnsGetDomainsListResponse{}
if _, err := c.doRequestWithResult(httpreq, result); err != nil {
return result, err
}
return result, nil
}
+202
View File
@@ -0,0 +1,202 @@
package v3
import (
"crypto/tls"
"encoding/json"
"fmt"
"net/http"
"sync"
"time"
"github.com/go-resty/resty/v2"
"github.com/certimate-go/certimate/internal/app"
)
type Client struct {
userId string
userName string
userPassword string
tenantId string
tenantName string
accessToken string
accessTokenExp time.Time
accessTokenMtx sync.Mutex
client *resty.Client
}
func NewClient(userId, userName, userPassword, tenantId, tenantName string) (*Client, error) {
if userId == "" && userName == "" {
return nil, fmt.Errorf("sdkerr: unset userId or userName")
}
if userPassword == "" {
return nil, fmt.Errorf("sdkerr: unset userPassword")
}
if tenantId == "" || tenantName == "" {
return nil, fmt.Errorf("sdkerr: unset tenantId or tenantName")
}
client := &Client{
userId: userId,
userName: userName,
userPassword: userPassword,
tenantId: tenantId,
tenantName: tenantName,
}
client.client = resty.New().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
SetHeader("User-Agent", app.AppUserAgent).
SetPreRequestHook(func(c *resty.Client, req *http.Request) error {
if client.accessToken != "" {
req.Header.Set("X-Auth-Token", client.accessToken)
}
return nil
})
return client, nil
}
func (c *Client) SetTimeout(timeout time.Duration) *Client {
c.client.SetTimeout(timeout)
return c
}
func (c *Client) SetTLSConfig(config *tls.Config) *Client {
c.client.SetTLSClientConfig(config)
return c
}
func (c *Client) newRequest(method string, path string) (*resty.Request, error) {
if method == "" {
return nil, fmt.Errorf("sdkerr: unset method")
}
if path == "" {
return nil, fmt.Errorf("sdkerr: unset path")
}
req := c.client.R()
req.Method = method
req.URL = path
return req, nil
}
func (c *Client) doRequest(req *resty.Request) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
// WARN:
// PLEASE DO NOT USE `req.SetResult` or `req.SetError` HERE! USE `doRequestWithResult` INSTEAD.
resp, err := req.Send()
if err != nil {
return resp, fmt.Errorf("sdkerr: failed to send request: %w", err)
} else if resp.IsError() {
return resp, fmt.Errorf("sdkerr: unexpected status code: %d (resp: %s)", resp.StatusCode(), resp.String())
}
return resp, nil
}
func (c *Client) doRequestWithResult(req *resty.Request, res sdkResponse) (*resty.Response, error) {
if req == nil {
return nil, fmt.Errorf("sdkerr: nil request")
}
resp, err := c.doRequest(req)
if err != nil {
if resp != nil {
json.Unmarshal(resp.Body(), &res)
}
return resp, err
}
if len(resp.Body()) != 0 {
if err := json.Unmarshal(resp.Body(), &res); err != nil {
return resp, fmt.Errorf("sdkerr: failed to unmarshal response: %w (resp: %s)", err, resp.String())
} else {
if tcode := res.GetCode(); tcode == 0 {
return resp, fmt.Errorf("sdkerr: code='%d', error='%s'", tcode, res.GetError())
}
}
}
return resp, nil
}
func (c *Client) ensureAccessTokenExists() error {
c.accessTokenMtx.Lock()
defer c.accessTokenMtx.Unlock()
if c.accessToken != "" && c.accessTokenExp.After(time.Now()) {
return nil
}
httpreq, err := c.newRequest(http.MethodPost, fmt.Sprintf("%s/v3/auth/tokens", identityBaseURL))
if err != nil {
return err
} else {
authUserParams := map[string]string{"password": c.userPassword}
if c.userId != "" {
authUserParams["id"] = c.userId
}
if c.userName != "" {
authUserParams["name"] = c.userName
}
authProjectParams := map[string]string{}
if c.tenantId != "" {
authProjectParams["id"] = c.tenantId
}
if c.tenantName != "" {
authProjectParams["name"] = c.tenantName
}
httpreq.SetBody(map[string]any{
"auth": map[string]any{
"identity": map[string]any{
"methods": []string{"password"},
"password": map[string]any{
"user": authUserParams,
},
"scope": map[string]any{
"project": authProjectParams,
},
},
},
})
}
type createAuthTokenResponse struct {
sdkResponseBase
Token *struct {
IssuedAt string `json:"issued_at"`
ExpiresAt string `json:"expires_at"`
} `json:"token,omitempty"`
}
result := &createAuthTokenResponse{}
if httpresp, err := c.doRequestWithResult(httpreq, result); err != nil {
return err
} else if code := result.GetCode(); code != 0 {
return fmt.Errorf("sdkerr: failed to get conoha access token: code='%d', error='%s'", code, result.GetError())
} else {
token := httpresp.Header().Get("X-Subject-Token")
if token == "" {
return fmt.Errorf("sdkerr: api error: received empty auth token")
}
tokenExp, err := time.Parse(time.RFC3339Nano, result.Token.ExpiresAt)
if err != nil {
return fmt.Errorf("sdkerr: api error: received invalid auth token expiration: %w", err)
}
c.accessToken = token
c.accessTokenExp = tokenExp
}
return nil
}
+10
View File
@@ -0,0 +1,10 @@
package v3
import "fmt"
const region = "c3j1"
var (
identityBaseURL = fmt.Sprintf("https://identity.%s.conoha.io", region)
dnsBaseURL = fmt.Sprintf("https://dns-service.%s.conoha.io", region)
)
+36
View File
@@ -0,0 +1,36 @@
package v3
type sdkResponse interface {
GetCode() int
GetError() string
}
type sdkResponseBase struct {
Code *int `json:"code,omitempty"`
Error *string `json:"error,omitempty"`
}
func (r *sdkResponseBase) GetCode() int {
if r.Code == nil {
return 0
}
return *r.Code
}
func (r *sdkResponseBase) GetError() string {
if r.Error == nil {
return ""
}
return *r.Error
}
var _ sdkResponse = (*sdkResponseBase)(nil)
type DnsDomainRecord struct {
UUID string `json:"uuid"`
Name string `json:"name"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
+4 -4
View File
@@ -8,10 +8,10 @@ import (
)
type SSLInstallSSLRequest struct {
Domain *string `url:"domain,omitempty"`
Cert *string `url:"cert,omitempty"`
Key *string `url:"key,omitempty"`
CABundle *string `url:"cabundle,omitempty"`
Domain *string `json:"domain,omitempty" url:"domain,omitempty"`
Cert *string `json:"cert,omitempty" url:"cert,omitempty"`
Key *string `json:"key,omitempty" url:"key,omitempty"`
CABundle *string `json:"cabundle,omitempty" url:"cabundle,omitempty"`
}
type SSLInstallSSLResponse struct {
+1
View File
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" version="1.1" x="0" y="0" viewBox="0 0 652 652" style="enable-background:new 0 0 652 652;" width="200" height="200"><style type="text/css">.st0{fill:#005BAC;}.st1{fill:#12B8D7;}</style><path class="st0" d="M223.9,393.3c-1.3,0-2.7,0.4-4,1.3v-6.7h-3.1v19.3h3.1v-1.3c0.4,0.9,1.8,1.8,3.6,1.8c3.1,0,6.3-2.7,6.3-7.2 S227,393.3,223.9,393.3z M223.4,404.5c-0.4,0-2.2,0-3.1-2.7V400c0-2.7,1.8-4,3.1-4c1.8,0,3.6,1.3,3.1,4 C226.6,401.8,225.7,404.5,223.4,404.5z M239.6,393.7l-2.7,9.9l-3.1-9.9h-3.6l4.9,13.9c0,0.4-0.4,0.9-0.9,0.9 c-0.4,0.4-1.3,0.4-1.3,0.4H232v2.7h1.8c2.7,0,3.6-0.9,4.5-3.6l0.9-2.7l3.6-11.7H239.6z M319.4,405.9h2.2l8.1-13l4.5,15.2h9l-9-28.3 h-3.6l-10.8,16.6l-10.8-16.6h-3.6l-9,28.3h9l4.5-15.2l8.1,13C318.1,405.9,319.4,405.9,319.4,405.9z M296.5,392h-24.2 c-1.3,0-2.2,0.9-2.2,2.2s0.9,2.2,2.2,2.2h14.3c-1.8,4.5-7.6,7.6-14.3,7.6c-8.1,0-14.8-4.9-14.8-10.8c0-5.8,6.7-10.3,14.8-10.3 c4.5,0,8.5,1.3,11.2,3.6h9.9c-4-4.5-12.1-7.6-21.1-7.6c-13.5,0-24.2,6.7-24.2,14.8c0,8.1,10.8,14.8,24.2,14.8s24.2-6.7,24.2-14.8 C296.5,393.7,296.5,392,296.5,392z M366.5,379c-13.5,0-24.2,6.7-24.2,14.8c0,8.1,10.8,14.8,24.2,14.8s24.2-6.7,24.2-14.8 C391.2,385.7,379.9,379,366.5,379z M366.5,404.1c-8.1,0-14.8-4.9-14.8-10.3c0-5.8,6.7-10.3,14.8-10.3c8.1,0,14.8,4.9,14.8,10.3 C381.3,399.6,375,404.1,366.5,404.1z"/><path class="st1" d="M153.9,349.8l-0.4,0.9c-10.3,15.2-27.4,24.2-45.3,24.2c-30,0-54.7-24.7-54.7-54.7c0-30,24.7-54.7,54.7-54.7 c17.9,0,35,9,45.3,24.2l0.4,0.9c5.4-9.9,13.5-17.9,23.3-23.8c-16.6-22-42.2-34.5-69.1-34.5c-48.4,0-87.9,39.5-87.9,87.9 s39.5,87.9,87.9,87.9c27.4,0,52.5-12.6,69.1-34.1C167.8,368.2,159.8,359.7,153.9,349.8z"/><path class="st1" d="M593.8,274.5v4c-5.8-2.7-12.6-4-19.3-4c-17,0-31.8,9.4-39.5,23.3v-65.5h-32.7v71.3H449v-71.3h-32.7v65.5 c-8.1-13.9-22.9-22.9-39.5-22.9c-19.3,0-35.9,12.1-42.6,29.6c-6.7-17-23.3-29.6-42.6-29.6S255.7,287,249,304.1 c-6.7-17-23.3-29.1-42.6-29.1c-25.1,0-45.3,20.2-45.3,45.3s20.6,45.3,45.3,45.3c17,0,31.8-9.4,39.5-23.3v23.8h26.5v-45.7 c0-10.3,8.5-18.8,18.8-18.8s18.8,8.5,18.8,18.8v46.2h26.5v-23.8c7.6,13.9,22.9,23.3,39.5,23.3c17,0,31.8-9.4,39.5-22.9v65.5H449 v-72.2h53.4v71.3h32.7v-65.5c7.6,13.9,22.9,23.3,39.5,23.3c6.7,0,13-1.3,19.3-4v4h26.5v-91H593.8z M206.4,339 c-10.3,0-18.8-8.5-18.8-18.8s8.5-18.8,18.8-18.8c10.3,0,18.8,8.5,18.8,18.8C225.7,330.5,217.2,339,206.4,339z M376.8,339 c-10.3,0-18.8-8.5-18.8-18.8s8.5-18.8,18.8-18.8c10.3,0,18.8,8.5,18.8,18.8S387.1,339,376.8,339z M575,339 c-10.3,0-18.8-8.5-18.8-18.8s8.5-18.8,18.8-18.8c10.3,0,18.8,8.5,18.8,18.8S585.3,339,575,339z"/></svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

@@ -28,6 +28,7 @@ import AccessConfigFieldsProviderCdnfly from "./AccessConfigFieldsProviderCdnfly
import AccessConfigFieldsProviderCloudflare from "./AccessConfigFieldsProviderCloudflare";
import AccessConfigFieldsProviderClouDNS from "./AccessConfigFieldsProviderClouDNS";
import AccessConfigFieldsProviderCMCCCloud from "./AccessConfigFieldsProviderCMCCCloud";
import AccessConfigFieldsProviderConoHaVPS from "./AccessConfigFieldsProviderConoHaVPS";
import AccessConfigFieldsProviderConstellix from "./AccessConfigFieldsProviderConstellix";
import AccessConfigFieldsProviderCPanel from "./AccessConfigFieldsProviderCPanel";
import AccessConfigFieldsProviderCTCCCloud from "./AccessConfigFieldsProviderCTCCCloud";
@@ -146,6 +147,7 @@ const providerComponentMap: Partial<Record<AccessProviderType, React.ComponentTy
[ACCESS_PROVIDERS.CLOUDFLARE]: AccessConfigFieldsProviderCloudflare,
[ACCESS_PROVIDERS.CLOUDNS]: AccessConfigFieldsProviderClouDNS,
[ACCESS_PROVIDERS.CMCCCLOUD]: AccessConfigFieldsProviderCMCCCloud,
[ACCESS_PROVIDERS.CONOHAVPS]: AccessConfigFieldsProviderConoHaVPS,
[ACCESS_PROVIDERS.CONSTELLIX]: AccessConfigFieldsProviderConstellix,
[ACCESS_PROVIDERS.CPANEL]: AccessConfigFieldsProviderCPanel,
[ACCESS_PROVIDERS.CTCCCLOUD]: AccessConfigFieldsProviderCTCCCloud,
@@ -0,0 +1,236 @@
import { getI18n, useTranslation } from "react-i18next";
import { Form, Input, Select } from "antd";
import { createSchemaFieldRule } from "antd-zod";
import { z } from "zod";
import Show from "@/components/Show";
import { useFormNestedFieldsContext } from "./_context";
const API_VERSION_V2 = "v2" as const;
const API_VERSION_V3 = "v3" as const;
const AccessConfigFormFieldsProviderConoHaVPS = () => {
const { i18n, t } = useTranslation();
const { parentNamePath } = useFormNestedFieldsContext();
const formSchema = z.object({
[parentNamePath]: getSchema({ i18n }),
});
const formRule = createSchemaFieldRule(formSchema);
const formInst = Form.useFormInstance<z.infer<typeof formSchema>>();
const initialValues = getInitialValues();
const fieldApiVersion = Form.useWatch([parentNamePath, "apiVersion"], formInst);
return (
<>
<Form.Item
name={[parentNamePath, "apiVersion"]}
initialValue={initialValues.apiVersion}
label={t("access.form.conohavps_api_version.label")}
rules={[formRule]}
>
<Select
options={[API_VERSION_V2, API_VERSION_V3].map((s) => ({ label: s, value: s }))}
placeholder={t("access.form.conohavps_api_version.placeholder")}
/>
</Form.Item>
<Show>
<Show.Case when={fieldApiVersion === API_VERSION_V2}>
<Form.Item
name={[parentNamePath, "apiUserName"]}
initialValue={initialValues.apiUserName}
label={t("access.form.conohavps_api_v2_username.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v2_username.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.conohavps_api_v2_username.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "apiPassword"]}
initialValue={initialValues.apiPassword}
label={t("access.form.conohavps_api_v2_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v2_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.conohavps_api_v2_password.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "tenantId"]}
initialValue={initialValues.tenantId}
label={t("access.form.conohavps_api_v2_tenant_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v2_tenant_id.tooltip") }}></span>}
>
<Input placeholder={t("access.form.conohavps_api_v2_tenant_id.placeholder")} />
</Form.Item>
</Show.Case>
<Show.Case when={fieldApiVersion === API_VERSION_V3}>
<Form.Item
name={[parentNamePath, "apiUserId"]}
initialValue={initialValues.apiUserId}
label={t("access.form.conohavps_api_v3_user_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v3_user_id.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.conohavps_api_v3_user_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "apiUserName"]}
initialValue={initialValues.apiUserName}
label={t("access.form.conohavps_api_v3_user_name.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v3_user_name.tooltip") }}></span>}
>
<Input autoComplete="new-password" placeholder={t("access.form.conohavps_api_v3_user_name.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "apiPassword"]}
initialValue={initialValues.apiPassword}
label={t("access.form.conohavps_api_v3_password.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v3_password.tooltip") }}></span>}
>
<Input.Password autoComplete="new-password" placeholder={t("access.form.conohavps_api_v3_password.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "tenantId"]}
initialValue={initialValues.tenantId}
label={t("access.form.conohavps_api_v3_project_id.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v3_project_id.tooltip") }}></span>}
>
<Input placeholder={t("access.form.conohavps_api_v3_project_id.placeholder")} />
</Form.Item>
<Form.Item
name={[parentNamePath, "tenantName"]}
initialValue={initialValues.tenantName}
label={t("access.form.conohavps_api_v3_project_name.label")}
rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("access.form.conohavps_api_v3_project_name.tooltip") }}></span>}
>
<Input placeholder={t("access.form.conohavps_api_v3_project_name.placeholder")} />
</Form.Item>
</Show.Case>
</Show>
</>
);
};
const getInitialValues = (): Nullish<z.infer<ReturnType<typeof getSchema>>> => {
return {
apiVersion: API_VERSION_V3,
apiUserId: "",
apiUserName: "",
apiPassword: "",
tenantId: "",
tenantName: "",
};
};
const getSchema = ({ i18n = getI18n() }: { i18n: ReturnType<typeof getI18n> }) => {
const { t } = i18n;
return z
.object({
apiVersion: z.enum([API_VERSION_V2, API_VERSION_V3]),
apiUserId: z.string().nullish(),
apiUserName: z.string().nullish(),
apiPassword: z.string().nonempty(),
tenantId: z.string().nullish(),
tenantName: z.string().nullish(),
})
.superRefine((values, ctx) => {
switch (values.apiVersion) {
case API_VERSION_V2:
{
const scUserName = z.string().nonempty();
const spUserName = scUserName.safeParse(values.apiUserName);
if (!spUserName.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spUserName.error).errors.join(),
path: ["apiUserName"],
});
}
const scTenantId = z.string().nonempty();
const spTenantId = scTenantId.safeParse(values.tenantId);
if (!spTenantId.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spTenantId.error).errors.join(),
path: ["tenantId"],
});
}
}
break;
case API_VERSION_V3:
{
const scUserId = z.string().nonempty();
const spUserId = scUserId.safeParse(values.apiUserId);
const scUserName = z.string().nonempty();
const spUserName = scUserName.safeParse(values.apiUserName);
if (!spUserId.success && !spUserName.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spUserName.error).errors.join(),
path: ["apiUserName"],
});
} else if (spUserId.success && spUserName.success) {
ctx.addIssue({
code: "custom",
message: t("access.form.conohavps_api_v3_user_id.errmsg.conflict"),
path: ["apiUserId"],
});
ctx.addIssue({
code: "custom",
message: t("access.form.conohavps_api_v3_user_name.errmsg.conflict"),
path: ["apiUserName"],
});
}
const scTenantId = z.string().nonempty();
const spTenantId = scTenantId.safeParse(values.tenantId);
const scTenantName = z.string().nonempty();
const spTenantName = scTenantName.safeParse(values.tenantName);
if (!spTenantId.success && !spTenantName.success) {
ctx.addIssue({
code: "custom",
message: z.treeifyError(spTenantId.error).errors.join(),
path: ["tenantId"],
});
} else if (spTenantId.success && spTenantName.success) {
ctx.addIssue({
code: "custom",
message: t("access.form.conohavps_api_v3_project_id.errmsg.conflict"),
path: ["tenantId"],
});
ctx.addIssue({
code: "custom",
message: t("access.form.conohavps_api_v3_project_name.errmsg.conflict"),
path: ["tenantName"],
});
}
}
break;
}
});
};
const _default = Object.assign(AccessConfigFormFieldsProviderConoHaVPS, {
getInitialValues,
getSchema,
});
export default _default;
+4
View File
@@ -41,6 +41,7 @@ export const ACCESS_PROVIDERS = Object.freeze({
CLOUDFLARE: "cloudflare",
CLOUDNS: "cloudns",
CMCCCLOUD: "cmcccloud",
CONOHAVPS: "conohavps",
CONSTELLIX: "constellix",
CPANEL: "cpanel",
CTCCCLOUD: "ctcccloud",
@@ -213,6 +214,7 @@ export const accessProvidersMap: Map<AccessProvider["type"] | string, AccessProv
[ACCESS_PROVIDERS.BOOKMYNAME, "provider.bookmyname", "/imgs/providers/bookmyname.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CLOUDFLARE, "provider.cloudflare", "/imgs/providers/cloudflare.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CLOUDNS, "provider.cloudns", "/imgs/providers/cloudns.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CONOHAVPS, "provider.conohavps", "/imgs/providers/conohavps.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.CONSTELLIX, "provider.constellix", "/imgs/providers/constellix.png", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DESEC, "provider.desec", "/imgs/providers/desec.svg", [ACCESS_USAGES.DNS]],
[ACCESS_PROVIDERS.DIGITALOCEAN, "provider.digitalocean", "/imgs/providers/digitalocean.svg", [ACCESS_USAGES.DNS]],
@@ -375,6 +377,7 @@ export const ACME_DNS01_PROVIDERS = Object.freeze({
CLOUDNS: `${ACCESS_PROVIDERS.CLOUDNS}`,
CMCCCLOUD: `${ACCESS_PROVIDERS.CMCCCLOUD}`, // 兼容旧值,等同于 `CMCCCLOUD_DNS`
CMCCCLOUD_DNS: `${ACCESS_PROVIDERS.CMCCCLOUD}-dns`,
CONOHAVPS: `${ACCESS_PROVIDERS.CONOHAVPS}`,
CONSTELLIX: `${ACCESS_PROVIDERS.CONSTELLIX}`,
CPANEL: `${ACCESS_PROVIDERS.CPANEL}`,
CTCCCLOUD: `${ACCESS_PROVIDERS.CTCCCLOUD}`, // 兼容旧值,等同于 `CTCCCLOUD_SMARTDNS`
@@ -459,6 +462,7 @@ export const acmeDns01ProvidersMap: Map<ACMEDns01Provider["type"] | string, ACME
[ACME_DNS01_PROVIDERS.BUNNY, "provider.bunny"],
[ACME_DNS01_PROVIDERS.CLOUDFLARE, "provider.cloudflare"],
[ACME_DNS01_PROVIDERS.CLOUDNS, "provider.cloudns"],
[ACME_DNS01_PROVIDERS.CONOHAVPS, "provider.conohavps"],
[ACME_DNS01_PROVIDERS.CONSTELLIX, "provider.constellix"],
[ACME_DNS01_PROVIDERS.DESEC, "provider.desec"],
[ACME_DNS01_PROVIDERS.DIGITALOCEAN, "provider.digitalocean"],
+56
View File
@@ -377,6 +377,62 @@
"placeholder": "Please enter CMCC ECloud AccessKeySecret",
"tooltip": "For more information, see <a href=\"https://ecloud.10086.cn/op-help-center/doc/article/49739\" target=\"_blank\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>"
},
"conohavps_api_version": {
"label": "ConoHa VPS version",
"placeholder": "Please select ConoHa VPS version"
},
"conohavps_api_v2_username": {
"label": "ConoHa VPS API username",
"placeholder": "Please enter ConoHa VPS 2.0 API username",
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/</a>"
},
"conohavps_api_v2_password": {
"label": "ConoHa VPS API password",
"placeholder": "Please enter ConoHa VPS 2.0 API password",
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/</a>"
},
"conohavps_api_v2_tenant_id": {
"label": "ConoHa VPS tenant ID",
"placeholder": "Please enter ConoHa VPS 2.0 tenant ID",
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps2/api-identity-vps2/identity-post_tokens-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-identity-vps2/identity-post_tokens-v2/</a>"
},
"conohavps_api_v3_user_id": {
"label": "ConoHa VPS API user ID",
"placeholder": "Please enter ConoHa VPS 3.0 API user ID",
"errmsg": {
"conflict": "Conflict with the \"API user name\", please retain only one field"
},
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_user_name": {
"label": "ConoHa VPS API user name",
"placeholder": "Please enter ConoHa VPS 3.0 API user name",
"errmsg": {
"conflict": "Conflict with the \"API user ID\", please retain only one field"
},
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_password": {
"label": "ConoHa VPS API password",
"placeholder": "Please enter ConoHa VPS 3.0 API password",
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_project_id": {
"label": "ConoHa VPS project ID",
"placeholder": "Please enter ConoHa VPS 3.0 tenant project ID",
"errmsg": {
"conflict": "Conflict with the \"project name\", please retain only one field"
},
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/</a>"
},
"conohavps_api_v3_project_name": {
"label": "ConoHa VPS project name",
"placeholder": "Please enter ConoHa VPS 3.0 tenant project name",
"errmsg": {
"conflict": "Conflict with the \"project ID\", please retain only one field"
},
"tooltip": "For more information, see <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/</a>"
},
"constellix_api_key": {
"label": "Constellix API key",
"placeholder": "Please enter Constellix API key",
@@ -95,6 +95,7 @@
"cloudns": "ClouDNS",
"cmcccloud": "China Mobile ECloud",
"cmcccloud_dns": "China Mobile ECloud - DNS",
"conohavps": "ConoHa VPS",
"constellix": "Constellix",
"cpanel": "cPanel",
"ctcccloud": "China Telecom StateCloud",
+56
View File
@@ -375,6 +375,62 @@
"placeholder": "请输入移动云 AccessKeySecret",
"tooltip": "这是什么?请参阅 <a href=\"https://ecloud.10086.cn/op-help-center/doc/article/49739\" target=\"_blank\">https://ecloud.10086.cn/op-help-center/doc/article/49739</a>"
},
"conohavps_api_version": {
"label": "ConoHa VPS 版本",
"placeholder": "请选择 ConoHa VPS 版本"
},
"conohavps_api_v2_username": {
"label": "ConoHa VPS API 用户名",
"placeholder": "请输入 ConoHa VPS 2.0 API 用户名",
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/</a>"
},
"conohavps_api_v2_password": {
"label": "ConoHa VPS API 密码",
"placeholder": "请输入 ConoHa VPS 2.0 API 密码",
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-guideline-vps2/api-guideline-v2/</a>"
},
"conohavps_api_v2_tenant_id": {
"label": "ConoHa VPS 租户 ID",
"placeholder": "请输入 ConoHa VPS 2.0 租户 ID",
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps2/api-identity-vps2/identity-post_tokens-v2/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps2/api-identity-vps2/identity-post_tokens-v2/</a>"
},
"conohavps_api_v3_user_id": {
"label": "ConoHa VPS API 用户 ID",
"placeholder": "请输入 ConoHa VPS 3.0 API 用户 ID",
"errmsg": {
"conflict": "与字段「API 用户名」冲突,请二选一"
},
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_user_name": {
"label": "ConoHa VPS API 用户名",
"placeholder": "请输入 ConoHa VPS 3.0 API 用户名",
"errmsg": {
"conflict": "与字段「API 用户 ID」冲突,请二选一"
},
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_password": {
"label": "ConoHa VPS API 密码",
"placeholder": "请输入 ConoHa VPS 3.0 API 密码",
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-create_api_user-v3/</a>"
},
"conohavps_api_v3_project_id": {
"label": "ConoHa VPS 项目 ID",
"placeholder": "请输入 ConoHa VPS 3.0 租户项目 ID",
"errmsg": {
"conflict": "与字段「项目名称」冲突,请二选一"
},
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/</a>"
},
"conohavps_api_v3_project_name": {
"label": "ConoHa VPS 项目名称",
"placeholder": "请输入 ConoHa VPS 3.0 租户项目名称",
"errmsg": {
"conflict": "与字段「项目 ID」冲突,请二选一"
},
"tooltip": "这是什么?请参阅 <a href=\"https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/\" target=\"_blank\">https://doc.conoha.jp/reference/api-vps3/api-cp-vps3/cp-get_api_info-v3/</a>"
},
"constellix_api_key": {
"label": "Constellix API Key",
"placeholder": "请输入 Constellix API Key",
@@ -95,6 +95,7 @@
"cloudns": "ClouDNS",
"cmcccloud": "移动云",
"cmcccloud_dns": "移动云 - 云解析 DNS",
"conohavps": "ConoHa VPS",
"constellix": "Constellix",
"cpanel": "cPanel",
"ctcccloud": "天翼云",