Add DNS provider for Selectel v2 (#2152)

Co-authored-by: Fernandez Ludovic <ldez@users.noreply.github.com>
This commit is contained in:
Artem Chirkov 2024-04-10 13:29:29 +03:00 committed by GitHub
parent a832515ad5
commit 8623f0df01
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 631 additions and 14 deletions

View file

@ -79,13 +79,13 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns).
| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [plesk.com](https://go-acme.github.io/lego/dns/plesk/) | [Porkbun](https://go-acme.github.io/lego/dns/porkbun/) |
| [PowerDNS](https://go-acme.github.io/lego/dns/pdns/) | [Rackspace](https://go-acme.github.io/lego/dns/rackspace/) | [RcodeZero](https://go-acme.github.io/lego/dns/rcodezero/) | [reg.ru](https://go-acme.github.io/lego/dns/regru/) |
| [RFC2136](https://go-acme.github.io/lego/dns/rfc2136/) | [RimuHosting](https://go-acme.github.io/lego/dns/rimuhosting/) | [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/) | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/) | [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/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [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/) | [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/) | [Webnames](https://go-acme.github.io/lego/dns/webnames/) | [Websupport](https://go-acme.github.io/lego/dns/websupport/) |
| [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) | [Yandex PDD](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/) | | |
| [Selectel v2](https://go-acme.github.io/lego/dns/selectelv2/) | [Selectel](https://go-acme.github.io/lego/dns/selectel/) | [Servercow](https://go-acme.github.io/lego/dns/servercow/) | [Shellrent](https://go-acme.github.io/lego/dns/shellrent/) |
| [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/) | [Ultradns](https://go-acme.github.io/lego/dns/ultradns/) | [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/) |
| [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/) | [Webnames](https://go-acme.github.io/lego/dns/webnames/) |
| [Websupport](https://go-acme.github.io/lego/dns/websupport/) | [WEDOS](https://go-acme.github.io/lego/dns/wedos/) | [Yandex 360](https://go-acme.github.io/lego/dns/yandex360/) | [Yandex Cloud](https://go-acme.github.io/lego/dns/yandexcloud/) |
| [Yandex PDD](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 -->

View file

@ -119,6 +119,7 @@ func allDNSCodes() string {
"sakuracloud",
"scaleway",
"selectel",
"selectelv2",
"servercow",
"shellrent",
"simply",
@ -2398,6 +2399,30 @@ func displayDNSHelp(w io.Writer, name string) error {
ew.writeln()
ew.writeln(`More information: https://go-acme.github.io/lego/dns/selectel`)
case "selectelv2":
// generated from: providers/dns/selectelv2/selectelv2.toml
ew.writeln(`Configuration for Selectel v2.`)
ew.writeln(`Code: 'selectelv2'`)
ew.writeln(`Since: 'v4.17.0'`)
ew.writeln()
ew.writeln(`Credentials:`)
ew.writeln(` - "SELECTELV2_ACCOUNT_ID": Selectel account ID (INT)`)
ew.writeln(` - "SELECTELV2_PASSWORD": Openstack username's password`)
ew.writeln(` - "SELECTELV2_PROJECT_ID": Cloud project ID (UUID)`)
ew.writeln(` - "SELECTELV2_USERNAME": Openstack username`)
ew.writeln()
ew.writeln(`Additional Configuration:`)
ew.writeln(` - "SELECTELV2_BASE_URL": API endpoint URL`)
ew.writeln(` - "SELECTELV2_HTTP_TIMEOUT": API request timeout`)
ew.writeln(` - "SELECTELV2_POLLING_INTERVAL": Time between DNS propagation check`)
ew.writeln(` - "SELECTELV2_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`)
ew.writeln(` - "SELECTELV2_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/selectelv2`)
case "servercow":
// generated from: providers/dns/servercow/servercow.toml
ew.writeln(`Configuration for Servercow.`)

View file

@ -0,0 +1,75 @@
---
title: "Selectel v2"
date: 2019-03-03T16:39:46+01:00
draft: false
slug: selectelv2
dnsprovider:
since: "v4.17.0"
code: "selectelv2"
url: "https://selectel.ru"
---
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/selectelv2/selectelv2.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
Configuration for [Selectel v2](https://selectel.ru).
<!--more-->
- Code: `selectelv2`
- Since: v4.17.0
Here is an example bash command using the Selectel v2 provider:
```bash
SELECTEL_USERNAME=trex \
SELECTEL_PASSWORD=xxxxx \
SELECTEL_ACCOUNT_ID=1234567 \
SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \
lego --email you@example.com --dns selectelv2 --domains my.example.org run
```
## Credentials
| Environment Variable Name | Description |
|-----------------------|-------------|
| `SELECTELV2_ACCOUNT_ID` | Selectel account ID (INT) |
| `SELECTELV2_PASSWORD` | Openstack username's password |
| `SELECTELV2_PROJECT_ID` | Cloud project ID (UUID) |
| `SELECTELV2_USERNAME` | Openstack username |
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 |
|--------------------------------|-------------|
| `SELECTELV2_BASE_URL` | API endpoint URL |
| `SELECTELV2_HTTP_TIMEOUT` | API request timeout |
| `SELECTELV2_POLLING_INTERVAL` | Time between DNS propagation check |
| `SELECTELV2_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation |
| `SELECTELV2_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" >}}).
## More information
- [API documentation](https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/)
- [Go client](https://github.com/selectel/domains-go)
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->
<!-- providers/dns/selectelv2/selectelv2.toml -->
<!-- THIS DOCUMENTATION IS AUTO-GENERATED. PLEASE DO NOT EDIT. -->

View file

@ -137,7 +137,7 @@ To display the documentation for a specific DNS provider, run:
$ lego dnshelp -c code
Supported DNS providers:
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, servercow, shellrent, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
acme-dns, alidns, allinkl, arvancloud, auroradns, autodns, azure, azuredns, bindman, bluecat, brandit, bunny, checkdomain, civo, clouddns, cloudflare, cloudns, cloudru, cloudxns, conoha, constellix, cpanel, derak, desec, designate, digitalocean, dnshomede, dnsimple, dnsmadeeasy, dnspod, dode, domeneshop, dreamhost, duckdns, dyn, dynu, easydns, edgedns, efficientip, epik, exec, exoscale, freemyip, gandi, gandiv5, gcloud, gcore, glesys, godaddy, googledomains, hetzner, hostingde, hosttech, httpnet, httpreq, hurricane, hyperone, ibmcloud, iij, iijdpf, infoblox, infomaniak, internetbs, inwx, ionos, ipv64, iwantmyname, joker, liara, lightsail, linode, liquidweb, loopia, luadns, mailinabox, manual, metaname, mydnsjp, mythicbeasts, namecheap, namedotcom, namesilo, nearlyfreespeech, netcup, netlify, nicmanager, nifcloud, njalla, nodion, ns1, oraclecloud, otc, ovh, pdns, plesk, porkbun, rackspace, rcodezero, regru, rfc2136, rimuhosting, route53, safedns, sakuracloud, scaleway, selectel, selectelv2, servercow, shellrent, simply, sonic, stackpath, tencentcloud, transip, ultradns, variomedia, vegadns, vercel, versio, vinyldns, vkcloud, vscale, vultr, webnames, websupport, wedos, yandex, yandex360, yandexcloud, zoneee, zonomi
More information: https://go-acme.github.io/lego/dns
"""

4
go.mod
View file

@ -35,7 +35,7 @@ require (
github.com/go-jose/go-jose/v4 v4.0.1
github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1
github.com/google/go-querystring v1.1.0
github.com/gophercloud/gophercloud v1.0.0
github.com/gophercloud/gophercloud v1.5.0
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae
github.com/hashicorp/go-retryablehttp v0.7.5
github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df
@ -65,6 +65,8 @@ require (
github.com/sacloud/api-client-go v0.2.8
github.com/sacloud/iaas-api-go v1.11.1
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22
github.com/selectel/domains-go v1.0.2
github.com/selectel/go-selvpcclient/v3 v3.1.1
github.com/softlayer/softlayer-go v1.1.3
github.com/stretchr/testify v1.8.4
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.0.490

14
go.sum
View file

@ -304,8 +304,8 @@ github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5m
github.com/googleapis/gax-go/v2 v2.11.0 h1:9V9PWXEsWnPpQhu/PeQIkS4eGzMlTLGgt80cUUI8Ki4=
github.com/googleapis/gax-go/v2 v2.11.0/go.mod h1:DxmR61SGKkGLa2xigwuZIQpkCI2S5iydzRfb3peWZJI=
github.com/gophercloud/gophercloud v0.15.1-0.20210202035223-633d73521055/go.mod h1:wRtmUelyIIv3CSSDI47aUwbs075O6i+LY+pXsKCBsb4=
github.com/gophercloud/gophercloud v1.0.0 h1:9nTGx0jizmHxDobe4mck89FyQHVyA3CaXLIUSGJjP9k=
github.com/gophercloud/gophercloud v1.0.0/go.mod h1:Q8fZtyi5zZxPS/j9aj3sSxtvj41AdQMDwyo1myduD5c=
github.com/gophercloud/gophercloud v1.5.0 h1:cDN6XFCLKiiqvYpjQLq9AiM7RDRbIC9450WpPH+yvXo=
github.com/gophercloud/gophercloud v1.5.0/go.mod h1:aAVqcocTSXh2vYFZ1JTvx4EQmfgzxRcNupUfxZbBNDM=
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae h1:Hi3IgB9RQDE15Kfovd8MTZrcana+UlQqNbOif8dLpA0=
github.com/gophercloud/utils v0.0.0-20210216074907-f6de111f2eae/go.mod h1:wx8HMD8oQD0Ryhz6+6ykq75PJ79iPyEqYHfwZ4l7OsA=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
@ -357,8 +357,8 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt
github.com/infobloxopen/infoblox-go-client v1.1.1 h1:728A6LbLjptj/7kZjHyIxQnm768PWHfGFm0HH8FnbtU=
github.com/infobloxopen/infoblox-go-client v1.1.1/go.mod h1:BXiw7S2b9qJoM8MS40vfgCNB2NLHGusk1DtO16BD9zI=
github.com/jarcoal/httpmock v1.0.8/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik=
github.com/jarcoal/httpmock v1.3.0 h1:2RJ8GP0IIaWwcC9Fp2BmVi8Kog3v2Hn7VXM3fTd+nuc=
github.com/jarcoal/httpmock v1.3.0/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jarcoal/httpmock v1.3.1 h1:iUx3whfZWVf3jT01hQTO/Eo5sAYtB2/rqaUuOtpInww=
github.com/jarcoal/httpmock v1.3.1/go.mod h1:3yb8rc4BI7TCBhFY8ng0gjuLKJNquuDNiPaZjnENuYg=
github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k=
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
@ -572,6 +572,10 @@ github.com/sacloud/packages-go v0.0.9/go.mod h1:k+EEUMF2LlncjbNIJNOqLyZ9wjTESPIW
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22 h1:wJrcTdddKOI8TFxs8cemnhKP2EmKy3yfUKHj3ZdfzYo=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.22/go.mod h1:fCa7OJZ/9DRTnOKmxvT6pn+LPWUptQAmHF/SBJUGEcg=
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/selectel/domains-go v1.0.2 h1:Si6iGaMnTFJxwiJVI50DOdZnwcxc87kqaWrVQYW0a4U=
github.com/selectel/domains-go v1.0.2/go.mod h1:SugRKfq4sTpnOHquslCpzda72wV8u0cMBHx0C0l+bzA=
github.com/selectel/go-selvpcclient/v3 v3.1.1 h1:C1q2LqqosiapoLpnGITGmysg0YCSQYDo2Gh69CioevM=
github.com/selectel/go-selvpcclient/v3 v3.1.1/go.mod h1:NM7IXhh1IzqZ88DOw1Qc5Ez3tULLViXo95l5+rKPuyQ=
github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
@ -683,10 +687,10 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210817164053-32db794688a5/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.0.0-20211202192323-5770296d904e/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220314234659-1baeb1ce4c0b/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58=
golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=

View file

@ -110,6 +110,7 @@ import (
"github.com/go-acme/lego/v4/providers/dns/sakuracloud"
"github.com/go-acme/lego/v4/providers/dns/scaleway"
"github.com/go-acme/lego/v4/providers/dns/selectel"
"github.com/go-acme/lego/v4/providers/dns/selectelv2"
"github.com/go-acme/lego/v4/providers/dns/servercow"
"github.com/go-acme/lego/v4/providers/dns/shellrent"
"github.com/go-acme/lego/v4/providers/dns/simply"
@ -351,6 +352,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) {
return scaleway.NewDNSProvider()
case "selectel":
return selectel.NewDNSProvider()
case "selectelv2":
return selectelv2.NewDNSProvider()
case "servercow":
return servercow.NewDNSProvider()
case "shellrent":

View file

@ -0,0 +1,293 @@
// Package selectelv2 implements a DNS provider for solving the DNS-01 challenge using Selectel Domains APIv2.
package selectelv2
import (
"context"
"errors"
"fmt"
"net/http"
"strings"
"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/internal/selectel"
selectelapi "github.com/selectel/domains-go/pkg/v2"
"github.com/selectel/go-selvpcclient/v3/selvpcclient"
)
const tokenHeader = "X-Auth-Token"
const (
defaultBaseURL = "https://api.selectel.ru/domains/v2"
defaultTTL = 60
defaultPropagationTimeout = 120 * time.Second
defaultPollingInterval = 5 * time.Second
defaultHTTPTimeout = 30 * time.Second
)
const (
envNamespace = "SELECTELV2_"
EnvBaseURL = envNamespace + "BASE_URL"
EnvUsernameOS = envNamespace + "USERNAME"
EnvPasswordOS = envNamespace + "PASSWORD"
EnvAccount = envNamespace + "ACCOUNT_ID"
EnvProjectID = envNamespace + "PROJECT_ID"
EnvTTL = envNamespace + "TTL"
EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
)
var errNotFound = errors.New("rrset not found")
// Config is used to configure the creation of the DNSProvider.
type Config struct {
BaseURL string
Username string
Password string
Account string
ProjectID string
TTL int
PropagationTimeout time.Duration
PollingInterval time.Duration
HTTPClient *http.Client
}
// NewDefaultConfig returns a default configuration for the DNSProvider.
func NewDefaultConfig() *Config {
return &Config{
BaseURL: env.GetOrDefaultString(EnvBaseURL, selectel.DefaultSelectelBaseURL),
TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL),
PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, defaultPropagationTimeout),
PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, defaultPollingInterval),
HTTPClient: &http.Client{
Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, defaultHTTPTimeout),
},
}
}
type DNSProvider struct {
baseClient selectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]
config *Config
}
// NewDNSProvider returns a DNSProvider instance configured for Selectel Domains APIv2.
func NewDNSProvider() (*DNSProvider, error) {
values, err := env.Get(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID)
if err != nil {
return nil, fmt.Errorf("selectelv2: %w", err)
}
config := NewDefaultConfig()
config.Username = values[EnvUsernameOS]
config.Password = values[EnvPasswordOS]
config.Account = values[EnvAccount]
config.ProjectID = values[EnvProjectID]
return NewDNSProviderConfig(config)
}
// NewDNSProviderConfig return a DNSProvider instance configured for selectel.
func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
if config == nil {
return nil, errors.New("selectelv2: the configuration of the DNS provider is nil")
}
if config.Username == "" {
return nil, errors.New("selectelv2: missing username")
}
if config.Password == "" {
return nil, errors.New("selectelv2: missing password")
}
if config.Account == "" {
return nil, errors.New("selectelv2: missing account")
}
if config.ProjectID == "" {
return nil, errors.New("selectelv2: missing project ID")
}
headers := http.Header{}
headers.Set("User-Agent", "lego/selectelv2")
return &DNSProvider{
baseClient: selectelapi.NewClient(defaultBaseURL, config.HTTPClient, headers),
config: config,
}, nil
}
// Timeout returns the Timeout and interval to use when checking for DNS propagation.
// Adjusting here to cope with spikes in propagation times.
func (p *DNSProvider) Timeout() (timeout, interval time.Duration) {
return p.config.PropagationTimeout, p.config.PollingInterval
}
// Present creates a TXT record to fulfill DNS-01 challenge.
func (p *DNSProvider) Present(domain, _, keyAuth string) error {
ctx := context.Background()
client, err := p.authorize()
if err != nil {
return fmt.Errorf("selectelv2: authorize: %w", err)
}
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := client.getZone(ctx, domain)
if err != nil {
return fmt.Errorf("selectelv2: get zone: %w", err)
}
rrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)
if err != nil {
if !errors.Is(err, errNotFound) {
return fmt.Errorf("selectelv2: get RRSet: %w", err)
}
newRRSet := &selectelapi.RRSet{
Name: info.EffectiveFQDN,
Type: selectelapi.TXT,
TTL: p.config.TTL,
Records: []selectelapi.RecordItem{{Content: fmt.Sprintf("%q", info.Value)}},
}
_, err = client.CreateRRSet(ctx, zone.ID, newRRSet)
if err != nil {
return fmt.Errorf("selectelv2: create RRSet: %w", err)
}
return nil
}
rrset.Records = append(rrset.Records, selectelapi.RecordItem{Content: fmt.Sprintf("%q", info.Value)})
err = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)
if err != nil {
return fmt.Errorf("selectelv2: update RRSet: %w", err)
}
return nil
}
// CleanUp removes a TXT record used for DNS-01 challenge.
func (p *DNSProvider) CleanUp(domain, _, keyAuth string) error {
ctx := context.Background()
client, err := p.authorize()
if err != nil {
return fmt.Errorf("selectelv2: authorize: %w", err)
}
info := dns01.GetChallengeInfo(domain, keyAuth)
zone, err := client.getZone(ctx, domain)
if err != nil {
return fmt.Errorf("selectelv2: get zone: %w", err)
}
rrset, err := client.getRRset(ctx, dns01.UnFqdn(info.EffectiveFQDN), zone.ID)
if err != nil {
return fmt.Errorf("selectelv2: get RRSet: %w", err)
}
if len(rrset.Records) <= 1 {
err = client.DeleteRRSet(ctx, zone.ID, rrset.ID)
if err != nil {
return fmt.Errorf("selectelv2: %w", err)
}
return nil
}
for i, item := range rrset.Records {
if strings.Trim(item.Content, `"`) == info.Value {
rrset.Records = append(rrset.Records[:i], rrset.Records[i+1:]...)
break
}
}
err = client.UpdateRRSet(ctx, zone.ID, rrset.ID, rrset)
if err != nil {
return fmt.Errorf("selectelv2: update RRSet: %w", err)
}
return nil
}
func (p *DNSProvider) authorize() (*clientWrapper, error) {
token, err := obtainOpenstackToken(p.config)
if err != nil {
return nil, err
}
extraHeaders := http.Header{}
extraHeaders.Set(tokenHeader, token)
return &clientWrapper{
DNSClient: p.baseClient.WithHeaders(extraHeaders),
}, nil
}
func obtainOpenstackToken(config *Config) (string, error) {
vpcClient, err := selvpcclient.NewClient(&selvpcclient.ClientOptions{
Username: config.Username,
Password: config.Password,
UserDomainName: config.Account,
ProjectID: config.ProjectID,
})
if err != nil {
return "", fmt.Errorf("new VPC client: %w", err)
}
return vpcClient.GetXAuthToken(), nil
}
type clientWrapper struct {
selectelapi.DNSClient[selectelapi.Zone, selectelapi.RRSet]
}
func (w *clientWrapper) getZone(ctx context.Context, name string) (*selectelapi.Zone, error) {
params := &map[string]string{"filter": name}
zones, err := w.ListZones(ctx, params)
if err != nil {
return nil, fmt.Errorf("list zone: %w", err)
}
for _, zone := range zones.GetItems() {
if zone.Name == dns01.ToFqdn(name) {
return zone, nil
}
}
if len(strings.Split(dns01.UnFqdn(name), ".")) == 1 {
return nil, errors.New("zone for challenge has not been found")
}
// -1 can not be returned since if no dots present we exit above
i := strings.Index(name, ".")
return w.getZone(ctx, name[i+1:])
}
func (w *clientWrapper) getRRset(ctx context.Context, name, zoneID string) (*selectelapi.RRSet, error) {
params := &map[string]string{"name": name, "rrset_types": string(selectelapi.TXT)}
resp, err := w.ListRRSets(ctx, zoneID, params)
if err != nil {
return nil, fmt.Errorf("list rrset: %w", err)
}
for _, rrset := range resp.GetItems() {
if rrset.Name == dns01.ToFqdn(name) {
return rrset, nil
}
}
return nil, errNotFound
}

View file

@ -0,0 +1,30 @@
Name = "Selectel v2"
Description = ''''''
URL = "https://selectel.ru"
Code = "selectelv2"
Since = "v4.17.0"
Example = '''
SELECTEL_USERNAME=trex \
SELECTEL_PASSWORD=xxxxx \
SELECTEL_ACCOUNT_ID=1234567 \
SELECTEL_PROJECT_ID=111a11111aaa11aa1a11aaa11111aa1a \
lego --email you@example.com --dns selectelv2 --domains my.example.org run
'''
[Configuration]
[Configuration.Credentials]
SELECTELV2_USERNAME = "Openstack username"
SELECTELV2_PASSWORD = "Openstack username's password"
SELECTELV2_ACCOUNT_ID = "Selectel account ID (INT)"
SELECTELV2_PROJECT_ID = "Cloud project ID (UUID)"
[Configuration.Additional]
SELECTELV2_BASE_URL = "API endpoint URL"
SELECTELV2_POLLING_INTERVAL = "Time between DNS propagation check"
SELECTELV2_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation"
SELECTELV2_TTL = "The TTL of the TXT record used for the DNS challenge"
SELECTELV2_HTTP_TIMEOUT = "API request timeout"
[Links]
API = "https://developers.selectel.ru/docs/cloud-services/dns_api/dns_api_actual/"
GoClient = "https://github.com/selectel/domains-go"

View file

@ -0,0 +1,185 @@
package selectelv2
import (
"testing"
"time"
"github.com/go-acme/lego/v4/platform/tester"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
const envDomain = envNamespace + "DOMAIN"
var envTest = tester.NewEnvTest(EnvUsernameOS, EnvPasswordOS, EnvAccount, EnvProjectID).
WithDomain(envDomain)
func TestNewDNSProvider(t *testing.T) {
testCases := []struct {
desc string
envVars map[string]string
expected string
}{
{
desc: "success",
envVars: map[string]string{
EnvUsernameOS: "someName",
EnvPasswordOS: "qwerty",
EnvAccount: "1",
EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a",
},
},
{
desc: "missing username",
envVars: map[string]string{
EnvPasswordOS: "qwerty",
EnvAccount: "1",
EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a",
},
expected: "selectelv2: some credentials information are missing: SELECTELV2_USERNAME",
},
{
desc: "missing password",
envVars: map[string]string{
EnvUsernameOS: "someName",
EnvAccount: "1",
EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a",
},
expected: "selectelv2: some credentials information are missing: SELECTELV2_PASSWORD",
},
{
desc: "missing account",
envVars: map[string]string{
EnvUsernameOS: "someName",
EnvPasswordOS: "qwerty",
EnvProjectID: "111a11111aaa11aa1a11aaa11111aa1a",
},
expected: "selectelv2: some credentials information are missing: SELECTELV2_ACCOUNT_ID",
},
{
desc: "missing project",
envVars: map[string]string{
EnvUsernameOS: "someName",
EnvPasswordOS: "qwerty",
EnvAccount: "1",
},
expected: "selectelv2: some credentials information are missing: SELECTELV2_PROJECT_ID",
},
}
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)
assert.NotNil(t, p.config)
assert.NotNil(t, p.baseClient)
} else {
require.EqualError(t, err, test.expected)
}
})
}
}
func TestNewDNSProviderConfig(t *testing.T) {
testCases := []struct {
desc string
username string
password string
account string
projectID string
expected string
}{
{
desc: "success",
username: "user",
password: "secret",
account: "1",
projectID: "111a11111aaa11aa1a11aaa11111aa1a",
},
{
desc: "missing username",
password: "secret",
account: "1",
projectID: "111a11111aaa11aa1a11aaa11111aa1a",
expected: "selectelv2: missing username",
},
{
desc: "missing password",
username: "user",
account: "1",
projectID: "111a11111aaa11aa1a11aaa11111aa1a",
expected: "selectelv2: missing password",
},
{
desc: "missing account",
username: "user",
password: "secret",
projectID: "111a11111aaa11aa1a11aaa11111aa1a",
expected: "selectelv2: missing account",
},
{
desc: "missing projectID",
username: "user",
password: "secret",
account: "1",
expected: "selectelv2: missing project ID",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
config := NewDefaultConfig()
config.Username = test.username
config.Password = test.password
config.Account = test.account
config.ProjectID = test.projectID
p, err := NewDNSProviderConfig(config)
if test.expected == "" {
require.NoError(t, err)
require.NotNil(t, p)
assert.NotNil(t, p.config)
assert.NotNil(t, p.baseClient)
} 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)
time.Sleep(2 * time.Second)
err = provider.CleanUp(envTest.GetDomain(), "", "123d==")
require.NoError(t, err)
}