Skip to content

Commit e0690ed

Browse files
authored
Merge pull request #6 from bugout-dev/parameters-go
Go version of parameters script
2 parents 7337591 + f24785f commit e0690ed

10 files changed

Lines changed: 425 additions & 7 deletions

File tree

README.md

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,11 @@ except they explicitly `export` the environment variables. We source these envir
2020
run the development versions of our applications.
2121

2222
It can sometimes be difficult to understand:
23-
1. Whether all the environment variables we *expect* to be defined in production actually have been.
23+
24+
1. Whether all the environment variables we _expect_ to be defined in production actually have been.
2425
2. What the particular value of a production environment actually is.
2526
3. What the differences are between our expectations and the actual environment variables in a running
26-
application process.
27+
application process.
2728

2829
We are building and maintaining `checkenv` to make it easier for us to diagnose and fix issues with
2930
application configuration via environment variables. We stand in solidarity with anyone else who
@@ -40,3 +41,24 @@ binary which supports your needs.
4041

4142
There is currently no need to support runtime plugins. Since doing so would make this program a lot
4243
more complicated, we have decided to forego runtime plugin functionality for now.
44+
45+
## Usage
46+
47+
```bash
48+
./checkenv plugins
49+
```
50+
51+
Available plugins:
52+
53+
- env - Provides the environment variables defined in the checkenv process.
54+
- file - Provides the environment variables defined in the env file with the given path.
55+
- proc - Provides the environment variables set for the process with the given pid.
56+
- aws_ssm - Provides environment variables defined in AWS Systems Manager Parameter Store.
57+
58+
### aws_ssm plugin
59+
60+
In order to fetch parameters with tags `Product` = `test` and `Node` = `true` with `export ` prefix execute following command
61+
62+
```bash
63+
./checkenv show -export aws_ssm+Product:test,Node:true
64+
```

aws_ssm.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
// checkenv plugin that provides environment variables defined in AWS System Manager Parameter Store.
2+
3+
package main
4+
5+
import (
6+
"context"
7+
8+
"github.com/bugout-dev/checkenv/aws_ssm"
9+
)
10+
11+
func AWSSystemsManagerParameterStoreProvider(filter string) (map[string]string, error) {
12+
environment := make(map[string]string)
13+
14+
// Convert string of tags for filter to key:value structure
15+
filterTags := aws_ssm.ParseFilterTags(filter)
16+
17+
ctx := context.Background()
18+
19+
api := aws_ssm.InitAWSClient(ctx)
20+
21+
keys := aws_ssm.FetchKeysOfParameters(ctx, api, filterTags)
22+
23+
// Split slice of parameter keys to chunks by 10 (max len allowed by AWS)
24+
// and fetch values for required parameters
25+
keyChunks := aws_ssm.GenerateChunks(keys, 10)
26+
parameters := aws_ssm.FetchParameters(ctx, api, keyChunks)
27+
28+
for _, parameter := range parameters {
29+
environment[parameter.Name] = parameter.Value
30+
}
31+
32+
return environment, nil
33+
}
34+
35+
func init() {
36+
helpString := "Provides environment variables defined in AWS Systems Manager Parameter Store."
37+
RegisterPlugin("aws_ssm", helpString, noop, AWSSystemsManagerParameterStoreProvider)
38+
}

aws_ssm/aws_ssm.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
/*
2+
Based on: https://github.com/awsdocs/aws-doc-sdk-examples/blob/main/gov2/ssm/GetParameter/GetParameterv2.go
3+
*/
4+
package aws_ssm
5+
6+
import (
7+
"context"
8+
"log"
9+
10+
"github.com/aws/aws-sdk-go-v2/config"
11+
"github.com/aws/aws-sdk-go-v2/service/ssm"
12+
)
13+
14+
// AWSSystemsManagerParameterStoreAPI defines the interface
15+
// for the GetParameters and DescribeParameters function.
16+
// We use this interface to test the function using a mocked service
17+
type AWSSystemsManagerParameterStoreAPI interface {
18+
GetParameters(
19+
ctx context.Context,
20+
params *ssm.GetParametersInput,
21+
optFns ...func(*ssm.Options),
22+
) (*ssm.GetParametersOutput, error)
23+
24+
DescribeParameters(
25+
ctx context.Context,
26+
params *ssm.DescribeParametersInput,
27+
optFns ...func(*ssm.Options),
28+
) (*ssm.DescribeParametersOutput, error)
29+
}
30+
31+
// ExecGetParameters and ExecDescribeParameters retrieves an AWS Systems Manager string parameter
32+
// Inputs:
33+
// c: is the context of the method call, which includes the AWS Region
34+
// api: is the interface that defines the method call
35+
// input: defines the input arguments to the service call
36+
// Output:
37+
// If success, a GetParametersOutput object containing the result of the service call and nil
38+
// Otherwise, nil and an error from the call to GetParameters
39+
func ExecGetParameters(c context.Context, api AWSSystemsManagerParameterStoreAPI, input *ssm.GetParametersInput) (*ssm.GetParametersOutput, error) {
40+
return api.GetParameters(c, input)
41+
}
42+
43+
func ExecDescribeParameters(c context.Context, api AWSSystemsManagerParameterStoreAPI, input *ssm.DescribeParametersInput) (*ssm.DescribeParametersOutput, error) {
44+
return api.DescribeParameters(c, input)
45+
}
46+
47+
// Load the Shared AWS Configuration (~/.aws/config)
48+
func InitAWSClient(ctx context.Context) *ssm.Client {
49+
cfg, err := config.LoadDefaultConfig(ctx)
50+
if err != nil {
51+
log.Fatalln("Failed loading AWS Configuration", err)
52+
}
53+
client := ssm.NewFromConfig(cfg)
54+
55+
return client
56+
}

aws_ssm/data.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package aws_ssm
2+
3+
// Parameter structure for storing final result from AWS SSM
4+
type Parameter struct {
5+
Name string
6+
Value string
7+
}
8+
9+
// Tags for filter defined by user
10+
type FilterTag struct {
11+
Name string
12+
Value string
13+
}

aws_ssm/data_test.json

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
[
2+
{
3+
"Name": "/wrong/dev/y1",
4+
"Value": "w1",
5+
"Tags": [
6+
{
7+
"Product": "wrong"
8+
}
9+
]
10+
},
11+
{
12+
"Name": "/test/dev/t1",
13+
"Value": "q1",
14+
"Tags": [
15+
{
16+
"Product": "test"
17+
}
18+
]
19+
},
20+
{
21+
"Name": "/test/dev/t2",
22+
"Value": "q2",
23+
"Tags": [
24+
{
25+
"Product": "test",
26+
"Application": "dev"
27+
}
28+
]
29+
},
30+
{
31+
"Name": "/test/dev/t3",
32+
"Value": "q3",
33+
"Tags": []
34+
},
35+
{
36+
"Name": "/test/dev/t4",
37+
"Value": "q4",
38+
"Tags": [
39+
{
40+
"Product": "wrong"
41+
}
42+
]
43+
}
44+
]

aws_ssm/parameters.go

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package aws_ssm
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"log"
7+
"os"
8+
"strconv"
9+
"strings"
10+
11+
"github.com/aws/aws-sdk-go-v2/service/ssm"
12+
"github.com/aws/aws-sdk-go-v2/service/ssm/types"
13+
)
14+
15+
// Fetch values for parameters
16+
// Inputs:
17+
// chunks: list of lists with parameter key values
18+
func FetchParameters(ctx context.Context, api AWSSystemsManagerParameterStoreAPI, chunks [][]string) []Parameter {
19+
var parameters []Parameter
20+
21+
for _, chunk := range chunks {
22+
getInput := &ssm.GetParametersInput{
23+
Names: chunk,
24+
}
25+
results, err := ExecGetParameters(ctx, api, getInput)
26+
if err != nil {
27+
log.Fatal(err)
28+
}
29+
30+
for _, p := range results.Parameters {
31+
parameter := Parameter{
32+
Name: *p.Name, Value: *p.Value,
33+
}
34+
parameters = append(parameters, parameter)
35+
}
36+
}
37+
log.Println("Retrieved values for parameters")
38+
39+
return parameters
40+
}
41+
42+
// Fetch list of parameter keys from AWS with defined filters
43+
func FetchKeysOfParameters(
44+
ctx context.Context,
45+
api AWSSystemsManagerParameterStoreAPI,
46+
filterTags []FilterTag,
47+
) []string {
48+
var parameters []string
49+
50+
// Set parameter filters
51+
parameterFilters := []types.ParameterStringFilter{}
52+
for _, ft := range filterTags {
53+
filterKey := fmt.Sprintf("tag:%s", ft.Name)
54+
parameterFilters = append(parameterFilters, types.ParameterStringFilter{
55+
Key: &filterKey,
56+
Values: []string{ft.Value},
57+
})
58+
}
59+
describeInput := &ssm.DescribeParametersInput{
60+
MaxResults: 10,
61+
ParameterFilters: parameterFilters,
62+
}
63+
64+
// CHECKENV_AWS_FETCH_LOOP_LIMIT by default set to 10,
65+
// it is allows to load 100 parameters from AWS and it is
66+
// a limiter to prevent loading too many parameters without
67+
// control during passing erroneous filters
68+
var err error
69+
var fetchLoopLimit int
70+
fetchLoopLimitStr := os.Getenv("CHECKENV_AWS_FETCH_LOOP_LIMIT")
71+
if fetchLoopLimitStr != "" {
72+
fetchLoopLimit, err = strconv.Atoi(fetchLoopLimitStr)
73+
}
74+
if fetchLoopLimitStr == "" || err != nil {
75+
fetchLoopLimit = 10
76+
}
77+
78+
n := 0
79+
for {
80+
// Fetch list of parameter keys
81+
results, err := ExecDescribeParameters(ctx, api, describeInput)
82+
if err != nil {
83+
log.Fatal(err)
84+
}
85+
for _, p := range results.Parameters {
86+
parameters = append(parameters, *p.Name)
87+
}
88+
89+
// If there are no more parameters break
90+
if results.NextToken == nil {
91+
break
92+
}
93+
describeInput.NextToken = results.NextToken
94+
95+
n++
96+
if n >= fetchLoopLimit {
97+
log.Fatal("To many iterations over DescribeParameters loop")
98+
}
99+
}
100+
log.Printf("Retrieved %d parameters", len(parameters))
101+
102+
return parameters
103+
}
104+
105+
// Split list of reports on nested lists
106+
func GenerateChunks(flatSlice []string, chunkSize int) [][]string {
107+
if len(flatSlice) == 0 {
108+
return [][]string{}
109+
}
110+
111+
chunks := make([][]string, 0, len(flatSlice)/chunkSize+1)
112+
113+
for i, v := range flatSlice {
114+
if i%chunkSize == 0 {
115+
chunks = append(chunks, make([]string, 0, chunkSize))
116+
}
117+
chunks[len(chunks)-1] = append(chunks[len(chunks)-1], v)
118+
}
119+
120+
return chunks
121+
}
122+
123+
// ParseFilterTags convert string from user input to key value structure
124+
func ParseFilterTags(filterTagsStr string) []FilterTag {
125+
var filterTags []FilterTag
126+
127+
filterTagsSlice := strings.Split(filterTagsStr, ",")
128+
for _, t := range filterTagsSlice {
129+
tagNameValue := strings.Split(t, ":")
130+
if len(tagNameValue) != 2 || len(tagNameValue[0]) == 0 || len(tagNameValue[1]) == 0 {
131+
log.Printf("Unable to parse tag name and value: %s", t)
132+
continue
133+
}
134+
filterTags = append(filterTags, FilterTag{
135+
Name: tagNameValue[0],
136+
Value: tagNameValue[1],
137+
})
138+
}
139+
140+
return filterTags
141+
}

aws_ssm/parameters_test.go

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
package aws_ssm
2+
3+
import (
4+
"reflect"
5+
"testing"
6+
)
7+
8+
func TestGenerateChunks(t *testing.T) {
9+
var cases = []struct {
10+
flatSlice []string
11+
chunkSIze int
12+
expected [][]string
13+
}{
14+
{[]string{}, 1, [][]string{}},
15+
{[]string{}, 2, [][]string{}},
16+
{[]string{"val-1", "val-2"}, 2, [][]string{{"val-1", "val-2"}}},
17+
{[]string{"val-1", "val-2", "val-3", "val-4", "val-5"}, 1, [][]string{{"val-1"}, {"val-2"}, {"val-3"}, {"val-4"}, {"val-5"}}},
18+
{[]string{"val-1", "val-2", "val-3", "val-4", "val-5"}, 2, [][]string{{"val-1", "val-2"}, {"val-3", "val-4"}, {"val-5"}}},
19+
{[]string{"val-1", "val-2", "val-3", "val-4", "val-5", "val-6"}, 3, [][]string{{"val-1", "val-2", "val-3"}, {"val-4", "val-5", "val-6"}}},
20+
}
21+
for _, c := range cases {
22+
chunks := GenerateChunks(c.flatSlice, c.chunkSIze)
23+
if !reflect.DeepEqual(chunks, c.expected) {
24+
t.Logf("Value should be %s, but got %s", c.expected, chunks)
25+
t.Fail()
26+
}
27+
}
28+
}
29+
30+
func TestFilterTags(t *testing.T) {
31+
var emptyFilterTags []FilterTag
32+
var cases = []struct {
33+
filterTagsStr string
34+
expected []FilterTag
35+
}{
36+
{"Product", emptyFilterTags},
37+
{"Product:", emptyFilterTags},
38+
{":test", emptyFilterTags},
39+
{":", emptyFilterTags},
40+
{"Product:test", []FilterTag{{Name: "Product", Value: "test"}}},
41+
{"Product:test,Node:true", []FilterTag{{Name: "Product", Value: "test"}, {Name: "Node", Value: "true"}}},
42+
}
43+
for _, c := range cases {
44+
filterTags := ParseFilterTags(c.filterTagsStr)
45+
if !reflect.DeepEqual(filterTags, c.expected) {
46+
t.Logf("Value should be %s, but got %s", c.expected, filterTags)
47+
t.Fatal()
48+
}
49+
}
50+
}

0 commit comments

Comments
 (0)