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