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

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