/
secretsmanager.go
216 lines (179 loc) · 6.1 KB
/
secretsmanager.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
// Package secretsmanager implements a koanf.Provider for AWS SecretsManager
// and provides it to koanf to be parsed by a koanf.Parser.
package secretsmanager
import (
"context"
"errors"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/credentials/stscreds"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/aws/aws-sdk-go-v2/service/sts"
"github.com/knadh/koanf/maps"
"github.com/knadh/koanf/parsers/json"
)
// Config holds the AWS SecretsManager Configuration.
type Config struct {
// The AWS SecretsManager Delim that might be used
// delim string
Delim string
// The SecretsManager name or arn to fetch
// name or the secret ID.
SecretId string
// The type of values secre value set, it can only be string or map.
// if the value is type of app, each key is unfallten to create new
// single var like: parent: {"child": "value"} -> parent.child = value
Type string
// The SecretsManager Configuration Version to fetch. Specifying a VersionId
// ensures that the configuration is only fetched if it is updated. If not specified,
// the latest available configuration is fetched always.
// Setting this to the latest configuration version will return an empty slice of bytes.
VersionId *string
// The AWS Access Key ID to use. This value is fetched from the environment
// if not specified.
AWSAccessKeyID string
// The AWS Secret Access Key to use. This value is fetched from the environment
// if not specified.
AWSSecretAccessKey string
// The AWS IAM Role ARN to use. Useful for access requiring IAM AssumeRole.
AWSRoleARN string
// The AWS Region to use. This value is fetched from teh environment if not specified.
AWSRegion string
// Time interval at which the watcher will refresh the configuration.
// Defaults to 3600 seconds.
WatchInterval time.Duration
}
// SMConfig implements an AWS SecretsManager provider.
type SMConfig struct {
client *secretsmanager.Client
config Config
input secretsmanager.GetSecretValueInput
cb func(s string) string
}
// Provider returns an AWS SecretsManager provider.
func Provider(cfg Config, cb func(s string) string) *SMConfig {
// load the default config
c, err := config.LoadDefaultConfig(context.TODO())
if err != nil {
return nil
}
// check inputs and set
if cfg.Delim == "" {
cfg.Delim = "_"
}
if cfg.AWSRegion != "" {
c.Region = cfg.AWSRegion
}
// Check if AWS Access Key ID and Secret Key are specified.
if cfg.AWSAccessKeyID != "" && cfg.AWSSecretAccessKey != "" {
c.Credentials = credentials.NewStaticCredentialsProvider(cfg.AWSAccessKeyID, cfg.AWSSecretAccessKey, "")
}
// Check if AWS Role ARN is present.
if cfg.AWSRoleARN != "" {
stsSvc := sts.NewFromConfig(c)
credentials := stscreds.NewAssumeRoleProvider(stsSvc, cfg.AWSRoleARN)
c.Credentials = aws.NewCredentialsCache(credentials)
}
client := secretsmanager.NewFromConfig(c)
return &SMConfig{client: client, config: cfg, cb: cb}
}
// ProviderWithClient returns an AWS SecretsManager provider
// using an existing AWS SecretsManager client.
func ProviderWithClient(cfg Config, cb func(s string) string, client *secretsmanager.Client) *SMConfig {
return &SMConfig{client: client, config: cfg, cb: cb}
}
// Read is not supported by the SecretsManager provider.
func (sm *SMConfig) Read() (map[string]interface{}, error) {
// check if secretId is provided
if sm.config.SecretId == "" {
return nil, errors.New("no secret id provided")
}
// set secretsmanger input
sm.input = secretsmanager.GetSecretValueInput{
SecretId: aws.String(sm.config.SecretId),
}
// check if latest version exist
if sm.config.VersionId != nil {
sm.input.VersionId = sm.config.VersionId
}
// get secret value
conf, err := sm.client.GetSecretValue(context.TODO(), &sm.input)
if err != nil {
return nil, err
}
mp := make(map[string]interface{})
// check if secret is set as string
if conf.SecretString != nil {
key := *conf.Name
// transform key id transformer provided
if sm.cb != nil {
key = sm.cb(key)
}
if key == "" {
return nil, errors.New("transformed key has become null")
}
// set key value
mp[key] = *conf.SecretString
}
// if value is set as map it will unfaltten
if sm.config.Type == "map" {
// create new instance of map
mp = make(map[string]interface{})
// parse secret value as map if type is set as map
valueMap, err := json.Parser().Unmarshal([]byte(*conf.SecretString))
if err != nil {
return nil, errors.New("unable to unmarshal value as obj")
}
// modify each value
for k, v := range valueMap {
updated_key := *conf.Name + sm.config.Delim + k
// transform key id transformer provided
if sm.cb != nil {
updated_key = sm.cb(updated_key)
}
// If the callback blanked the key, it should be omitted
if updated_key == "" {
return nil, errors.New("transformed key has become null")
}
mp[updated_key] = v
}
}
// Set the response configuration version as the current configuration version.
// Useful for Watch().
sm.config.VersionId = conf.VersionId
return maps.Unflatten(mp, sm.config.Delim), nil
}
// ReadBytes returns the raw bytes for parsing.
func (sm *SMConfig) ReadBytes() ([]byte, error) {
// shoud implement for SecretBinary. maybe in future
return nil, errors.New("secretsmanager provider does not support this method")
}
// Watch polls AWS AppConfig for configuration updates.
func (sm *SMConfig) Watch(cb func(event interface{}, err error)) error {
if sm.config.WatchInterval == 0 {
// Set default watch interval to 3600 seconds. to reduce cost
sm.config.WatchInterval = 3600 * time.Second
}
go func() {
loop:
for {
conf, err := sm.client.GetSecretValue(context.TODO(), &sm.input)
if err != nil {
cb(nil, err)
break loop
}
// Check if the the configuration has been updated.
if *conf.VersionId == *sm.config.VersionId {
// Configuration is not updated and we have the latest version.
// Sleep for WatchInterval and retry watcher.
time.Sleep(sm.config.WatchInterval)
continue
}
// Trigger event.
cb(nil, nil)
}
}()
return nil
}