sakuracloud.go raw
1 // Package sakuracloud implements a DNS provider for solving the DNS-01 challenge using SakuraCloud DNS.
2 package sakuracloud
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "strings"
10 "time"
11
12 "github.com/go-acme/lego/v4/challenge"
13 "github.com/go-acme/lego/v4/challenge/dns01"
14 "github.com/go-acme/lego/v4/platform/config/env"
15 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
16 "github.com/go-acme/lego/v4/providers/dns/internal/useragent"
17 client "github.com/sacloud/api-client-go"
18 "github.com/sacloud/iaas-api-go"
19 "github.com/sacloud/iaas-api-go/defaults"
20 "github.com/sacloud/iaas-api-go/helper/api"
21 )
22
23 // Environment variables names.
24 const (
25 envNamespace = "SAKURACLOUD_"
26
27 EnvAccessToken = envNamespace + "ACCESS_TOKEN"
28 EnvAccessTokenSecret = envNamespace + "ACCESS_TOKEN_SECRET"
29
30 EnvTTL = envNamespace + "TTL"
31 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
32 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
33 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
34 )
35
36 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
37
38 // Config is used to configure the creation of the DNSProvider.
39 type Config struct {
40 Token string
41 Secret string
42 PropagationTimeout time.Duration
43 PollingInterval time.Duration
44 TTL int
45 HTTPClient *http.Client
46 }
47
48 // NewDefaultConfig returns a default configuration for the DNSProvider.
49 func NewDefaultConfig() *Config {
50 return &Config{
51 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
52 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
53 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
54 HTTPClient: &http.Client{
55 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 10*time.Second),
56 },
57 }
58 }
59
60 // DNSProvider implements the challenge.Provider interface.
61 type DNSProvider struct {
62 config *Config
63 client iaas.DNSAPI
64 }
65
66 // NewDNSProvider returns a DNSProvider instance configured for SakuraCloud.
67 // Credentials must be passed in the environment variables:
68 // SAKURACLOUD_ACCESS_TOKEN & SAKURACLOUD_ACCESS_TOKEN_SECRET.
69 func NewDNSProvider() (*DNSProvider, error) {
70 values, err := env.Get(EnvAccessToken, EnvAccessTokenSecret)
71 if err != nil {
72 return nil, fmt.Errorf("sakuracloud: %w", err)
73 }
74
75 config := NewDefaultConfig()
76 config.Token = values[EnvAccessToken]
77 config.Secret = values[EnvAccessTokenSecret]
78
79 return NewDNSProviderConfig(config)
80 }
81
82 // NewDNSProviderConfig return a DNSProvider instance configured for SakuraCloud.
83 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
84 if config == nil {
85 return nil, errors.New("sakuracloud: the configuration of the DNS provider is nil")
86 }
87
88 if config.Token == "" {
89 return nil, errors.New("sakuracloud: AccessToken is missing")
90 }
91
92 if config.Secret == "" {
93 return nil, errors.New("sakuracloud: AccessSecret is missing")
94 }
95
96 defaultOption, err := api.DefaultOption()
97 if err != nil {
98 return nil, fmt.Errorf("sakuracloud: %w", err)
99 }
100
101 options := &api.CallerOptions{
102 Options: &client.Options{
103 AccessToken: config.Token,
104 AccessTokenSecret: config.Secret,
105 HttpClient: clientdebug.Wrap(config.HTTPClient),
106 UserAgent: fmt.Sprintf("%s %s", iaas.DefaultUserAgent, useragent.Get()),
107 },
108 }
109
110 return &DNSProvider{
111 client: iaas.NewDNSOp(newCallerWithOptions(api.MergeOptions(defaultOption, options))),
112 config: config,
113 }, nil
114 }
115
116 // Present creates a TXT record to fulfill the dns-01 challenge.
117 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
118 info := dns01.GetChallengeInfo(domain, keyAuth)
119
120 err := d.addTXTRecord(context.Background(), info.EffectiveFQDN, info.Value, d.config.TTL)
121 if err != nil {
122 return fmt.Errorf("sakuracloud: %w", err)
123 }
124
125 return nil
126 }
127
128 // CleanUp removes the TXT record matching the specified parameters.
129 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
130 info := dns01.GetChallengeInfo(domain, keyAuth)
131
132 err := d.cleanupTXTRecord(context.Background(), info.EffectiveFQDN, info.Value)
133 if err != nil {
134 return fmt.Errorf("sakuracloud: %w", err)
135 }
136
137 return nil
138 }
139
140 // Timeout returns the timeout and interval to use when checking for DNS propagation.
141 // Adjusting here to cope with spikes in propagation times.
142 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
143 return d.config.PropagationTimeout, d.config.PollingInterval
144 }
145
146 // Extracted from https://github.com/sacloud/iaas-api-go/blob/af06b3ccc2c38625d2dc684ad39590d0ae13eed3/helper/api/caller.go#L36-L81
147 // Trace and fake are removed.
148 // Related to https://github.com/sacloud/iaas-api-go/issues/376.
149 func newCallerWithOptions(opts *api.CallerOptions) iaas.APICaller {
150 return newCaller(opts)
151 }
152
153 func newCaller(opts *api.CallerOptions) iaas.APICaller {
154 if opts.UserAgent == "" {
155 opts.UserAgent = iaas.DefaultUserAgent
156 }
157
158 caller := iaas.NewClientWithOptions(opts.Options)
159
160 defaults.DefaultStatePollingTimeout = 72 * time.Hour
161
162 if opts.DefaultZone != "" {
163 iaas.APIDefaultZone = opts.DefaultZone
164 }
165
166 if len(opts.Zones) > 0 {
167 iaas.SakuraCloudZones = opts.Zones
168 }
169
170 if opts.APIRootURL != "" {
171 if strings.HasSuffix(opts.APIRootURL, "/") {
172 opts.APIRootURL = strings.TrimRight(opts.APIRootURL, "/")
173 }
174
175 iaas.SakuraCloudAPIRoot = opts.APIRootURL
176 }
177
178 return caller
179 }
180