Skip to content

Commit a4e048d

Browse files
Craig Petersonldez
authored andcommitted
implement dynamic provider to delegate to other providers dynamically
1 parent a5f0a3f commit a4e048d

File tree

2 files changed

+255
-0
lines changed

2 files changed

+255
-0
lines changed

providers/dns/multi/config.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package multi
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"strings"
9+
)
10+
11+
// MultiProviderConfig is the configuration for a multiple provider setup. This is expected to be given in json format via
12+
// MULTI_CONFIG environment variable, or in a file location specified by MULTI_CONFIG_FILE.
13+
type MultiProviderConfig struct {
14+
// Domain names to list of provider names
15+
Domains map[string][]string
16+
// Provider Name -> Key/Value pairs for environment
17+
Providers map[string]map[string]string
18+
}
19+
20+
// providerNamesForDomain chooses the most appropriate domain from the config and returns its' list of dns providers
21+
// looks for most specific match to least specific, one dot at a time. Finally folling back to "default" domain.
22+
func (m *MultiProviderConfig) providerNamesForDomain(domain string) ([]string, error) {
23+
parts := strings.Split(domain, ".")
24+
var names []string
25+
for i := 0; i < len(parts); i++ {
26+
partial := strings.Join(parts[i:], ".")
27+
if names = m.Domains[partial]; names != nil {
28+
break
29+
}
30+
}
31+
if names == nil {
32+
names = m.Domains["default"]
33+
}
34+
if names == nil {
35+
return nil, fmt.Errorf("Couldn't find any suitable dns provider for domain %s", domain)
36+
}
37+
return names, nil
38+
}
39+
40+
func getConfig() (*MultiProviderConfig, error) {
41+
var rawJSON []byte
42+
var err error
43+
if cfg := os.Getenv("MULTI_CONFIG"); cfg != "" {
44+
rawJSON = []byte(cfg)
45+
} else if path := os.Getenv("MULTI_CONFIG_FILE"); path != "" {
46+
if rawJSON, err = ioutil.ReadFile(path); err != nil {
47+
return nil, err
48+
}
49+
} else {
50+
return nil, fmt.Errorf("'multi' provider requires json config in MULTI_CONFIG or MULTI_CONFIG_PATH")
51+
}
52+
cfg := &MultiProviderConfig{}
53+
if err = json.Unmarshal(rawJSON, cfg); err != nil {
54+
return nil, err
55+
}
56+
return cfg, nil
57+
}

providers/dns/multi/multi.go

Lines changed: 198 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,198 @@
1+
// package multi implements a dynamic challenge provider that can select different dns providers for different domains,
2+
// and even multiple distinct dns providers and accounts for each individual domain. This can be useful if:
3+
//
4+
// - Multiple dns providers are used for active-active redundant dns service
5+
//
6+
// - You need a single certificate issued for different domains, each using different dns services
7+
//
8+
// Configuration is given by selecting DNS provider type "multi", and by giving further per-domain information via a json object:
9+
//
10+
// {
11+
// "Providers": {
12+
// "cloudflare": {
13+
// "CLOUDFLARE_EMAIL": "[email protected]",
14+
// "CLOUDFLARE_API_KEY": "123qwerty"
15+
// },
16+
// "digitalocean":{
17+
// "DO_AUTH_TOKEN": "456uiop"
18+
// }
19+
// }
20+
// "Domains": {
21+
// "example.com": ["digitalocean"],
22+
// "example.org": ["cloudflare"],
23+
// "example.net": ["digitalocean, cloudflare"]
24+
// }
25+
// }
26+
//
27+
// In the above json, each "Provider" is a named provider instance along with the associated credentials. The credentials will be set as environment
28+
// variables as appropriate when the provider is instantiated for the first time.
29+
//
30+
// If the provider name is the same as a registered provider type (like "cloudflare"), the type will be inferred. If it is not the same (perhaps in cases where multiple
31+
// different accounts are involved), you may specify it with the `type` field on the provider object.
32+
//
33+
// Domains are then linked to one or more of the named providers by name. Challenges will be filled on every provider specified for the domain. When looking for a domain
34+
// configuration, config domains will be checked from most specific to least specific by each dot. For example, to fill a challenge for `foo.example.com`,
35+
// a configured domain for `foo.example.com` will be looked for, failing that it will look for `example.com` and `com` in that order. If there is still no match and a
36+
// domain with the name `default` is found, that will be used. Otherwise an error will be returned.
37+
//
38+
// The json configuration for domains can be specified directly via environment variable (`MULTI_CONFIG`), or from a file referenced by `MULTI_CONFIG_FILE`.
39+
package multi
40+
41+
import (
42+
"fmt"
43+
"os"
44+
"time"
45+
46+
"github.com/xenolf/lego/acme"
47+
)
48+
49+
// NewDNSChallengeProviderByName is defined here to avoid recursive imports, this must be injected by the dns package so that
50+
// the delegated dns providers may be dynamically instantiated
51+
var NewDNSChallengeProviderByName func(string) (acme.ChallengeProvider, error)
52+
53+
type MultiProvider struct {
54+
config *MultiProviderConfig
55+
providers map[string]acme.ChallengeProvider
56+
}
57+
58+
// AggregateProvider is simply a list of dns providers. All Challenges are filled by all members of the aggregate.
59+
type AggregateProvider []acme.ChallengeProvider
60+
61+
func (a AggregateProvider) Present(domain, token, keyAuth string) error {
62+
for _, p := range a {
63+
if err := p.Present(domain, token, keyAuth); err != nil {
64+
return err
65+
}
66+
}
67+
return nil
68+
}
69+
func (a AggregateProvider) CleanUp(domain, token, keyAuth string) error {
70+
for _, p := range a {
71+
if err := p.CleanUp(domain, token, keyAuth); err != nil {
72+
return err
73+
}
74+
}
75+
return nil
76+
}
77+
78+
// AggregateProviderTimeout is simply a list of dns providers. This type will be chosen when any of the 'subproviders' implement Timeout control.
79+
// All Challenges are filled by all members of the aggregate.
80+
// Timeout returned will be the maximum time of any child provider.
81+
type AggregateProviderTimeout struct {
82+
AggregateProvider
83+
}
84+
85+
func (a AggregateProviderTimeout) Timeout() (timeout, interval time.Duration) {
86+
for _, p := range a.AggregateProvider {
87+
if to, ok := p.(acme.ChallengeProviderTimeout); ok {
88+
t, i := to.Timeout()
89+
if t > timeout {
90+
timeout = t
91+
}
92+
if i > interval {
93+
interval = i
94+
}
95+
}
96+
}
97+
return
98+
}
99+
100+
func (m *MultiProvider) getProviderForDomain(domain string) (acme.ChallengeProvider, error) {
101+
names, err := m.config.providerNamesForDomain(domain)
102+
if err != nil {
103+
return nil, err
104+
}
105+
var agg AggregateProvider
106+
anyTimeouts := false
107+
for _, n := range names {
108+
p, err := m.providerByName(n)
109+
if err != nil {
110+
return nil, err
111+
}
112+
if _, ok := p.(acme.ChallengeProviderTimeout); ok {
113+
anyTimeouts = true
114+
}
115+
agg = append(agg, p)
116+
}
117+
// don't wrap provider in aggregate if there is only one
118+
if len(agg) == 1 {
119+
return agg[0], nil
120+
}
121+
if anyTimeouts {
122+
return AggregateProviderTimeout{agg}, nil
123+
}
124+
return agg, nil
125+
}
126+
127+
func (m *MultiProvider) providerByName(name string) (acme.ChallengeProvider, error) {
128+
if p, ok := m.providers[name]; ok {
129+
return p, nil
130+
}
131+
if params, ok := m.config.Providers[name]; ok {
132+
return m.buildProvider(name, params)
133+
}
134+
return nil, fmt.Errorf("Couldn't find appropriate config for dns provider named '%s'", name)
135+
}
136+
137+
func (m *MultiProvider) buildProvider(name string, params map[string]string) (acme.ChallengeProvider, error) {
138+
pType := name
139+
origEnv := map[string]string{}
140+
141+
// copy parameters into environment, keeping track of previous values
142+
for k, v := range params {
143+
if k == "type" {
144+
pType = v
145+
continue
146+
}
147+
if oldVal, ok := os.LookupEnv(k); ok {
148+
origEnv[k] = oldVal
149+
}
150+
os.Setenv(k, v)
151+
}
152+
// restore previous values
153+
defer func() {
154+
for k := range params {
155+
if k == "type" {
156+
continue
157+
}
158+
if oldVal, ok := origEnv[k]; ok {
159+
os.Setenv(k, oldVal)
160+
} else {
161+
os.Unsetenv(k)
162+
}
163+
}
164+
}()
165+
prv, err := NewDNSChallengeProviderByName(pType)
166+
if err != nil {
167+
return nil, err
168+
}
169+
m.providers[name] = prv
170+
return prv, nil
171+
}
172+
173+
func New() (*MultiProvider, error) {
174+
config, err := getConfig()
175+
if err != nil {
176+
return nil, err
177+
}
178+
return &MultiProvider{
179+
providers: map[string]acme.ChallengeProvider{},
180+
config: config,
181+
}, nil
182+
}
183+
184+
func (m *MultiProvider) Present(domain, token, keyAuth string) error {
185+
provider, err := m.getProviderForDomain(domain)
186+
if err != nil {
187+
return err
188+
}
189+
return provider.Present(domain, token, keyAuth)
190+
}
191+
192+
func (m *MultiProvider) CleanUp(domain, token, keyAuth string) error {
193+
provider, err := m.getProviderForDomain(domain)
194+
if err != nil {
195+
return err
196+
}
197+
return provider.CleanUp(domain, token, keyAuth)
198+
}

0 commit comments

Comments
 (0)