scaleway.go raw
1 // Package scaleway implements a DNS provider for solving the DNS-01 challenge using Scaleway Domains API.
2 // Token: https://www.scaleway.com/en/docs/generate-an-api-token/
3 package scaleway
4
5 import (
6 "errors"
7 "fmt"
8 "net/http"
9 "strconv"
10 "strings"
11 "time"
12
13 "github.com/go-acme/lego/v4/challenge"
14 "github.com/go-acme/lego/v4/challenge/dns01"
15 "github.com/go-acme/lego/v4/platform/config/env"
16 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
17 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
18 scwdomain "github.com/scaleway/scaleway-sdk-go/api/domain/v2beta1"
19 "github.com/scaleway/scaleway-sdk-go/scw"
20 )
21
22 // Environment variables names.
23 const (
24 envNamespace = "SCALEWAY_"
25
26 EnvAPIToken = envNamespace + "API_TOKEN"
27 EnvProjectID = envNamespace + "PROJECT_ID"
28
29 altEnvNamespace = "SCW_"
30
31 EnvAccessKey = altEnvNamespace + "ACCESS_KEY"
32 EnvSecretKey = altEnvNamespace + "SECRET_KEY"
33
34 EnvTTL = envNamespace + "TTL"
35 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
36 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
37 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
38 )
39
40 const (
41 minTTL = 60
42 defaultPollingInterval = 10 * time.Second
43 defaultPropagationTimeout = 120 * time.Second
44 )
45
46 // The access key is not used by the Scaleway client.
47 const dumpAccessKey = "SCWXXXXXXXXXXXXXXXXX"
48
49 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
50
51 // Config is used to configure the creation of the DNSProvider.
52 type Config struct {
53 ProjectID string
54 Token string // TODO(ldez) rename to SecretKey in the next major.
55 AccessKey string
56
57 PropagationTimeout time.Duration
58 PollingInterval time.Duration
59 TTL int
60 HTTPClient *http.Client
61 }
62
63 // NewDefaultConfig returns a default configuration for the DNSProvider.
64 func NewDefaultConfig() *Config {
65 return &Config{
66 AccessKey: dumpAccessKey,
67 TTL: env.GetOneWithFallback(EnvTTL, minTTL, strconv.Atoi, altEnvName(EnvTTL)),
68 PropagationTimeout: env.GetOneWithFallback(EnvPropagationTimeout, defaultPropagationTimeout, env.ParseSecond, altEnvName(EnvPropagationTimeout)),
69 PollingInterval: env.GetOneWithFallback(EnvPollingInterval, defaultPollingInterval, env.ParseSecond, altEnvName(EnvPollingInterval)),
70 HTTPClient: &http.Client{
71 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
72 },
73 }
74 }
75
76 // DNSProvider implements the challenge.Provider interface.
77 type DNSProvider struct {
78 config *Config
79 client *scwdomain.API
80 }
81
82 // NewDNSProvider returns a DNSProvider instance configured for Scaleway Domains API.
83 // Credentials must be passed in the environment variables:
84 // SCALEWAY_API_TOKEN, SCALEWAY_PROJECT_ID.
85 func NewDNSProvider() (*DNSProvider, error) {
86 values, err := env.GetWithFallback([]string{EnvSecretKey, EnvAPIToken})
87 if err != nil {
88 return nil, fmt.Errorf("scaleway: %w", err)
89 }
90
91 config := NewDefaultConfig()
92 config.Token = values[EnvSecretKey]
93 config.AccessKey = env.GetOrDefaultString(EnvAccessKey, dumpAccessKey)
94 config.ProjectID = env.GetOrFile(EnvProjectID)
95
96 return NewDNSProviderConfig(config)
97 }
98
99 // NewDNSProviderConfig return a DNSProvider instance configured for scaleway.
100 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
101 if config == nil {
102 return nil, errors.New("scaleway: the configuration of the DNS provider is nil")
103 }
104
105 if config.Token == "" {
106 return nil, errors.New("scaleway: credentials missing")
107 }
108
109 if config.TTL < minTTL {
110 config.TTL = minTTL
111 }
112
113 configuration := []scw.ClientOption{
114 scw.WithAuth(config.AccessKey, config.Token),
115 scw.WithUserAgent(useragent.Get()),
116 }
117
118 if config.HTTPClient != nil {
119 configuration = append(configuration, scw.WithHTTPClient(clientdebug.Wrap(config.HTTPClient)))
120 }
121
122 if config.ProjectID != "" {
123 configuration = append(configuration, scw.WithDefaultProjectID(config.ProjectID))
124 }
125
126 // Create a Scaleway client
127 clientScw, err := scw.NewClient(configuration...)
128 if err != nil {
129 return nil, fmt.Errorf("scaleway: %w", err)
130 }
131
132 return &DNSProvider{config: config, client: scwdomain.NewAPI(clientScw)}, nil
133 }
134
135 // Timeout returns the Timeout and interval to use when checking for DNS propagation.
136 // Adjusting here to cope with spikes in propagation times.
137 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
138 return d.config.PropagationTimeout, d.config.PollingInterval
139 }
140
141 // Present creates a TXT record to fulfill DNS-01 challenge.
142 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
143 info := dns01.GetChallengeInfo(domain, keyAuth)
144
145 records := []*scwdomain.Record{{
146 Data: fmt.Sprintf(`%q`, info.Value),
147 Name: info.EffectiveFQDN,
148 TTL: uint32(d.config.TTL),
149 Type: scwdomain.RecordTypeTXT,
150 Comment: scw.StringPtr("used by lego"),
151 }}
152
153 req := &scwdomain.UpdateDNSZoneRecordsRequest{
154 DNSZone: info.EffectiveFQDN,
155 Changes: []*scwdomain.RecordChange{{
156 Add: &scwdomain.RecordChangeAdd{Records: records},
157 }},
158 ReturnAllRecords: scw.BoolPtr(false),
159 DisallowNewZoneCreation: true,
160 }
161
162 _, err := d.client.UpdateDNSZoneRecords(req)
163 if err != nil {
164 return fmt.Errorf("scaleway: %w", err)
165 }
166
167 return nil
168 }
169
170 // CleanUp removes a TXT record used for DNS-01 challenge.
171 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
172 info := dns01.GetChallengeInfo(domain, keyAuth)
173
174 recordIdentifier := &scwdomain.RecordIdentifier{
175 Name: info.EffectiveFQDN,
176 Type: scwdomain.RecordTypeTXT,
177 Data: scw.StringPtr(fmt.Sprintf(`%q`, info.Value)),
178 }
179
180 req := &scwdomain.UpdateDNSZoneRecordsRequest{
181 DNSZone: info.EffectiveFQDN,
182 Changes: []*scwdomain.RecordChange{{
183 Delete: &scwdomain.RecordChangeDelete{IDFields: recordIdentifier},
184 }},
185 ReturnAllRecords: scw.BoolPtr(false),
186 DisallowNewZoneCreation: true,
187 }
188
189 _, err := d.client.UpdateDNSZoneRecords(req)
190 if err != nil {
191 return fmt.Errorf("scaleway: %w", err)
192 }
193
194 return nil
195 }
196
197 func altEnvName(v string) string {
198 return strings.ReplaceAll(v, envNamespace, altEnvNamespace)
199 }
200