Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add DNS provider for VK Cloud #1706

Merged
merged 12 commits into from Sep 2, 2022
Merged
2 changes: 1 addition & 1 deletion .golangci.toml
@@ -1,5 +1,5 @@
[run]
timeout = "5m"
timeout = "7m"
skip-files = []

[linters-settings]
Expand Down
6 changes: 3 additions & 3 deletions README.md
Expand Up @@ -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/) | |

<!-- END DNS PROVIDERS LIST -->

Expand Down
2 changes: 0 additions & 2 deletions cmd/account.go
Expand Up @@ -29,5 +29,3 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey {
func (a *Account) GetRegistration() *registration.Resource {
return a.Registration
}

/** End **/
25 changes: 25 additions & 0 deletions cmd/zz_gen_cmd_dnshelp.go
Expand Up @@ -112,6 +112,7 @@ func allDNSCodes() string {
"vercel",
"versio",
"vinyldns",
"vkcloud",
"vscale",
"vultr",
"wedos",
Expand Down Expand Up @@ -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.`)
Expand Down
83 changes: 83 additions & 0 deletions 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/"
---

<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/vkcloud/vkcloud.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->


Configuration for [VK Cloud](https://mcs.mail.ru/).


<!--more-->

- Code: `vkcloud`
- Since: v4.9.0


Here is an example bash command using the VK Cloud provider:

```bash
VK_CLOUD_PROJECT_ID="<your_project_id>" \
VK_CLOUD_USERNAME="<your_email>" \
VK_CLOUD_PASSWORD="<your_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)

<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/vkcloud/vkcloud.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
3 changes: 3 additions & 0 deletions providers/dns/dns_providers.go
Expand Up @@ -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"
Expand Down Expand Up @@ -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":
Expand Down
160 changes: 160 additions & 0 deletions 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
}
23 changes: 23 additions & 0 deletions 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"`
}