From 8fe27e0cc3be3aaf2212d98e2b2d5a68f56a0a8f Mon Sep 17 00:00:00 2001 From: Andrew Kluev Date: Fri, 2 Sep 2022 23:56:10 +0300 Subject: [PATCH] Add DNS provider for VK Cloud (#1706) Co-authored-by: Fernandez Ludovic --- .golangci.toml | 2 +- README.md | 6 +- cmd/account.go | 2 - cmd/zz_gen_cmd_dnshelp.go | 25 +++ docs/content/dns/zz_gen_vkcloud.md | 83 ++++++++ providers/dns/dns_providers.go | 3 + providers/dns/vkcloud/internal/client.go | 160 +++++++++++++++ providers/dns/vkcloud/internal/types.go | 23 +++ providers/dns/vkcloud/vkcloud.go | 236 +++++++++++++++++++++++ providers/dns/vkcloud/vkcloud.toml | 41 ++++ providers/dns/vkcloud/vkcloud_test.go | 209 ++++++++++++++++++++ 11 files changed, 784 insertions(+), 6 deletions(-) create mode 100644 docs/content/dns/zz_gen_vkcloud.md create mode 100644 providers/dns/vkcloud/internal/client.go create mode 100644 providers/dns/vkcloud/internal/types.go create mode 100644 providers/dns/vkcloud/vkcloud.go create mode 100644 providers/dns/vkcloud/vkcloud.toml create mode 100644 providers/dns/vkcloud/vkcloud_test.go diff --git a/.golangci.toml b/.golangci.toml index ebd6838d..e7a9a560 100644 --- a/.golangci.toml +++ b/.golangci.toml @@ -1,5 +1,5 @@ [run] - timeout = "5m" + timeout = "7m" skip-files = [] [linters-settings] diff --git a/README.md b/README.md index fe5cd702..bcca9330 100644 --- a/README.md +++ b/README.md @@ -73,9 +73,9 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [Sakura Cloud](https://go-acme.github.io/lego/dns/sakuracloud/) | [Scaleway](https://go-acme.github.io/lego/dns/scaleway/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | | [Simply.com](https://go-acme.github.io/lego/dns/simply/) | [Sonic](https://go-acme.github.io/lego/dns/sonic/) | [Stackpath](https://go-acme.github.io/lego/dns/stackpath/) | [Tencent Cloud DNS](https://go-acme.github.io/lego/dns/tencentcloud/) | | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [UKFast SafeDNS](https://go-acme.github.io/lego/dns/safedns/) | [Variomedia](https://go-acme.github.io/lego/dns/variomedia/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | -| [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [Vscale](https://go-acme.github.io/lego/dns/vscale/) | -| [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex](https://go-acme.github.io/lego/dns/yandex/) | -| [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | | +| [Vercel](https://go-acme.github.io/lego/dns/vercel/) | [Versio.[nl/eu/uk]](https://go-acme.github.io/lego/dns/versio/) | [VinylDNS](https://go-acme.github.io/lego/dns/vinyldns/) | [VK Cloud](https://go-acme.github.io/lego/dns/vkcloud/) | +| [Vscale](https://go-acme.github.io/lego/dns/vscale/) | [Vultr](https://go-acme.github.io/lego/dns/vultr/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | +| [Yandex](https://go-acme.github.io/lego/dns/yandex/) | [Zone.ee](https://go-acme.github.io/lego/dns/zoneee/) | [Zonomi](https://go-acme.github.io/lego/dns/zonomi/) | | diff --git a/cmd/account.go b/cmd/account.go index 73247c93..fba0c61b 100644 --- a/cmd/account.go +++ b/cmd/account.go @@ -29,5 +29,3 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey { func (a *Account) GetRegistration() *registration.Resource { return a.Registration } - -/** End **/ diff --git a/cmd/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index 3062e557..728927eb 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -112,6 +112,7 @@ func allDNSCodes() string { "vercel", "versio", "vinyldns", + "vkcloud", "vscale", "vultr", "wedos", @@ -2222,6 +2223,30 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/vinyldns`) + case "vkcloud": + // generated from: providers/dns/vkcloud/vkcloud.toml + ew.writeln(`Configuration for VK Cloud.`) + ew.writeln(`Code: 'vkcloud'`) + ew.writeln(`Since: 'v4.9.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "VK_CLOUD_PASSWORD": Password for VK Cloud account`) + ew.writeln(` - "VK_CLOUD_PROJECT_ID": String ID of project in VK Cloud`) + ew.writeln(` - "VK_CLOUD_USERNAME": Email of VK Cloud account`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "VK_CLOUD_DNS_ENDPOINT": URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds`) + ew.writeln(` - "VK_CLOUD_DOMAIN_NAME": Openstack users domain name. Defaults to 'users' but can be changed for usage with private clouds`) + ew.writeln(` - "VK_CLOUD_IDENTITY_ENDPOINT": URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds`) + ew.writeln(` - "VK_CLOUD_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "VK_CLOUD_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "VK_CLOUD_TTL": The TTL of the TXT record used for the DNS challenge`) + + ew.writeln() + ew.writeln(`More information: https://go-acme.github.io/lego/dns/vkcloud`) + case "vscale": // generated from: providers/dns/vscale/vscale.toml ew.writeln(`Configuration for Vscale.`) diff --git a/docs/content/dns/zz_gen_vkcloud.md b/docs/content/dns/zz_gen_vkcloud.md new file mode 100644 index 00000000..59a70947 --- /dev/null +++ b/docs/content/dns/zz_gen_vkcloud.md @@ -0,0 +1,83 @@ +--- +title: "VK Cloud" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: vkcloud +dnsprovider: + since: "v4.9.0" + code: "vkcloud" + url: "https://mcs.mail.ru/" +--- + + + + + + +Configuration for [VK Cloud](https://mcs.mail.ru/). + + + + +- Code: `vkcloud` +- Since: v4.9.0 + + +Here is an example bash command using the VK Cloud provider: + +```bash +VK_CLOUD_PROJECT_ID="" \ +VK_CLOUD_USERNAME="" \ +VK_CLOUD_PASSWORD="" \ +lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `VK_CLOUD_PASSWORD` | Password for VK Cloud account | +| `VK_CLOUD_PROJECT_ID` | String ID of project in VK Cloud | +| `VK_CLOUD_USERNAME` | Email of VK Cloud account | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{< ref "dns#configuration-and-credentials" >}}). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `VK_CLOUD_DNS_ENDPOINT` | URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds | +| `VK_CLOUD_DOMAIN_NAME` | Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds | +| `VK_CLOUD_IDENTITY_ENDPOINT` | URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds | +| `VK_CLOUD_POLLING_INTERVAL` | Time between DNS propagation check | +| `VK_CLOUD_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `VK_CLOUD_TTL` | The TTL of the TXT record used for the DNS challenge | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here]({{< ref "dns#configuration-and-credentials" >}}). + +## Credential inforamtion + +You can find all required and additional information on ["Project/Keys" page](https://mcs.mail.ru/app/en/project/keys) of your cloud. + +| ENV Variable | Parameter from page | +|----------------------------|---------------------| +| VK_CLOUD_PROJECT_ID | Project ID | +| VK_CLOUD_USERNAME | Username | +| VK_CLOUD_DOMAIN_NAME | User Domain Name | +| VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint | + + + +## More information + +- [API documentation](https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api) + + + + diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index 8125ea82..d6a0fc50 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -103,6 +103,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/vercel" "github.com/go-acme/lego/v4/providers/dns/versio" "github.com/go-acme/lego/v4/providers/dns/vinyldns" + "github.com/go-acme/lego/v4/providers/dns/vkcloud" "github.com/go-acme/lego/v4/providers/dns/vscale" "github.com/go-acme/lego/v4/providers/dns/vultr" "github.com/go-acme/lego/v4/providers/dns/wedos" @@ -313,6 +314,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return versio.NewDNSProvider() case "vinyldns": return vinyldns.NewDNSProvider() + case "vkcloud": + return vkcloud.NewDNSProvider() case "vultr": return vultr.NewDNSProvider() case "vscale": diff --git a/providers/dns/vkcloud/internal/client.go b/providers/dns/vkcloud/internal/client.go new file mode 100644 index 00000000..ce4af7ba --- /dev/null +++ b/providers/dns/vkcloud/internal/client.go @@ -0,0 +1,160 @@ +package internal + +import ( + "errors" + "fmt" + "net/http" + "net/url" + "path" + "strings" + + "github.com/gophercloud/gophercloud" + "github.com/gophercloud/gophercloud/openstack" +) + +// Client VK client. +type Client struct { + baseURL *url.URL + openstack *gophercloud.ProviderClient + authOpts gophercloud.AuthOptions + authenticated bool +} + +// NewClient creates a Client. +func NewClient(endpoint string, authOpts gophercloud.AuthOptions) (*Client, error) { + err := validateAuthOptions(authOpts) + if err != nil { + return nil, err + } + + openstackClient, err := openstack.NewClient(authOpts.IdentityEndpoint) + if err != nil { + return nil, fmt.Errorf("new client: %w", err) + } + + baseURL, err := url.Parse(endpoint) + if err != nil { + return nil, fmt.Errorf("parse URL: %w", err) + } + + return &Client{ + baseURL: baseURL, + openstack: openstackClient, + authOpts: authOpts, + }, nil +} + +func (c *Client) ListZones() ([]DNSZone, error) { + var zones []DNSZone + opts := &gophercloud.RequestOpts{JSONResponse: &zones} + + // TODO(ldez): go1.19 => c.baseURL.JoinPath("/") + endpoint := joinPath(c.baseURL, "/") + + err := c.request(http.MethodGet, endpoint, opts) + if err != nil { + return nil, err + } + + return zones, nil +} + +func (c *Client) ListTXTRecords(zoneUUID string) ([]DNSTXTRecord, error) { + var records []DNSTXTRecord + opts := &gophercloud.RequestOpts{JSONResponse: &records} + + // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", "/") + endpoint := joinPath(c.baseURL, zoneUUID, "txt", "/") + + err := c.request(http.MethodGet, endpoint, opts) + if err != nil { + return nil, err + } + + return records, nil +} + +func (c *Client) CreateTXTRecord(zoneUUID string, record *DNSTXTRecord) error { + opts := &gophercloud.RequestOpts{ + JSONBody: record, + JSONResponse: record, + } + + // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", "/") + endpoint := joinPath(c.baseURL, zoneUUID, "txt", "/") + + return c.request(http.MethodPost, endpoint, opts) +} + +func (c *Client) DeleteTXTRecord(zoneUUID, recordUUID string) error { + // TODO(ldez): go1.19 => c.baseURL.JoinPath(zoneUUID, "txt", recordUUID) + endpoint := joinPath(c.baseURL, zoneUUID, "txt", recordUUID) + + return c.request(http.MethodDelete, endpoint, &gophercloud.RequestOpts{}) +} + +func (c *Client) request(method string, endpoint *url.URL, options *gophercloud.RequestOpts) error { + if err := c.lazyAuth(); err != nil { + return fmt.Errorf("auth: %w", err) + } + + _, err := c.openstack.Request(method, endpoint.String(), options) + if err != nil { + return fmt.Errorf("request: %w", err) + } + + return nil +} + +func (c *Client) lazyAuth() error { + if c.authenticated { + return nil + } + + err := openstack.Authenticate(c.openstack, c.authOpts) + if err != nil { + return err + } + + c.authenticated = true + + return nil +} + +func validateAuthOptions(opts gophercloud.AuthOptions) error { + if opts.TenantID == "" { + return errors.New("project id is missing in credentials information") + } + + if opts.Username == "" { + return errors.New("username is missing in credentials information") + } + + if opts.Password == "" { + return errors.New("password is missing in credentials information") + } + + if opts.IdentityEndpoint == "" { + return errors.New("identity endpoint is missing in config") + } + + if opts.DomainName == "" { + return errors.New("domain name is missing in config") + } + + return nil +} + +// light version of go1.19 url.URL.JoinPath. +// TODO(ldez): must be remove when we will update to go1.19. +func joinPath(uri *url.URL, elem ...string) *url.URL { + result := path.Join(elem...) + result = path.Join(uri.Path, result) + if len(elem) > 0 && strings.HasSuffix(elem[len(elem)-1], "/") { + result += "/" + } + + parse, _ := uri.Parse(result) + + return parse +} diff --git a/providers/dns/vkcloud/internal/types.go b/providers/dns/vkcloud/internal/types.go new file mode 100644 index 00000000..8afc6dff --- /dev/null +++ b/providers/dns/vkcloud/internal/types.go @@ -0,0 +1,23 @@ +package internal + +type DNSZone struct { + UUID string `json:"uuid,omitempty"` + Tenant string `json:"tenant,omitempty"` + SoaPrimaryDNS string `json:"soa_primary_dns,omitempty"` + SoaAdminEmail string `json:"soa_admin_email,omitempty"` + SoaSerial int `json:"soa_serial,omitempty"` + SoaRefresh int `json:"soa_refresh,omitempty"` + SoaRetry int `json:"soa_retry,omitempty"` + SoaExpire int `json:"soa_expire,omitempty"` + SoaTTL int `json:"soa_ttl,omitempty"` + Zone string `json:"zone,omitempty"` + Status string `json:"status,omitempty"` +} + +type DNSTXTRecord struct { + UUID string `json:"uuid,omitempty"` + Name string `json:"name,omitempty"` + DNS string `json:"dns,omitempty"` + Content string `json:"content,omitempty"` + TTL int `json:"ttl,omitempty"` +} diff --git a/providers/dns/vkcloud/vkcloud.go b/providers/dns/vkcloud/vkcloud.go new file mode 100644 index 00000000..1eb00e05 --- /dev/null +++ b/providers/dns/vkcloud/vkcloud.go @@ -0,0 +1,236 @@ +// Package vkcloud implements a DNS provider for solving the DNS-01 challenge using VK Cloud. +package vkcloud + +import ( + "errors" + "fmt" + "time" + + "github.com/go-acme/lego/v4/challenge/dns01" + "github.com/go-acme/lego/v4/platform/config/env" + "github.com/go-acme/lego/v4/providers/dns/vkcloud/internal" + "github.com/gophercloud/gophercloud" +) + +const ( + defaultIdentityEndpoint = "https://infra.mail.ru/identity/v3/" + defaultDNSEndpoint = "https://mcs.mail.ru/public-dns/v2/dns" +) + +const defaultTTL = 60 + +const defaultDomainName = "users" + +// Environment variables names. +const ( + envNamespace = "VK_CLOUD_" + + EnvDNSEndpoint = envNamespace + "DNS_ENDPOINT" + + EnvIdentityEndpoint = envNamespace + "IDENTITY_ENDPOINT" + EnvDomainName = envNamespace + "DOMAIN_NAME" + + EnvProjectID = envNamespace + "PROJECT_ID" + EnvUsername = envNamespace + "USERNAME" + EnvPassword = envNamespace + "PASSWORD" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + ProjectID string + Username string + Password string + + DNSEndpoint string + + IdentityEndpoint string + DomainName string + + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +func NewDefaultConfig() *Config { + return &Config{ + TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL), + PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout), + PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + client *internal.Client + config *Config +} + +// NewDNSProvider returns a DNSProvider instance configured for VK Cloud. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvProjectID, EnvUsername, EnvPassword) + if err != nil { + return nil, fmt.Errorf("vkcloud: %w", err) + } + + config := NewDefaultConfig() + config.ProjectID = values[EnvProjectID] + config.Username = values[EnvUsername] + config.Password = values[EnvPassword] + config.IdentityEndpoint = env.GetOrDefaultString(EnvIdentityEndpoint, defaultIdentityEndpoint) + config.DomainName = env.GetOrDefaultString(EnvDomainName, defaultDomainName) + config.DNSEndpoint = env.GetOrDefaultString(EnvDNSEndpoint, defaultDNSEndpoint) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for VK Cloud. +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("vkcloud: the configuration of the DNS provider is nil") + } + + if config.DNSEndpoint == "" { + return nil, fmt.Errorf("vkcloud: DNS endpoint is missing in config") + } + + authOpts := gophercloud.AuthOptions{ + IdentityEndpoint: config.IdentityEndpoint, + Username: config.Username, + Password: config.Password, + DomainName: config.DomainName, + TenantID: config.ProjectID, + } + + client, err := internal.NewClient(config.DNSEndpoint, authOpts) + if err != nil { + return nil, fmt.Errorf("vkcloud: unable to build VK Cloud client: %w", err) + } + + return &DNSProvider{ + client: client, + config: config, + }, nil +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (r *DNSProvider) Present(domain, _, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return fmt.Errorf("vkcloud: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zones, err := r.client.ListZones() + if err != nil { + return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) + } + + var zoneUUID string + for _, zone := range zones { + if zone.Zone == authZone { + zoneUUID = zone.UUID + } + } + + if zoneUUID == "" { + return fmt.Errorf("vkcloud: cant find dns zone %s in VK Cloud", authZone) + } + + name := fqdn[:len(fqdn)-len(authZone)-1] + + err = r.upsertTXTRecord(zoneUUID, name, value) + if err != nil { + return fmt.Errorf("vkcloud: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (r *DNSProvider) CleanUp(domain, _, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + authZone, err := dns01.FindZoneByFqdn(fqdn) + if err != nil { + return fmt.Errorf("vkcloud: %w", err) + } + + authZone = dns01.UnFqdn(authZone) + + zones, err := r.client.ListZones() + if err != nil { + return fmt.Errorf("vkcloud: unable to fetch dns zones: %w", err) + } + + var zoneUUID string + + for _, zone := range zones { + if zone.Zone == authZone { + zoneUUID = zone.UUID + } + } + + if zoneUUID == "" { + return nil + } + + name := fqdn[:len(fqdn)-len(authZone)-1] + + err = r.removeTXTRecord(zoneUUID, name, value) + if err != nil { + return fmt.Errorf("vkcloud: %w", err) + } + + return nil +} + +// Timeout returns the timeout and interval to use when checking for DNS propagation. +// Adjusting here to cope with spikes in propagation times. +func (r *DNSProvider) Timeout() (timeout, interval time.Duration) { + return r.config.PropagationTimeout, r.config.PollingInterval +} + +func (r *DNSProvider) upsertTXTRecord(zoneUUID, name, value string) error { + records, err := r.client.ListTXTRecords(zoneUUID) + if err != nil { + return err + } + + for _, record := range records { + if record.Name == name && record.Content == value { + // The DNSRecord is already present, nothing to do + return nil + } + } + + return r.client.CreateTXTRecord(zoneUUID, &internal.DNSTXTRecord{ + Name: name, + Content: value, + TTL: r.config.TTL, + }) +} + +func (r *DNSProvider) removeTXTRecord(zoneUUID, name, value string) error { + records, err := r.client.ListTXTRecords(zoneUUID) + if err != nil { + return err + } + + name = dns01.UnFqdn(name) + for _, record := range records { + if record.Name == name && record.Content == value { + return r.client.DeleteTXTRecord(zoneUUID, record.UUID) + } + } + + // The DNSRecord is not present, nothing to do + return nil +} diff --git a/providers/dns/vkcloud/vkcloud.toml b/providers/dns/vkcloud/vkcloud.toml new file mode 100644 index 00000000..573daa09 --- /dev/null +++ b/providers/dns/vkcloud/vkcloud.toml @@ -0,0 +1,41 @@ +Name = "VK Cloud" +Description = '''''' +URL = "https://mcs.mail.ru/" +Code = "vkcloud" +Since = "v4.9.0" + +Example = ''' +VK_CLOUD_PROJECT_ID="" \ +VK_CLOUD_USERNAME="" \ +VK_CLOUD_PASSWORD="" \ +lego --email you@example.com --dns vkcloud --domains "example.org" --domains "*.example.org" run +''' + +Additional = ''' +## Credential inforamtion + +You can find all required and additional information on ["Project/Keys" page](https://mcs.mail.ru/app/en/project/keys) of your cloud. + +| ENV Variable | Parameter from page | +|----------------------------|---------------------| +| VK_CLOUD_PROJECT_ID | Project ID | +| VK_CLOUD_USERNAME | Username | +| VK_CLOUD_DOMAIN_NAME | User Domain Name | +| VK_CLOUD_IDENTITY_ENDPOINT | Identity endpoint | +''' + +[Configuration] + [Configuration.Credentials] + VK_CLOUD_PROJECT_ID = "String ID of project in VK Cloud" + VK_CLOUD_USERNAME = "Email of VK Cloud account" + VK_CLOUD_PASSWORD = "Password for VK Cloud account" + [Configuration.Additional] + VK_CLOUD_DNS_ENDPOINT="URL of DNS API. Defaults to https://mcs.mail.ru/public-dns but can be changed for usage with private clouds" + VK_CLOUD_IDENTITY_ENDPOINT="URL of OpenStack Auth API, Defaults to https://infra.mail.ru:35357/v3/ but can be changed for usage with private clouds" + VK_CLOUD_DOMAIN_NAME="Openstack users domain name. Defaults to `users` but can be changed for usage with private clouds" + VK_CLOUD_POLLING_INTERVAL = "Time between DNS propagation check" + VK_CLOUD_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + VK_CLOUD_TTL = "The TTL of the TXT record used for the DNS challenge" + +[Links] + API = "https://mcs.mail.ru/docs/networks/vnet/networks/publicdns/api" diff --git a/providers/dns/vkcloud/vkcloud_test.go b/providers/dns/vkcloud/vkcloud_test.go new file mode 100644 index 00000000..edc32363 --- /dev/null +++ b/providers/dns/vkcloud/vkcloud_test.go @@ -0,0 +1,209 @@ +package vkcloud + +import ( + "testing" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +const ( + fakeProjectID = "an_project_id_from_vk_cloud_ui" + fakeUsername = "vkclouduser@email.address" + fakePasswd = "vkcloudpasswd" +) + +var envTest = tester.NewEnvTest(EnvProjectID, EnvUsername, EnvPassword).WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvProjectID: fakeProjectID, + EnvUsername: fakeUsername, + EnvPassword: fakePasswd, + }, + }, + { + desc: "missing project id", + envVars: map[string]string{ + EnvUsername: fakeUsername, + EnvPassword: fakePasswd, + }, + expected: "vkcloud: some credentials information are missing: VK_CLOUD_PROJECT_ID", + }, + { + desc: "missing username", + envVars: map[string]string{ + EnvProjectID: fakeProjectID, + EnvPassword: fakePasswd, + }, + expected: "vkcloud: some credentials information are missing: VK_CLOUD_USERNAME", + }, + { + desc: "missing password", + envVars: map[string]string{ + EnvProjectID: fakeProjectID, + EnvUsername: fakeUsername, + }, + expected: "vkcloud: some credentials information are missing: VK_CLOUD_PASSWORD", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + defer envTest.RestoreEnv() + envTest.ClearEnv() + + envTest.Apply(test.envVars) + + p, err := NewDNSProvider() + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + config *Config + expected string + }{ + { + desc: "success", + config: &Config{ + ProjectID: fakeProjectID, + Username: fakeUsername, + Password: fakePasswd, + DNSEndpoint: defaultDNSEndpoint, + IdentityEndpoint: defaultIdentityEndpoint, + DomainName: defaultDomainName, + }, + }, + { + desc: "nil config", + config: nil, + expected: "vkcloud: the configuration of the DNS provider is nil", + }, + { + desc: "missing project id", + config: &Config{ + Username: fakeUsername, + Password: fakePasswd, + DNSEndpoint: defaultDNSEndpoint, + IdentityEndpoint: defaultIdentityEndpoint, + DomainName: defaultDomainName, + }, + expected: "vkcloud: unable to build VK Cloud client: project id is missing in credentials information", + }, + { + desc: "missing username", + config: &Config{ + ProjectID: fakeProjectID, + Password: fakePasswd, + DNSEndpoint: defaultDNSEndpoint, + IdentityEndpoint: defaultIdentityEndpoint, + DomainName: defaultDomainName, + }, + expected: "vkcloud: unable to build VK Cloud client: username is missing in credentials information", + }, + { + desc: "missing password", + config: &Config{ + ProjectID: fakeProjectID, + Username: fakeUsername, + DNSEndpoint: defaultDNSEndpoint, + IdentityEndpoint: defaultIdentityEndpoint, + DomainName: defaultDomainName, + }, + expected: "vkcloud: unable to build VK Cloud client: password is missing in credentials information", + }, + { + desc: "missing dns endpoint", + config: &Config{ + ProjectID: fakeProjectID, + Username: fakeUsername, + Password: fakePasswd, + IdentityEndpoint: defaultIdentityEndpoint, + DomainName: defaultDomainName, + }, + expected: "vkcloud: DNS endpoint is missing in config", + }, + { + desc: "missing identity endpoint", + config: &Config{ + ProjectID: fakeProjectID, + Username: fakeUsername, + Password: fakePasswd, + DNSEndpoint: defaultDNSEndpoint, + DomainName: defaultDomainName, + }, + expected: "vkcloud: unable to build VK Cloud client: identity endpoint is missing in config", + }, + { + desc: "missing domain name", + config: &Config{ + ProjectID: fakeProjectID, + Username: fakeUsername, + Password: fakePasswd, + DNSEndpoint: defaultDNSEndpoint, + IdentityEndpoint: defaultIdentityEndpoint, + }, + expected: "vkcloud: unable to build VK Cloud client: domain name is missing in config", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + p, err := NewDNSProviderConfig(test.config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestLivePresent(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.Present(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} + +func TestLiveCleanUp(t *testing.T) { + if !envTest.IsLiveTest() { + t.Skip("skipping live test") + } + + envTest.RestoreEnv() + provider, err := NewDNSProvider() + require.NoError(t, err) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +}