volcengine.go raw
1 // Package volcengine implements a DNS provider for solving the DNS-01 challenge using Volcano Engine.
2 package volcengine
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "sync"
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/ptr"
16 "github.com/volcengine/volc-sdk-golang/base"
17 volc "github.com/volcengine/volc-sdk-golang/service/dns"
18 )
19
20 // Environment variables names.
21 const (
22 envNamespace = "VOLC_"
23
24 EnvAccessKey = envNamespace + "ACCESSKEY"
25 EnvSecretKey = envNamespace + "SECRETKEY"
26
27 EnvRegion = envNamespace + "REGION"
28 EnvHost = envNamespace + "HOST"
29 EnvScheme = envNamespace + "SCHEME"
30
31 EnvTTL = envNamespace + "TTL"
32 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
33 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
34 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
35 )
36
37 // https://www.volcengine.com/docs/6758/170354
38 const defaultTTL = 600
39
40 var _ challenge.ProviderTimeout = (*DNSProvider)(nil)
41
42 // Config is used to configure the creation of the DNSProvider.
43 type Config struct {
44 AccessKey string
45 SecretKey string
46
47 Region string
48 Host string
49 Scheme string
50
51 PropagationTimeout time.Duration
52 PollingInterval time.Duration
53 TTL int
54 HTTPTimeout time.Duration
55 }
56
57 // NewDefaultConfig returns a default configuration for the DNSProvider.
58 func NewDefaultConfig() *Config {
59 return &Config{
60 Scheme: env.GetOrDefaultString(EnvScheme, "https"),
61 Host: env.GetOrDefaultString(EnvHost, "open.volcengineapi.com"),
62 Region: env.GetOrDefaultString(EnvRegion, volc.DefaultRegion),
63
64 TTL: env.GetOrDefaultInt(EnvTTL, defaultTTL),
65 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, 4*time.Minute),
66 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, 10*time.Second),
67 HTTPTimeout: env.GetOrDefaultSecond(EnvHTTPTimeout, volc.Timeout*time.Second),
68 }
69 }
70
71 // DNSProvider implements the challenge.Provider interface.
72 type DNSProvider struct {
73 client *volc.Client
74 config *Config
75
76 recordIDs map[string]*string
77 recordIDsMu sync.Mutex
78 }
79
80 // NewDNSProvider returns a DNSProvider instance configured for Volcano Engine.
81 // Credentials must be passed in the environment variable: VOLC_ACCESSKEY, VOLC_SECRETKEY.
82 func NewDNSProvider() (*DNSProvider, error) {
83 values, err := env.Get(EnvAccessKey, EnvSecretKey)
84 if err != nil {
85 return nil, fmt.Errorf("volcengine: %w", err)
86 }
87
88 config := NewDefaultConfig()
89 config.AccessKey = values[EnvAccessKey]
90 config.SecretKey = values[EnvSecretKey]
91
92 return NewDNSProviderConfig(config)
93 }
94
95 // NewDNSProviderConfig return a DNSProvider instance configured for Volcano Engine.
96 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
97 if config == nil {
98 return nil, errors.New("volcengine: the configuration of the DNS provider is nil")
99 }
100
101 if config.AccessKey == "" || config.SecretKey == "" {
102 return nil, errors.New("volcengine: missing credentials")
103 }
104
105 return &DNSProvider{
106 config: config,
107 client: newClient(config),
108 recordIDs: make(map[string]*string),
109 }, nil
110 }
111
112 // Timeout returns the timeout and interval to use when checking for DNS propagation.
113 // Adjusting here to cope with spikes in propagation times.
114 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
115 return d.config.PropagationTimeout, d.config.PollingInterval
116 }
117
118 // Present creates a TXT record to fulfill the dns-01 challenge.
119 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
120 ctx := context.Background()
121
122 info := dns01.GetChallengeInfo(domain, keyAuth)
123
124 zone, err := d.getZone(ctx, info.EffectiveFQDN)
125 if err != nil {
126 return fmt.Errorf("volcengine: get zone ID: %w", err)
127 }
128
129 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, ptr.Deref(zone.ZoneName))
130 if err != nil {
131 return fmt.Errorf("volcengine: %w", err)
132 }
133
134 crr := &volc.CreateRecordRequest{
135 Host: ptr.Pointer(subDomain),
136 TTL: ptr.Pointer(int64(d.config.TTL)),
137 Type: ptr.Pointer("TXT"),
138 Value: ptr.Pointer(info.Value),
139 ZID: zone.ZID,
140 }
141
142 record, err := d.client.CreateRecord(ctx, crr)
143 if err != nil {
144 return fmt.Errorf("volcengine: create record: %w", err)
145 }
146
147 d.recordIDsMu.Lock()
148 d.recordIDs[token] = record.RecordID
149 d.recordIDsMu.Unlock()
150
151 return nil
152 }
153
154 // CleanUp removes the TXT record matching the specified parameters.
155 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
156 info := dns01.GetChallengeInfo(domain, keyAuth)
157
158 // gets the record's unique ID
159 d.recordIDsMu.Lock()
160 recordID, ok := d.recordIDs[token]
161 d.recordIDsMu.Unlock()
162
163 if !ok {
164 return fmt.Errorf("volcengine: unknown record ID for '%s' '%s'", info.EffectiveFQDN, token)
165 }
166
167 drr := &volc.DeleteRecordRequest{RecordID: recordID}
168
169 err := d.client.DeleteRecord(context.Background(), drr)
170 if err != nil {
171 return fmt.Errorf("volcengine: delete record: %w", err)
172 }
173
174 d.recordIDsMu.Lock()
175 delete(d.recordIDs, token)
176 d.recordIDsMu.Unlock()
177
178 return nil
179 }
180
181 func (d *DNSProvider) getZone(ctx context.Context, fqdn string) (volc.TopZoneResponse, error) {
182 for domain := range dns01.UnFqdnDomainsSeq(fqdn) {
183 lzr := &volc.ListZonesRequest{
184 Key: ptr.Pointer(dns01.UnFqdn(domain)),
185 SearchMode: ptr.Pointer("exact"),
186 }
187
188 zones, err := d.client.ListZones(ctx, lzr)
189 if err != nil {
190 return volc.TopZoneResponse{}, fmt.Errorf("list zones: %w", err)
191 }
192
193 total := ptr.Deref(zones.Total)
194
195 if total == 0 || len(zones.Zones) == 0 {
196 continue
197 }
198
199 if total > 1 {
200 return volc.TopZoneResponse{}, fmt.Errorf("too many zone for %s", domain)
201 }
202
203 return zones.Zones[0], nil
204 }
205
206 return volc.TopZoneResponse{}, fmt.Errorf("zone no found for fqdn: %s", fqdn)
207 }
208
209 // https://github.com/volcengine/volc-sdk-golang/tree/main/service/dns
210 // https://github.com/volcengine/volc-sdk-golang/blob/main/example/dns/demo_dns_test.go
211 func newClient(config *Config) *volc.Client {
212 // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/config.go#L20-L35
213 serviceInfo := &base.ServiceInfo{
214 Timeout: config.HTTPTimeout,
215 Host: config.Host,
216 Header: http.Header{"Accept": []string{"application/json"}},
217 Scheme: config.Scheme,
218 Credentials: base.Credentials{
219 Service: volc.ServiceName,
220 Region: config.Region,
221 AccessKeyID: config.AccessKey,
222 SecretAccessKey: config.SecretKey,
223 },
224 }
225
226 // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L17-L19
227 client := base.NewClient(serviceInfo, nil)
228
229 // https://github.com/volcengine/volc-sdk-golang/blob/fae992a31d02754e271c322095413d374ea4ea1b/service/dns/caller.go#L25-L34
230 caller := &volc.VolcCaller{Volc: client}
231 caller.Volc.SetAccessKey(serviceInfo.Credentials.AccessKeyID)
232 caller.Volc.SetSecretKey(serviceInfo.Credentials.SecretAccessKey)
233 caller.Volc.SetHost(serviceInfo.Host)
234 caller.Volc.SetScheme(serviceInfo.Scheme)
235 caller.Volc.SetTimeout(serviceInfo.Timeout)
236
237 return volc.NewClient(caller)
238 }
239