-
Notifications
You must be signed in to change notification settings - Fork 78
/
kmsconfig.go
275 lines (241 loc) · 7.58 KB
/
kmsconfig.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
// Package kmsconfig configures KMS tests.
package kmsconfig
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"time"
"github.com/aws/aws-k8s-tester/pkg/awsapi"
"github.com/aws/aws-k8s-tester/pkg/logutil"
"sigs.k8s.io/yaml"
)
// Config defines EKS test configuration.
type Config struct {
// ID is the ID used for KMS resource creation.
ID string `json:"id,omitempty"`
// ConfigPath is the configuration file path.
// Deployer is expected to update this file with latest status.
ConfigPath string `json:"config-path,omitempty"`
// AWSAccountID is the AWS account ID.
AWSAccountID string `json:"aws-account-id,omitempty"`
// AWSRegion is the AWS geographic area for EKS deployment.
// If empty, set default region.
AWSRegion string `json:"aws-region,omitempty"`
// LogLevel configures log level. Only supports debug, info, warn, error, panic, or fatal. Default 'info'.
LogLevel string `json:"log-level"`
// LogOutputs is a list of log outputs. Valid values are 'default', 'stderr', 'stdout', or file names.
// Logs are appended to the existing file, if any.
// Multiple values are accepted. If empty, it sets to 'default', which outputs to stderr.
// See https://godoc.org/go.uber.org/zap#Open and https://godoc.org/go.uber.org/zap#Config for more details.
LogOutputs []string `json:"log-outputs,omitempty"`
// UpdatedAt is the timestamp when the configuration has been updated.
// Read only to 'Config' struct users.
UpdatedAt time.Time `json:"updated-at,omitempty"` // read-only to user
// KeyMetadata is the EKS metadata status.
KeyMetadata *KeyMetadata `json:"key-meta-data,omitempty"`
// KeyRotationEnabled is true, if key rotation is enabled.
KeyRotationEnabled bool `json:"key-rotation-enabled,omitempty"`
}
// KeyMetadata is the key's current metadata.
type KeyMetadata struct {
AWSAccountID string `json:"aws-account-id"`
ARN string `json:"arn"`
CreationDate time.Time `json:"creation-date"`
Description string `json:"description"`
Enabled bool `json:"enabled"`
KeyID string `json:"key-id"`
KeyManager string `json:"key-manager"`
KeyState string `json:"key-state"`
KeyUsage string `json:"key-usage"`
Origin string `json:"origin"`
}
// NewDefault returns a copy of the default configuration.
func NewDefault() *Config {
vv := defaultConfig
return &vv
}
// defaultConfig is the default configuration.
// - empty string creates a non-nil object for pointer-type field
// - omitting an entire field returns nil value
// - make sure to check both
var defaultConfig = Config{
AWSAccountID: "",
AWSRegion: "us-west-2",
LogLevel: logutil.DefaultLogLevel,
// default, stderr, stdout, or file name
// log file named with cluster name will be added automatically
LogOutputs: []string{"stderr"},
KeyMetadata: &KeyMetadata{},
}
// Load loads configuration from YAML.
// Useful when injecting shared configuration via ConfigMap.
//
// Example usage:
//
// import "github.com/aws/aws-k8s-tester/kmsconfig"
// cfg := kmsconfig.Load("test.yaml")
// err := cfg.ValidateAndSetDefaults()
//
// Do not set default values in this function.
// "ValidateAndSetDefaults" must be called separately,
// to prevent overwriting previous data when loaded from disks.
func Load(p string) (cfg *Config, err error) {
var d []byte
d, err = ioutil.ReadFile(p)
if err != nil {
return nil, err
}
cfg = new(Config)
if err = yaml.Unmarshal(d, cfg); err != nil {
return nil, err
}
if cfg.KeyMetadata == nil {
cfg.KeyMetadata = &KeyMetadata{}
}
if cfg.ConfigPath != p {
cfg.ConfigPath = p
}
cfg.ConfigPath, err = filepath.Abs(p)
if err != nil {
return nil, err
}
return cfg, nil
}
// Sync persists current configuration and states to disk.
func (cfg *Config) Sync() (err error) {
if !filepath.IsAbs(cfg.ConfigPath) {
cfg.ConfigPath, err = filepath.Abs(cfg.ConfigPath)
if err != nil {
return err
}
}
cfg.UpdatedAt = time.Now().UTC()
var d []byte
d, err = yaml.Marshal(cfg)
if err != nil {
return err
}
return ioutil.WriteFile(cfg.ConfigPath, d, 0600)
}
// genTag generates a tag for cluster name, CloudFormation, and S3 bucket.
// Note that this would be used as S3 bucket name to upload tester logs.
func genTag() string {
// use UTC time for everything
now := time.Now().UTC()
return fmt.Sprintf("kms-%d%02d%02d%02d", now.Year(), int(now.Month()), now.Day(), now.Hour())
}
// ValidateAndSetDefaults returns an error for invalid configurations.
// And updates empty fields with default values.
// At the end, it writes populated YAML to aws-k8s-tester config path.
func (cfg *Config) ValidateAndSetDefaults() error {
if len(cfg.LogOutputs) == 0 {
return errors.New("LogOutputs is not empty")
}
if cfg.AWSRegion == "" {
return errors.New("AWSRegion is empty")
}
if _, ok := awsapi.RegionToAiport[cfg.AWSRegion]; !ok {
return fmt.Errorf("%q not found", cfg.AWSRegion)
}
if cfg.ID == "" {
region := cfg.AWSRegion
airport := awsapi.RegionToAiport[region]
cfg.ID = genTag() + "-" + strings.ToLower(airport) + "-" + region + "-" + randString(5)
}
if cfg.KeyMetadata == nil {
cfg.KeyMetadata = &KeyMetadata{}
}
if cfg.ConfigPath == "" {
f, err := ioutil.TempFile(os.TempDir(), "kms")
if err != nil {
return err
}
cfg.ConfigPath, _ = filepath.Abs(f.Name())
f.Close()
os.RemoveAll(cfg.ConfigPath)
}
return cfg.Sync()
}
const envPfx = "AWS_K8S_TESTER_KMS_"
// UpdateFromEnvs updates fields from environmental variables.
func (cfg *Config) UpdateFromEnvs() error {
cc := *cfg
tp, vv := reflect.TypeOf(&cc).Elem(), reflect.ValueOf(&cc).Elem()
for i := 0; i < tp.NumField(); i++ {
jv := tp.Field(i).Tag.Get("json")
if jv == "" {
continue
}
jv = strings.Replace(jv, ",omitempty", "", -1)
jv = strings.Replace(jv, "-", "_", -1)
jv = strings.ToUpper(strings.Replace(jv, "-", "_", -1))
env := envPfx + jv
if os.Getenv(env) == "" {
continue
}
sv := os.Getenv(env)
fieldName := tp.Field(i).Name
switch vv.Field(i).Type().Kind() {
case reflect.String:
vv.Field(i).SetString(sv)
case reflect.Bool:
bb, err := strconv.ParseBool(sv)
if err != nil {
return fmt.Errorf("failed to parse %q (%q, %v)", sv, env, err)
}
vv.Field(i).SetBool(bb)
case reflect.Int, reflect.Int32, reflect.Int64:
if fieldName == "DestroyWaitTime" {
dv, err := time.ParseDuration(sv)
if err != nil {
return fmt.Errorf("failed to parse %q (%q, %v)", sv, env, err)
}
vv.Field(i).SetInt(int64(dv))
continue
}
iv, err := strconv.ParseInt(sv, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse %q (%q, %v)", sv, env, err)
}
vv.Field(i).SetInt(iv)
case reflect.Uint, reflect.Uint32, reflect.Uint64:
iv, err := strconv.ParseUint(sv, 10, 64)
if err != nil {
return fmt.Errorf("failed to parse %q (%q, %v)", sv, env, err)
}
vv.Field(i).SetUint(iv)
case reflect.Float32, reflect.Float64:
fv, err := strconv.ParseFloat(sv, 64)
if err != nil {
return fmt.Errorf("failed to parse %q (%q, %v)", sv, env, err)
}
vv.Field(i).SetFloat(fv)
case reflect.Slice:
ss := strings.Split(sv, ",")
slice := reflect.MakeSlice(reflect.TypeOf([]string{}), len(ss), len(ss))
for i := range ss {
slice.Index(i).SetString(ss[i])
}
vv.Field(i).Set(slice)
default:
return fmt.Errorf("%q (%v) is not supported as an env", env, vv.Field(i).Type())
}
}
*cfg = cc
return nil
}
const ll = "0123456789abcdefghijklmnopqrstuvwxyz"
func randString(n int) string {
b := make([]byte, n)
for i := range b {
rand.Seed(time.Now().UTC().UnixNano())
b[i] = ll[rand.Intn(len(ll))]
}
return string(b)
}