github.com/akamai/AkamaiOPEN-edgegrid-golang/v4@v4.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  		Debug        bool     `ini:"debug"`
    52  
    53  		file    string
    54  		section string
    55  		env     bool
    56  	}
    57  
    58  	// Option defines a configuration option
    59  	Option func(*Config)
    60  )
    61  
    62  // New returns new configuration with the specified options
    63  func New(opts ...Option) (*Config, error) {
    64  	c := &Config{
    65  		section: DefaultSection,
    66  		env:     false,
    67  	}
    68  
    69  	for _, opt := range opts {
    70  		opt(c)
    71  	}
    72  
    73  	if c.env {
    74  		if err := c.FromEnv(c.section); err == nil {
    75  			return c, nil
    76  		} else if !errors.Is(err, ErrRequiredOptionEnv) {
    77  			return nil, err
    78  		}
    79  	}
    80  
    81  	if c.file != "" {
    82  		if err := c.FromFile(c.file, c.section); err != nil {
    83  			return c, fmt.Errorf("unable to load config from environment or .edgerc file: %w", err)
    84  		}
    85  	}
    86  
    87  	return c, nil
    88  }
    89  
    90  // Must will panic if the new method returns an error
    91  func Must(config *Config, err error) *Config {
    92  	if err != nil {
    93  		panic(err)
    94  	}
    95  	return config
    96  }
    97  
    98  // WithFile sets the config file path
    99  func WithFile(file string) Option {
   100  	return func(c *Config) {
   101  		c.file = file
   102  	}
   103  }
   104  
   105  // WithSection sets the section in the config
   106  func WithSection(section string) Option {
   107  	return func(c *Config) {
   108  		c.section = section
   109  	}
   110  }
   111  
   112  // WithEnv sets the option to try to the environment vars to populate the config
   113  // If loading from the env fails, will fallback to .edgerc
   114  func WithEnv(env bool) Option {
   115  	return func(c *Config) {
   116  		c.env = env
   117  	}
   118  }
   119  
   120  // FromFile creates a config the configuration in standard INI format
   121  func (c *Config) FromFile(file string, section string) error {
   122  	var (
   123  		requiredOptions = []string{"host", "client_token", "client_secret", "access_token"}
   124  	)
   125  
   126  	path, err := homedir.Expand(file)
   127  	if err != nil {
   128  		return fmt.Errorf("invalid path: %w", err)
   129  	}
   130  
   131  	edgerc, err := ini.Load(path)
   132  	if err != nil {
   133  		return fmt.Errorf("%w: %s", ErrLoadingFile, err)
   134  	}
   135  
   136  	sec, err := edgerc.GetSection(section)
   137  	if err != nil {
   138  		return fmt.Errorf("%w: %s", ErrSectionDoesNotExist, err)
   139  	}
   140  
   141  	err = sec.MapTo(&c)
   142  	if err != nil {
   143  		return err
   144  	}
   145  
   146  	for _, opt := range requiredOptions {
   147  		if !(edgerc.Section(section).HasKey(opt)) {
   148  			return fmt.Errorf("%w: %q", ErrRequiredOptionEdgerc, opt)
   149  		}
   150  	}
   151  
   152  	if c.MaxBody == 0 {
   153  		c.MaxBody = MaxBodySize
   154  	}
   155  
   156  	return nil
   157  }
   158  
   159  // FromEnv creates a new config using the Environment (ENV)
   160  //
   161  // By default, it uses AKAMAI_HOST, AKAMAI_CLIENT_TOKEN, AKAMAI_CLIENT_SECRET,
   162  // AKAMAI_ACCESS_TOKEN, and AKAMAI_MAX_BODY variables.
   163  //
   164  // You can define multiple configurations by prefixing with the section name specified, e.g.
   165  // passing "ccu" will cause it to look for AKAMAI_CCU_HOST, etc.
   166  //
   167  // If AKAMAI_{SECTION} does not exist, it will fall back to just AKAMAI_.
   168  func (c *Config) FromEnv(section string) error {
   169  	var (
   170  		requiredOptions = []string{"HOST", "CLIENT_TOKEN", "CLIENT_SECRET", "ACCESS_TOKEN"}
   171  		prefix          string
   172  	)
   173  
   174  	prefix = "AKAMAI"
   175  
   176  	if section != DefaultSection {
   177  		prefix = "AKAMAI_" + strings.ToUpper(section)
   178  	}
   179  
   180  	for _, opt := range requiredOptions {
   181  		optKey := fmt.Sprintf("%s_%s", prefix, opt)
   182  
   183  		val, ok := os.LookupEnv(optKey)
   184  		if !ok {
   185  			return fmt.Errorf("%w: %q", ErrRequiredOptionEnv, optKey)
   186  		}
   187  		switch {
   188  		case opt == "HOST":
   189  			c.Host = val
   190  		case opt == "CLIENT_TOKEN":
   191  			c.ClientToken = val
   192  		case opt == "CLIENT_SECRET":
   193  			c.ClientSecret = val
   194  		case opt == "ACCESS_TOKEN":
   195  			c.AccessToken = val
   196  		}
   197  	}
   198  
   199  	c.MaxBody = 0
   200  
   201  	val := os.Getenv(fmt.Sprintf("%s_%s", prefix, "MAX_BODY"))
   202  	if i, err := strconv.Atoi(val); err == nil {
   203  		c.MaxBody = i
   204  	}
   205  
   206  	if c.MaxBody <= 0 {
   207  		c.MaxBody = MaxBodySize
   208  	}
   209  
   210  	val, ok := os.LookupEnv(fmt.Sprintf("%s_%s", prefix, "ACCOUNT_KEY"))
   211  	if ok {
   212  		c.AccountKey = val
   213  	}
   214  
   215  	return nil
   216  }
   217  
   218  // Timestamp returns an edgegrid timestamp from the time
   219  func Timestamp(t time.Time) string {
   220  	local := time.FixedZone("GMT", 0)
   221  	t = t.In(local)
   222  	return t.Format("20060102T15:04:05-0700")
   223  }
   224  
   225  // Validate verifies that the host is not ending with the slash character
   226  func (c *Config) Validate() error {
   227  	if strings.HasSuffix(c.Host, "/") {
   228  		return fmt.Errorf("%w: %q", ErrHostContainsSlashAtTheEnd, c.Host)
   229  	}
   230  	return nil
   231  }