This repository has been archived by the owner on May 16, 2024. It is now read-only.
/
gconfig.go
384 lines (335 loc) · 10 KB
/
gconfig.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
package gconfig
import (
"context"
"encoding/json"
"errors"
"fmt"
)
// Config is the list of variables which a provider can be configured with.
type Config []*Field
type Dumper interface {
Dump(ctx context.Context, cfg Config) (map[string]string, error)
}
// Loader loads configuration for Granted providers.
type Loader interface {
// Load configuration. Returns a map of config values.
// The keys of the map are the value names, and the values
// are the actual values of the config.
// For example:
// {"orgUrl": "http://my-org.com"}
//
// Returns an error if loading the values fails.
//
// The Loader should internally handle sourcing the configuration for example from a map or environment variables
Load(ctx context.Context) (map[string]string, error)
}
// Load configuration using a Loader.
func (c Config) Load(ctx context.Context, l Loader) error {
loaded, err := l.Load(ctx)
if err != nil {
return err
}
for _, s := range c {
key := s.Key()
val, ok := loaded[key]
if !ok && !s.IsOptional() {
return fmt.Errorf("could not find %s in map", key)
} else if ok { // only set value if its found
err = s.setInitial(val)
if err != nil {
return err
}
}
}
return nil
}
// FindFieldByKey looks up a field by its key.
// If the field doesn't exist in the config, an error is returned.
func (c Config) FindFieldByKey(key string) (*Field, error) {
for _, field := range c {
if field.Key() == key {
return field, nil
}
}
return nil, fmt.Errorf("field with key %s not found", key)
}
// Dump renders a map[string]string where the values are mapped in different ways based on the provided dumper
//
// use SafeDumper to get all values with secrets redacted
//
// SSMDumper first pushes any updated secrets to ssm then returns the ssm paths to the secrets
func (c Config) Dump(ctx context.Context, dumper Dumper) (map[string]string, error) {
if dumper == nil {
return nil, fmt.Errorf("cannot dump with nil dumper")
}
return dumper.Dump(ctx, c)
}
type Valuer interface {
Set(s string)
Get() string
String() string
}
type SecretPathFunc func(args ...interface{}) (string, error)
// Use this if the path is a simple string
func WithNoArgs(path string) SecretPathFunc {
return WithArgs(path, 0)
}
// WithArgs returns a SecretPathFunc which is intended to be used when dynamic formatting of the path is required.
// For example a path refers to an id entered by a user, we only know this at dump time.
// The SSMDumper takes in args which are passed to the the format string
func WithArgs(path string, expectedCount int) SecretPathFunc {
return func(args ...interface{}) (string, error) {
if len(args) != expectedCount {
return "", IncorrectArgumentsToSecretPathFuncError{
ExpectedArgs: expectedCount,
FoundArgs: len(args),
Key: path,
}
}
return fmt.Sprintf(path, args...), nil
}
}
type cliPromptType uint8
const (
CLIPromptTypeString cliPromptType = iota
CLIPromptTypeFile
CLIPromptTypePassword
)
// Field represents a key-value pair in a configuration
// to create a Field, use one of the generator functions
// StringField(), SecretStringField() or OptionalStringField()
type Field struct {
key string
description string
value Valuer
secret bool
optional bool
// hasChanged is true if the Set() method has been called
hasChanged bool
// secretUpdated is true if the current value has been pushed to the secret backend e.g SSM
// This happens when Field.Dump(Dumper) is called with a secret dumper
secretUpdated bool
// secretPathFunc defines the path that this secret should be written to.
// it is a function that takes in args. for some usecases, an id will need to be inserted into the path dynamically
// For example, in aws ssm, this is the secret path
//
// func pathGen(args ...string)string {
// return fmt.Sprintf("granted/providers/secrets/%s/apiToken",args...)
// }
//
//
secretPathFunc SecretPathFunc
// When a secret is read from file with the aws ssm loader, the path will be set here.
// If this is a newly created secret, when it is put in ssm, the path is saved here.
// this value is typically derived from the secretPathPrefix a suffix and a version number
secretPath string
defaultFunc func() string
cliPrompt cliPromptType
}
func (s Field) HasChanged() bool {
return s.hasChanged
}
// Path returns the secret path
// secrets loaded from config with the SSM Loader will have an secret path relevant to the loader type
// secrets loaded from a test loader like JSONLoader or MapLoader will not have a path and this method will return an empty string
func (s Field) SecretPath() string {
return s.secretPath
}
// IsSecret returns true if this Field is a secret
func (s Field) IsSecret() bool {
return s.secret
}
// IsOptional returns true if this Field is optional
func (s Field) IsOptional() bool {
return s.optional
}
// Key returns the key for this field
func (s Field) Key() string {
return s.key
}
// Description returns the usage string for this field
func (s Field) Description() string {
return s.description
}
// Default returns the default value if available else and empty string
func (s Field) Default() string {
if s.defaultFunc != nil {
return s.defaultFunc()
}
return ""
}
// Set the value of this string
// The value is only update if it has changed and Field.HasChanged will only be updated if it has changed
func (s *Field) Set(v string) error {
if s.value == nil {
return errors.New("cannot call Set on nil Valuer")
}
// Only update the value if it has changed
// only update hasChanged if the value is updated
if s.value.Get() != v {
s.hasChanged = true
s.value.Set(v)
}
return nil
}
// setInitial sets the value of this string
// Does not set hasChanged to true, this method is used when loading a config for the first time
func (s *Field) setInitial(v string) error {
if s.value == nil {
return errors.New("cannot call Set on nil Valuer")
}
// Only update the value if it has changed
// only update hasChanged if the value is updated
if s.value.Get() != v {
s.value.Set(v)
}
return nil
}
// Get returns the value if it is set, or an empty string if it is not set
func (s *Field) Get() string {
if s.value == nil {
return ""
}
return s.value.Get()
}
func (s *Field) CLIPrompt() cliPromptType {
return s.cliPrompt
}
// String calls the Valuer.String() method for this fields value.
// If this field is a secret, then the response will be a redacted string.
// Use Field.Get() to retrieve the raw value for the field
func (s Field) String() string {
if s.value == nil {
return ""
}
return s.value.String()
}
// SecretStringValue value implements the Valuer interface, it should be used for secrets in configuration structs.
//
// It is configured to automatically redact the secret for common logging usecases like Zap, fmt.Println and json.Marshal
type SecretStringValue struct {
Value string
}
// Get the raw value of the secret
func (s *SecretStringValue) Get() string {
return s.Value
}
// Set the value of the secret
func (s *SecretStringValue) Set(value string) {
s.Value = value
}
// String returns a redacted value for this secret
func (s SecretStringValue) String() string {
return "*****"
}
// MarshalJSON returns a redacted value bytes for this secret
func (s SecretStringValue) MarshalJSON() ([]byte, error) {
return json.Marshal(s.String())
}
// StringValue value implements the Valuer interface
type StringValue struct {
Value string
}
// Get the value of the string
func (s *StringValue) Get() string {
return s.Value
}
// String calls StringValue.Get()
func (s StringValue) String() string {
return s.Get()
}
// Set the value of the string
func (s *StringValue) Set(value string) {
s.Value = value
}
// OptionalStringValue value implements the Valuer interface
type OptionalStringValue struct {
Value *string
}
// Get the value of the string
func (s *OptionalStringValue) Get() string {
if s.Value == nil {
return ""
}
return *s.Value
}
// Get the value of the string
func (s *OptionalStringValue) IsSet() bool {
return s.Value != nil
}
// String calls OptionalStringValue.Get()
func (s OptionalStringValue) String() string {
return s.Get()
}
// Set the value of the string
func (s *OptionalStringValue) Set(value string) {
s.Value = &value
}
type FieldOptFunc func(f *Field)
// WithDefaultFunc sets the default function for a field
// The default func can be used to initialise a new config
func WithDefaultFunc(df func() string) FieldOptFunc {
return func(f *Field) {
f.defaultFunc = df
}
}
// WithCLIPrompt allows to override the type of cli prompt used to collect a value for this field when used in the context of a CLI
func WithCLIPrompt(prompt cliPromptType) FieldOptFunc {
return func(f *Field) {
f.cliPrompt = prompt
}
}
// StringField creates a new field with a StringValue
// This field type is for non secrets
// for secrets, use SecretField()
func StringField(key string, dest *StringValue, usage string, opts ...FieldOptFunc) *Field {
if dest == nil {
panic(ErrFieldValueMustNotBeNil)
}
f := &Field{
key: key,
value: dest,
description: usage,
cliPrompt: CLIPromptTypeString,
}
for _, opt := range opts {
opt(f)
}
return f
}
// SecretStringField creates a new field with a SecretStringValue
func SecretStringField(key string, dest *SecretStringValue, usage string, secretPathFunc SecretPathFunc, opts ...FieldOptFunc) *Field {
if dest == nil {
panic(ErrFieldValueMustNotBeNil)
}
f := &Field{
key: key,
value: dest,
description: usage,
secret: true,
secretPathFunc: secretPathFunc,
cliPrompt: CLIPromptTypePassword,
}
for _, opt := range opts {
opt(f)
}
return f
}
// OptionalStringField creates a new optional field with an OptionalStringValue
// There is no OptionalSecret type.
func OptionalStringField(key string, dest *OptionalStringValue, usage string, opts ...FieldOptFunc) *Field {
if dest == nil {
panic(ErrFieldValueMustNotBeNil)
}
f := &Field{
key: key,
value: dest,
description: usage,
optional: true,
cliPrompt: CLIPromptTypeString,
}
for _, opt := range opts {
opt(f)
}
return f
}