f5xc.go raw
1 // Package f5xc implements a DNS provider for solving the DNS-01 challenge using F5 XC.
2 package f5xc
3
4 import (
5 "context"
6 "errors"
7 "fmt"
8 "net/http"
9 "time"
10
11 "github.com/cenkalti/backoff/v5"
12 "github.com/go-acme/lego/v4/challenge/dns01"
13 "github.com/go-acme/lego/v4/platform/config/env"
14 "github.com/go-acme/lego/v4/platform/wait"
15 "github.com/go-acme/lego/v4/providers/dns/f5xc/internal"
16 "github.com/go-acme/lego/v4/providers/dns/internal/clientdebug"
17 )
18
19 // Environment variables names.
20 const (
21 envNamespace = "F5XC_"
22
23 EnvToken = envNamespace + "API_TOKEN"
24 EnvTenantName = envNamespace + "TENANT_NAME"
25 EnvServer = envNamespace + "SERVER"
26 EnvGroupName = envNamespace + "GROUP_NAME"
27
28 EnvTTL = envNamespace + "TTL"
29 EnvPropagationTimeout = envNamespace + "PROPAGATION_TIMEOUT"
30 EnvPollingInterval = envNamespace + "POLLING_INTERVAL"
31 EnvHTTPTimeout = envNamespace + "HTTP_TIMEOUT"
32 )
33
34 // Config is used to configure the creation of the DNSProvider.
35 type Config struct {
36 APIToken string
37 TenantName string
38 Server string
39 GroupName string
40
41 PropagationTimeout time.Duration
42 PollingInterval time.Duration
43 TTL int
44 HTTPClient *http.Client
45 }
46
47 // NewDefaultConfig returns a default configuration for the DNSProvider.
48 func NewDefaultConfig() *Config {
49 return &Config{
50 TTL: env.GetOrDefaultInt(EnvTTL, dns01.DefaultTTL),
51 PropagationTimeout: env.GetOrDefaultSecond(EnvPropagationTimeout, dns01.DefaultPropagationTimeout),
52 PollingInterval: env.GetOrDefaultSecond(EnvPollingInterval, dns01.DefaultPollingInterval),
53 HTTPClient: &http.Client{
54 Timeout: env.GetOrDefaultSecond(EnvHTTPTimeout, 30*time.Second),
55 },
56 }
57 }
58
59 // DNSProvider implements the challenge.Provider interface.
60 type DNSProvider struct {
61 config *Config
62 client *internal.Client
63 }
64
65 // NewDNSProvider returns a DNSProvider instance configured for F5 XC.
66 func NewDNSProvider() (*DNSProvider, error) {
67 values, err := env.Get(EnvToken, EnvTenantName, EnvGroupName)
68 if err != nil {
69 return nil, fmt.Errorf("f5xc: %w", err)
70 }
71
72 config := NewDefaultConfig()
73 config.APIToken = values[EnvToken]
74 config.TenantName = values[EnvTenantName]
75 config.GroupName = values[EnvGroupName]
76 config.Server = env.GetOrFile(EnvServer)
77
78 return NewDNSProviderConfig(config)
79 }
80
81 // NewDNSProviderConfig return a DNSProvider instance configured for F5 XC.
82 func NewDNSProviderConfig(config *Config) (*DNSProvider, error) {
83 if config == nil {
84 return nil, errors.New("f5xc: the configuration of the DNS provider is nil")
85 }
86
87 if config.GroupName == "" {
88 return nil, errors.New("f5xc: missing group name")
89 }
90
91 client, err := internal.NewClient(config.APIToken, config.TenantName, config.Server)
92 if err != nil {
93 return nil, fmt.Errorf("f5xc: %w", err)
94 }
95
96 if config.HTTPClient != nil {
97 client.HTTPClient = config.HTTPClient
98 }
99
100 client.HTTPClient = clientdebug.Wrap(client.HTTPClient)
101
102 return &DNSProvider{
103 config: config,
104 client: client,
105 }, nil
106 }
107
108 // Present creates a TXT record using the specified parameters.
109 func (d *DNSProvider) Present(domain, token, keyAuth string) error {
110 ctx := context.Background()
111
112 info := dns01.GetChallengeInfo(domain, keyAuth)
113
114 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
115 if err != nil {
116 return fmt.Errorf("f5xc: could not find zone for domain %q: %w", domain, err)
117 }
118
119 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
120 if err != nil {
121 return fmt.Errorf("f5xc: %w", err)
122 }
123
124 existingRRSet, err := d.client.GetRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT")
125 if err != nil {
126 return fmt.Errorf("f5xc: get RR Set: %w", err)
127 }
128
129 // New RRSet.
130 if existingRRSet == nil || existingRRSet.RRSet.TXTRecord == nil {
131 rrSet := internal.RRSet{
132 Description: "lego",
133 TTL: d.config.TTL,
134 TXTRecord: &internal.TXTRecord{
135 Name: subDomain,
136 Values: []string{info.Value},
137 },
138 }
139
140 return d.waitFor(ctx, func() error {
141 _, err = d.client.CreateRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, rrSet)
142 if err != nil {
143 return fmt.Errorf("create RR set: %w", err)
144 }
145
146 return nil
147 })
148 }
149
150 // Update RRSet.
151 existingRRSet.RRSet.TXTRecord.Values = append(existingRRSet.RRSet.TXTRecord.Values, info.Value)
152
153 return d.waitFor(ctx, func() error {
154 _, err = d.client.ReplaceRRSet(ctx, dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT", existingRRSet.RRSet)
155 if err != nil {
156 return fmt.Errorf("replace RR set: %w", err)
157 }
158
159 return nil
160 })
161 }
162
163 func (d *DNSProvider) waitFor(ctx context.Context, operation func() error) error {
164 err := wait.Retry(ctx, operation,
165 backoff.WithBackOff(backoff.NewConstantBackOff(2*time.Second)),
166 backoff.WithMaxElapsedTime(60*time.Second),
167 )
168 if err != nil {
169 return fmt.Errorf("f5xc: %w", err)
170 }
171
172 return nil
173 }
174
175 // CleanUp removes the TXT record matching the specified parameters.
176 func (d *DNSProvider) CleanUp(domain, token, keyAuth string) error {
177 info := dns01.GetChallengeInfo(domain, keyAuth)
178
179 authZone, err := dns01.FindZoneByFqdn(info.EffectiveFQDN)
180 if err != nil {
181 return fmt.Errorf("f5xc: could not find zone for domain %q: %w", domain, err)
182 }
183
184 subDomain, err := dns01.ExtractSubDomain(info.EffectiveFQDN, authZone)
185 if err != nil {
186 return fmt.Errorf("f5xc: %w", err)
187 }
188
189 _, err = d.client.DeleteRRSet(context.Background(), dns01.UnFqdn(authZone), d.config.GroupName, subDomain, "TXT")
190 if err != nil {
191 return fmt.Errorf("f5xc: delete RR set: %w", err)
192 }
193
194 return nil
195 }
196
197 // Timeout returns the timeout and interval to use when checking for DNS propagation.
198 // Adjusting here to cope with spikes in propagation times.
199 func (d *DNSProvider) Timeout() (timeout, interval time.Duration) {
200 return d.config.PropagationTimeout, d.config.PollingInterval
201 }
202