diff --git a/README.md b/README.md index 75ef0f2a..d41c2ac3 100644 --- a/README.md +++ b/README.md @@ -57,19 +57,20 @@ Detailed documentation is available [here](https://go-acme.github.io/lego/dns). | [External program](https://go-acme.github.io/lego/dns/exec/) | [freemyip.com](https://go-acme.github.io/lego/dns/freemyip/) | [G-Core Labs](https://go-acme.github.io/lego/dns/gcore/) | [Gandi Live DNS (v5)](https://go-acme.github.io/lego/dns/gandiv5/) | | [Gandi](https://go-acme.github.io/lego/dns/gandi/) | [Glesys](https://go-acme.github.io/lego/dns/glesys/) | [Go Daddy](https://go-acme.github.io/lego/dns/godaddy/) | [Google Cloud](https://go-acme.github.io/lego/dns/gcloud/) | | [Hetzner](https://go-acme.github.io/lego/dns/hetzner/) | [Hosting.de](https://go-acme.github.io/lego/dns/hostingde/) | [Hosttech](https://go-acme.github.io/lego/dns/hosttech/) | [HTTP request](https://go-acme.github.io/lego/dns/httpreq/) | -| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | -| [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | [Ionos](https://go-acme.github.io/lego/dns/ionos/) | -| [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | -| [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | -| [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | -| [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | -| [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | -| [OVH](https://go-acme.github.io/lego/dns/ovh/) | [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/) | -| [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/) | [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/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | -| [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](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/) | +| [Hurricane Electric DNS](https://go-acme.github.io/lego/dns/hurricane/) | [HyperOne](https://go-acme.github.io/lego/dns/hyperone/) | [IBM Cloud (SoftLayer)](https://go-acme.github.io/lego/dns/ibmcloud/) | [Infoblox](https://go-acme.github.io/lego/dns/infoblox/) | +| [Infomaniak](https://go-acme.github.io/lego/dns/infomaniak/) | [Internet Initiative Japan](https://go-acme.github.io/lego/dns/iij/) | [Internet.bs](https://go-acme.github.io/lego/dns/internetbs/) | [INWX](https://go-acme.github.io/lego/dns/inwx/) | +| [Ionos](https://go-acme.github.io/lego/dns/ionos/) | [Joker](https://go-acme.github.io/lego/dns/joker/) | [Joohoi's ACME-DNS](https://go-acme.github.io/lego/dns/acme-dns/) | [Linode (v4)](https://go-acme.github.io/lego/dns/linode/) | +| [Liquid Web](https://go-acme.github.io/lego/dns/liquidweb/) | [Loopia](https://go-acme.github.io/lego/dns/loopia/) | [LuaDNS](https://go-acme.github.io/lego/dns/luadns/) | [Manual](https://go-acme.github.io/lego/dns/manual/) | +| [MyDNS.jp](https://go-acme.github.io/lego/dns/mydnsjp/) | [MythicBeasts](https://go-acme.github.io/lego/dns/mythicbeasts/) | [Name.com](https://go-acme.github.io/lego/dns/namedotcom/) | [Namecheap](https://go-acme.github.io/lego/dns/namecheap/) | +| [Namesilo](https://go-acme.github.io/lego/dns/namesilo/) | [Netcup](https://go-acme.github.io/lego/dns/netcup/) | [Netlify](https://go-acme.github.io/lego/dns/netlify/) | [Nicmanager](https://go-acme.github.io/lego/dns/nicmanager/) | +| [NIFCloud](https://go-acme.github.io/lego/dns/nifcloud/) | [Njalla](https://go-acme.github.io/lego/dns/njalla/) | [NS1](https://go-acme.github.io/lego/dns/ns1/) | [Open Telekom Cloud](https://go-acme.github.io/lego/dns/otc/) | +| [Oracle Cloud](https://go-acme.github.io/lego/dns/oraclecloud/) | [OVH](https://go-acme.github.io/lego/dns/ovh/) | [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/) | [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/) | +| [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/) | [TransIP](https://go-acme.github.io/lego/dns/transip/) | +| [VegaDNS](https://go-acme.github.io/lego/dns/vegadns/) | [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](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/zz_gen_cmd_dnshelp.go b/cmd/zz_gen_cmd_dnshelp.go index e41a8503..c917c934 100644 --- a/cmd/zz_gen_cmd_dnshelp.go +++ b/cmd/zz_gen_cmd_dnshelp.go @@ -59,6 +59,7 @@ func allDNSCodes() string { "httpreq", "hurricane", "hyperone", + "ibmcloud", "iij", "infoblox", "infomaniak", @@ -1064,6 +1065,27 @@ func displayDNSHelp(name string) error { ew.writeln() ew.writeln(`More information: https://go-acme.github.io/lego/dns/hyperone`) + case "ibmcloud": + // generated from: providers/dns/ibmcloud/ibmcloud.toml + ew.writeln(`Configuration for IBM Cloud (SoftLayer).`) + ew.writeln(`Code: 'ibmcloud'`) + ew.writeln(`Since: 'v4.5.0'`) + ew.writeln() + + ew.writeln(`Credentials:`) + ew.writeln(` - "SOFTLAYER_API_KEY": Classic Infrastructure API key`) + ew.writeln(` - "SOFTLAYER_USERNAME": User name (IBM Cloud is _)`) + ew.writeln() + + ew.writeln(`Additional Configuration:`) + ew.writeln(` - "SOFTLAYER_POLLING_INTERVAL": Time between DNS propagation check`) + ew.writeln(` - "SOFTLAYER_PROPAGATION_TIMEOUT": Maximum waiting time for DNS propagation`) + ew.writeln(` - "SOFTLAYER_TIMEOUT": API request timeout`) + ew.writeln(` - "SOFTLAYER_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/ibmcloud`) + case "iij": // generated from: providers/dns/iij/iij.toml ew.writeln(`Configuration for Internet Initiative Japan.`) diff --git a/docs/content/dns/zz_gen_ibmcloud.md b/docs/content/dns/zz_gen_ibmcloud.md new file mode 100644 index 00000000..0881a256 --- /dev/null +++ b/docs/content/dns/zz_gen_ibmcloud.md @@ -0,0 +1,65 @@ +--- +title: "IBM Cloud (SoftLayer)" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: ibmcloud +--- + + + + + +Since: v4.5.0 + +Configuration for [IBM Cloud (SoftLayer)](https://www.ibm.com/cloud/). + + + + +- Code: `ibmcloud` + +Here is an example bash command using the IBM Cloud (SoftLayer) provider: + +```bash +SOFTLAYER_USERNAME=xxxxx \ +SOFTLAYER_API_KEY=yyyyy \ +lego --email myemail@example.com --dns ibmcloud --domains my.example.org run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `SOFTLAYER_API_KEY` | Classic Infrastructure API key | +| `SOFTLAYER_USERNAME` | User name (IBM Cloud is _) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check | +| `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SOFTLAYER_TIMEOUT` | API request timeout | +| `SOFTLAYER_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](/lego/dns/#configuration-and-credentials). + + + + +## More information + +- [API documentation](https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api) +- [Go client](https://github.com/softlayer/softlayer-go) + + + + diff --git a/docs/content/dns/zz_gen_softlayer.md b/docs/content/dns/zz_gen_softlayer.md new file mode 100644 index 00000000..79ed6675 --- /dev/null +++ b/docs/content/dns/zz_gen_softlayer.md @@ -0,0 +1,65 @@ +--- +title: "SoftLayer (IBM Cloud)" +date: 2019-03-03T16:39:46+01:00 +draft: false +slug: softlayer +--- + + + + + +Since: v4.5.0 + +Configuration for [SoftLayer (IBM Cloud)](https://www.ibm.com/cloud/). + + + + +- Code: `softlayer` + +Here is an example bash command using the SoftLayer (IBM Cloud) provider: + +```bash +SOFTLAYER_USERNAME=xxxxx \ +SOFTLAYER_API_KEY=yyyyy \ +lego --email myemail@example.com --dns softlayer --domains my.example.org run +``` + + + + +## Credentials + +| Environment Variable Name | Description | +|-----------------------|-------------| +| `SOFTLAYER_API_KEY` | Classic Infrastructure API key | +| `SOFTLAYER_USERNAME` | User name (IBM Cloud is _) | + +The environment variable names can be suffixed by `_FILE` to reference a file instead of a value. +More information [here](/lego/dns/#configuration-and-credentials). + + +## Additional Configuration + +| Environment Variable Name | Description | +|--------------------------------|-------------| +| `SOFTLAYER_POLLING_INTERVAL` | Time between DNS propagation check | +| `SOFTLAYER_PROPAGATION_TIMEOUT` | Maximum waiting time for DNS propagation | +| `SOFTLAYER_TIMEOUT` | API request timeout | +| `SOFTLAYER_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](/lego/dns/#configuration-and-credentials). + + + + +## More information + +- [API documentation](https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api) +- [Go client](https://github.com/softlayer/softlayer-go) + + + + diff --git a/go.mod b/go.mod index 978985d1..c48af651 100644 --- a/go.mod +++ b/go.mod @@ -47,6 +47,7 @@ require ( github.com/rainycape/memcache v0.0.0-20150622160815-1031fa0ce2f2 github.com/sacloud/libsacloud v1.36.2 github.com/scaleway/scaleway-sdk-go v1.0.0-beta.7.0.20210127161313-bd30bebeac4f + github.com/softlayer/softlayer-go v1.0.3 github.com/stretchr/testify v1.7.0 github.com/transip/gotransip/v6 v6.6.1 github.com/urfave/cli v1.22.5 diff --git a/go.sum b/go.sum index 10ba1491..305a9bc8 100644 --- a/go.sum +++ b/go.sum @@ -248,6 +248,7 @@ github.com/iij/doapi v0.0.0-20190504054126-0bbf12d6d7df/go.mod h1:QMZY7/J/KSQEhK github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= 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.5/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jarcoal/httpmock v1.0.6 h1:e81vOSexXU3mJuJ4l//geOmKIt+Vkxerk1feQBC8D0g= github.com/jarcoal/httpmock v1.0.6/go.mod h1:ATjnClrvW/3tijVmpL/va5Z3aAyGvqU3gCT8nX0Txik= github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= @@ -435,6 +436,10 @@ github.com/smartystreets/goconvey v1.6.4 h1:fv0U8FUIMPNf1L9lnHLvLhgicrIVChEkdzIK github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= github.com/smartystreets/gunit v1.0.4 h1:tpTjnuH7MLlqhoD21vRoMZbMIi5GmBsAJDFyF67GhZA= github.com/smartystreets/gunit v1.0.4/go.mod h1:EH5qMBab2UclzXUcpR8b93eHsIlp9u+pDQIRp5DZNzQ= +github.com/softlayer/softlayer-go v1.0.3 h1:9FONm5xzQ9belQtbdryR6gBg4EF6hX6lrjNKi0IvZkU= +github.com/softlayer/softlayer-go v1.0.3/go.mod h1:6HepcfAXROz0Rf63krk5hPZyHT6qyx2MNvYyHof7ik4= +github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e h1:3OgWYFw7jxCZPcvAg+4R8A50GZ+CCkARF10lxu2qDsQ= +github.com/softlayer/xmlrpc v0.0.0-20200409220501-5f089df7cb7e/go.mod h1:fKZCUVdirrxrBpwd9wb+lSoVixvpwAu8eHzbQB2tums= github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ= @@ -479,6 +484,7 @@ github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2 github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= +github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= @@ -686,6 +692,7 @@ golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapK golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= +golang.org/x/tools v0.0.0-20200410194907-79a7a3126eef/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20201224043029-2b0845dc783e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/providers/dns/dns_providers.go b/providers/dns/dns_providers.go index a2958639..f25c2840 100644 --- a/providers/dns/dns_providers.go +++ b/providers/dns/dns_providers.go @@ -50,6 +50,7 @@ import ( "github.com/go-acme/lego/v4/providers/dns/httpreq" "github.com/go-acme/lego/v4/providers/dns/hurricane" "github.com/go-acme/lego/v4/providers/dns/hyperone" + "github.com/go-acme/lego/v4/providers/dns/ibmcloud" "github.com/go-acme/lego/v4/providers/dns/iij" "github.com/go-acme/lego/v4/providers/dns/infoblox" "github.com/go-acme/lego/v4/providers/dns/infomaniak" @@ -195,6 +196,8 @@ func NewDNSChallengeProviderByName(name string) (challenge.Provider, error) { return hurricane.NewDNSProvider() case "hyperone": return hyperone.NewDNSProvider() + case "ibmcloud": + return ibmcloud.NewDNSProvider() case "iij": return iij.NewDNSProvider() case "infoblox": diff --git a/providers/dns/ibmcloud/ibmcloud.go b/providers/dns/ibmcloud/ibmcloud.go new file mode 100644 index 00000000..bc64b8ec --- /dev/null +++ b/providers/dns/ibmcloud/ibmcloud.go @@ -0,0 +1,129 @@ +// Package ibmcloud implements a DNS provider for solving the DNS-01 challenge using IBM Cloud (SoftLayer). +package ibmcloud + +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/ibmcloud/internal" + "github.com/softlayer/softlayer-go/session" +) + +// Environment variables names. +const ( + envNamespace = "SOFTLAYER_" + + // EnvUsername the name must be the same as here: + // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L171 + EnvUsername = envNamespace + "USERNAME" + // EnvAPIKey the name must be the same as here: + // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L175 + EnvAPIKey = envNamespace + "API_KEY" + // EnvHTTPTimeout the name must be the same as here: + // https://github.com/softlayer/softlayer-go/blob/534185047ea683dd1e29fd23e445598295d94be4/session/session.go#L182 + EnvHTTPTimeout = envNamespace + "TIMEOUT" + EnvDebug = envNamespace + "DEBUG" + + EnvTTL = envNamespace + "TTL" + EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT" + EnvPollingInterval = envNamespace + "POLLING_INTERVAL" +) + +// Config is used to configure the creation of the DNSProvider. +type Config struct { + Username string + APIKey string + PropagationTimeout time.Duration + PollingInterval time.Duration + TTL int + HTTPTimeout time.Duration + Debug bool +} + +// NewDefaultConfig returns a default configuration for the DNSProvider. +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, session.DefaultTimeout), + } +} + +// DNSProvider implements the challenge.Provider interface. +type DNSProvider struct { + config *Config + wrapper *internal.Wrapper +} + +// NewDNSProvider returns a DNSProvider instance configured for IBM Cloud (SoftLayer). +// Credentials must be passed in the environment variables: +// SOFTLAYER_USERNAME, SOFTLAYER_API_KEY. +func NewDNSProvider() (*DNSProvider, error) { + values, err := env.Get(EnvUsername, EnvAPIKey) + if err != nil { + return nil, fmt.Errorf("ibmcloud: %w", err) + } + + config := NewDefaultConfig() + config.Username = values[EnvUsername] + config.APIKey = values[EnvAPIKey] + config.Debug = env.GetOrDefaultBool(EnvDebug, false) + + return NewDNSProviderConfig(config) +} + +// NewDNSProviderConfig return a DNSProvider instance configured for IBM Cloud (SoftLayer). +func NewDNSProviderConfig(config *Config) (*DNSProvider, error) { + if config == nil { + return nil, errors.New("ibmcloud: the configuration of the DNS provider is nil") + } + + if config.Username == "" { + return nil, errors.New("ibmcloud: username is missing") + } + + if config.APIKey == "" { + return nil, errors.New("ibmcloud: API key is missing") + } + + sess := session.New(config.Username, config.APIKey) + + sess.Timeout = config.HTTPTimeout + sess.Debug = config.Debug + + return &DNSProvider{wrapper: internal.NewWrapper(sess), 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 (d *DNSProvider) Timeout() (timeout, interval time.Duration) { + return d.config.PropagationTimeout, d.config.PollingInterval +} + +// Present creates a TXT record to fulfill the dns-01 challenge. +func (d *DNSProvider) Present(domain, token, keyAuth string) error { + fqdn, value := dns01.GetRecord(domain, keyAuth) + + err := d.wrapper.AddTXTRecord(fqdn, domain, value, d.config.TTL) + if err != nil { + return fmt.Errorf("ibmcloud: %w", err) + } + + return nil +} + +// CleanUp removes the TXT record matching the specified parameters. +func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error { + fqdn, _ := dns01.GetRecord(domain, keyAuth) + + err := d.wrapper.CleanupTXTRecord(fqdn, domain) + if err != nil { + return fmt.Errorf("ibmcloud: %w", err) + } + + return nil +} diff --git a/providers/dns/ibmcloud/ibmcloud.toml b/providers/dns/ibmcloud/ibmcloud.toml new file mode 100644 index 00000000..8f0f0898 --- /dev/null +++ b/providers/dns/ibmcloud/ibmcloud.toml @@ -0,0 +1,25 @@ +Name = "IBM Cloud (SoftLayer)" +Description = '''''' +URL = "https://www.ibm.com/cloud/" +Code = "ibmcloud" +Since = "v4.5.0" + +Example = ''' +SOFTLAYER_USERNAME=xxxxx \ +SOFTLAYER_API_KEY=yyyyy \ +lego --email myemail@example.com --dns ibmcloud --domains my.example.org run +''' + +[Configuration] + [Configuration.Credentials] + SOFTLAYER_USERNAME = "User name (IBM Cloud is _)" + SOFTLAYER_API_KEY = "Classic Infrastructure API key" + [Configuration.Additional] + SOFTLAYER_POLLING_INTERVAL = "Time between DNS propagation check" + SOFTLAYER_PROPAGATION_TIMEOUT = "Maximum waiting time for DNS propagation" + SOFTLAYER_TTL = "The TTL of the TXT record used for the DNS challenge" + SOFTLAYER_TIMEOUT = "API request timeout" + +[Links] + API = "https://cloud.ibm.com/docs/dns?topic=dns-getting-started-with-the-dns-api" + GoClient = "https://github.com/softlayer/softlayer-go" diff --git a/providers/dns/ibmcloud/ibmcloud_test.go b/providers/dns/ibmcloud/ibmcloud_test.go new file mode 100644 index 00000000..a000e3e5 --- /dev/null +++ b/providers/dns/ibmcloud/ibmcloud_test.go @@ -0,0 +1,150 @@ +package ibmcloud + +import ( + "testing" + "time" + + "github.com/go-acme/lego/v4/platform/tester" + "github.com/stretchr/testify/require" +) + +const envDomain = envNamespace + "DOMAIN" + +var envTest = tester.NewEnvTest(EnvUsername, EnvAPIKey). + WithDomain(envDomain) + +func TestNewDNSProvider(t *testing.T) { + testCases := []struct { + desc string + envVars map[string]string + expected string + }{ + { + desc: "success", + envVars: map[string]string{ + EnvUsername: "123", + EnvAPIKey: "456", + }, + }, + { + desc: "missing credentials", + envVars: map[string]string{ + EnvUsername: "", + EnvAPIKey: "", + }, + expected: "ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME,SOFTLAYER_API_KEY", + }, + { + desc: "missing access token", + envVars: map[string]string{ + EnvUsername: "", + EnvAPIKey: "456", + }, + expected: "ibmcloud: some credentials information are missing: SOFTLAYER_USERNAME", + }, + { + desc: "missing token secret", + envVars: map[string]string{ + EnvUsername: "123", + EnvAPIKey: "", + }, + expected: "ibmcloud: some credentials information are missing: SOFTLAYER_API_KEY", + }, + } + + 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) + require.NotNil(t, p.wrapper) + } else { + require.EqualError(t, err, test.expected) + } + }) + } +} + +func TestNewDNSProviderConfig(t *testing.T) { + testCases := []struct { + desc string + username string + apiKey string + expected string + }{ + { + desc: "success", + username: "123", + apiKey: "456", + }, + { + desc: "missing credentials", + expected: "ibmcloud: username is missing", + }, + { + desc: "missing token", + apiKey: "456", + expected: "ibmcloud: username is missing", + }, + { + desc: "missing secret", + username: "123", + expected: "ibmcloud: API key is missing", + }, + } + + for _, test := range testCases { + t.Run(test.desc, func(t *testing.T) { + config := NewDefaultConfig() + config.Username = test.username + config.APIKey = test.apiKey + + p, err := NewDNSProviderConfig(config) + + if test.expected == "" { + require.NoError(t, err) + require.NotNil(t, p) + require.NotNil(t, p.config) + require.NotNil(t, p.wrapper) + } 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(1 * time.Second) + + err = provider.CleanUp(envTest.GetDomain(), "", "123d==") + require.NoError(t, err) +} diff --git a/providers/dns/ibmcloud/internal/wrapper.go b/providers/dns/ibmcloud/internal/wrapper.go new file mode 100644 index 00000000..6129d9c1 --- /dev/null +++ b/providers/dns/ibmcloud/internal/wrapper.go @@ -0,0 +1,115 @@ +package internal + +import ( + "fmt" + + "github.com/softlayer/softlayer-go/datatypes" + "github.com/softlayer/softlayer-go/services" + "github.com/softlayer/softlayer-go/session" + "github.com/softlayer/softlayer-go/sl" +) + +type Wrapper struct { + session *session.Session +} + +func NewWrapper(sess *session.Session) *Wrapper { + return &Wrapper{session: sess} +} + +func (w Wrapper) AddTXTRecord(fqdn, domain, value string, ttl int) error { + service := services.GetDnsDomainService(w.session) + + domainID, err := getDomainID(service, domain) + if err != nil { + return fmt.Errorf("failed to get domain ID: %w", err) + } + + service.Options.Id = domainID + + if _, err := service.CreateTxtRecord(sl.String(fqdn), sl.String(value), sl.Int(ttl)); err != nil { + return fmt.Errorf("failed to create TXT record: %w", err) + } + + return nil +} + +func (w Wrapper) CleanupTXTRecord(fqdn, domain string) error { + service := services.GetDnsDomainService(w.session) + + domainID, err := getDomainID(service, domain) + if err != nil { + return fmt.Errorf("failed to get domain ID: %w", err) + } + + service.Options.Id = domainID + + records, err := findTxtRecords(service, fqdn) + if err != nil { + return fmt.Errorf("failed to find TXT records: %w", err) + } + + return deleteResourceRecords(service, records) +} + +func getDomainID(service services.Dns_Domain, domain string) (*int, error) { + res, err := service.GetByDomainName(sl.String(domain)) + if err != nil { + return nil, err + } + + for _, r := range res { + if r.Id == nil || toString(r.Name) != domain { + continue + } + + return r.Id, nil + } + + return nil, fmt.Errorf("no data found of domain: %s", domain) +} + +func findTxtRecords(service services.Dns_Domain, fqdn string) ([]datatypes.Dns_Domain_ResourceRecord, error) { + var results []datatypes.Dns_Domain_ResourceRecord + + records, err := service.GetResourceRecords() + if err != nil { + return nil, err + } + + for _, record := range records { + if toString(record.Host) == fqdn && toString(record.Type) == "txt" { + results = append(results, record) + } + } + + if len(results) == 0 { + return nil, fmt.Errorf("no data found of fqdn: %s", fqdn) + } + + return results, nil +} + +func deleteResourceRecords(service services.Dns_Domain, records []datatypes.Dns_Domain_ResourceRecord) error { + resourceRecord := services.GetDnsDomainResourceRecordService(service.Session) + + // TODO maybe a bug: only the last record will be deleted + for _, record := range records { + resourceRecord.Options.Id = record.Id + } + + _, err := resourceRecord.DeleteObject() + if err != nil { + return fmt.Errorf("no data found of fqdn: %w", err) + } + + return nil +} + +func toString(v *string) string { + if v == nil { + return "" + } + + return *v +}