github.com/akamai/AkamaiOPEN-edgegrid-golang/v8@v8.1.0/pkg/edgegrid/config.go (about)

     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  }