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