config.go raw

   1  // Package edgegrid provides Akamai .edgerc configuration parsing and http.Request signing.
   2  package edgegrid
   3  
   4  import (
   5  	"errors"
   6  	"fmt"
   7  	"os"
   8  	"strconv"
   9  	"strings"
  10  	"time"
  11  
  12  	"github.com/mitchellh/go-homedir"
  13  	"gopkg.in/ini.v1"
  14  )
  15  
  16  const (
  17  	// DefaultConfigFile is the default configuration file path
  18  	DefaultConfigFile = "~/.edgerc"
  19  
  20  	// DefaultSection is the .edgerc ini default section
  21  	DefaultSection = "default"
  22  
  23  	// MaxBodySize is the max payload size for client requests
  24  	MaxBodySize = 131072
  25  )
  26  
  27  var (
  28  	// ErrRequiredOptionEnv is returned when a required ENV variable is not found
  29  	ErrRequiredOptionEnv = errors.New("required option is missing from env")
  30  	// ErrRequiredOptionEdgerc is returned when a required value is not found in edgerc file
  31  	ErrRequiredOptionEdgerc = errors.New("required option is missing from edgerc")
  32  	// ErrLoadingFile indicates problem with loading configuration file
  33  	ErrLoadingFile = errors.New("loading config file")
  34  	// ErrSectionDoesNotExist is returned when a section with provided name does not exist in edgerc
  35  	ErrSectionDoesNotExist = errors.New("provided config section does not exist")
  36  	// ErrHostContainsSlashAtTheEnd is returned when host has unnecessary '/' at the end
  37  	ErrHostContainsSlashAtTheEnd = errors.New("host must not contain '/' at the end")
  38  )
  39  
  40  type (
  41  	// Config struct provides all the necessary fields to
  42  	// create authorization header, debug is optional
  43  	Config struct {
  44  		Host         string   `ini:"host"`
  45  		ClientToken  string   `ini:"client_token"`
  46  		ClientSecret string   `ini:"client_secret"`
  47  		AccessToken  string   `ini:"access_token"`
  48  		AccountKey   string   `ini:"account_key"`
  49  		HeaderToSign []string `ini:"headers_to_sign"`
  50  		MaxBody      int      `ini:"max_body"`
  51  		RequestLimit int      `ini:"request_limit"`
  52  		Debug        bool     `ini:"debug"`
  53  
  54  		file    string
  55  		section string
  56  		env     bool
  57  	}
  58  
  59  	// Option defines a configuration option
  60  	Option func(*Config)
  61  )
  62  
  63  // New returns new configuration with the specified options
  64  func New(opts ...Option) (*Config, error) {
  65  	c := &Config{
  66  		section: DefaultSection,
  67  		env:     false,
  68  	}
  69  
  70  	for _, opt := range opts {
  71  		opt(c)
  72  	}
  73  
  74  	if c.env {
  75  		if err := c.FromEnv(c.section); err == nil {
  76  			return c, nil
  77  		} else if !errors.Is(err, ErrRequiredOptionEnv) {
  78  			return nil, err
  79  		}
  80  	}
  81  
  82  	if c.file != "" {
  83  		if err := c.FromFile(c.file, c.section); err != nil {
  84  			return c, fmt.Errorf("unable to load config from environment or .edgerc file: %w", err)
  85  		}
  86  	}
  87  
  88  	return c, nil
  89  }
  90  
  91  // Must will panic if the new method returns an error
  92  func Must(config *Config, err error) *Config {
  93  	if err != nil {
  94  		panic(err)
  95  	}
  96  	return config
  97  }
  98  
  99  // WithFile sets the config file path
 100  func WithFile(file string) Option {
 101  	return func(c *Config) {
 102  		c.file = file
 103  	}
 104  }
 105  
 106  // WithSection sets the section in the config
 107  func WithSection(section string) Option {
 108  	return func(c *Config) {
 109  		c.section = section
 110  	}
 111  }
 112  
 113  // WithEnv sets the option to try to the environment vars to populate the config
 114  // If loading from the env fails, will fallback to .edgerc
 115  func WithEnv(env bool) Option {
 116  	return func(c *Config) {
 117  		c.env = env
 118  	}
 119  }
 120  
 121  // FromFile creates a config the configuration in standard INI format
 122  func (c *Config) FromFile(file string, section string) error {
 123  	var (
 124  		requiredOptions = []string{"host", "client_token", "client_secret", "access_token"}
 125  	)
 126  
 127  	path, err := homedir.Expand(file)
 128  	if err != nil {
 129  		return fmt.Errorf("invalid path: %w", err)
 130  	}
 131  
 132  	edgerc, err := ini.Load(path)
 133  	if err != nil {
 134  		return fmt.Errorf("%w: %s", ErrLoadingFile, err)
 135  	}
 136  
 137  	sec, err := edgerc.GetSection(section)
 138  	if err != nil {
 139  		return fmt.Errorf("%w: %s", ErrSectionDoesNotExist, err)
 140  	}
 141  
 142  	err = sec.MapTo(&c)
 143  	if err != nil {
 144  		return err
 145  	}
 146  
 147  	for _, opt := range requiredOptions {
 148  		if !(edgerc.Section(section).HasKey(opt)) {
 149  			return fmt.Errorf("%w: %q", ErrRequiredOptionEdgerc, opt)
 150  		}
 151  	}
 152  
 153  	if c.MaxBody == 0 {
 154  		c.MaxBody = MaxBodySize
 155  	}
 156  
 157  	return nil
 158  }
 159  
 160  // FromEnv creates a new config using the Environment (ENV)
 161  //
 162  // By default, it uses AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET,
 163  // AKAMAI_ACCESS_TOKEN and AKAMAI_MAX_BODY variables.
 164  //
 165  // You can define multiple configurations by prefixing with the section name specified, e.g.
 166  // passing "ccu" will cause it to look for AKAMAI_CCU_HOST, etc.
 167  //
 168  // If AKAMAI_{SECTION} does not exist, it will fall back to just AKAMAI_.
 169  func (c *Config) FromEnv(section string) error {
 170  	var (
 171  		requiredOptions = []string{"HOST", "CLIENT_TOKEN", "CLIENT_SECRET", "ACCESS_TOKEN"}
 172  		prefix          string
 173  	)
 174  
 175  	prefix = "AKAMAI"
 176  
 177  	if section != DefaultSection {
 178  		prefix = "AKAMAI_" + strings.ToUpper(section)
 179  	}
 180  
 181  	for _, opt := range requiredOptions {
 182  		optKey := fmt.Sprintf("%s_%s", prefix, opt)
 183  
 184  		val, ok := os.LookupEnv(optKey)
 185  		if !ok {
 186  			return fmt.Errorf("%w: %q", ErrRequiredOptionEnv, optKey)
 187  		}
 188  		switch {
 189  		case opt == "HOST":
 190  			c.Host = val
 191  		case opt == "CLIENT_TOKEN":
 192  			c.ClientToken = val
 193  		case opt == "CLIENT_SECRET":
 194  			c.ClientSecret = val
 195  		case opt == "ACCESS_TOKEN":
 196  			c.AccessToken = val
 197  		}
 198  	}
 199  
 200  	val := os.Getenv(fmt.Sprintf("%s_%s", prefix, "MAX_BODY"))
 201  	if i, err := strconv.Atoi(val); err == nil {
 202  		c.MaxBody = i
 203  	}
 204  
 205  	if c.MaxBody <= 0 {
 206  		c.MaxBody = MaxBodySize
 207  	}
 208  
 209  	val, ok := os.LookupEnv(fmt.Sprintf("%s_%s", prefix, "ACCOUNT_KEY"))
 210  	if ok {
 211  		c.AccountKey = val
 212  	}
 213  
 214  	return nil
 215  }
 216  
 217  // Timestamp returns an edgegrid timestamp from the time
 218  func Timestamp(t time.Time) string {
 219  	local := time.FixedZone("GMT", 0)
 220  	t = t.In(local)
 221  	return t.Format("20060102T15:04:05-0700")
 222  }
 223  
 224  // Validate verifies that the host is not ending with the slash character
 225  func (c *Config) Validate() error {
 226  	if strings.HasSuffix(c.Host, "/") {
 227  		return fmt.Errorf("%w: %q", ErrHostContainsSlashAtTheEnd, c.Host)
 228  	}
 229  	return nil
 230  }
 231