Skip to content

Commit

Permalink
Add --json flag to config list command
Browse files Browse the repository at this point in the history
A big refactor and tests
  • Loading branch information
ipmb committed Mar 27, 2023
1 parent 213454b commit a329a24
Show file tree
Hide file tree
Showing 7 changed files with 366 additions and 90 deletions.
27 changes: 25 additions & 2 deletions app/app.go
Expand Up @@ -730,8 +730,31 @@ func (a *App) ConfigPrefix() string {
}

// GetConfig returns a list of config parameters for the app
func (a *App) GetConfig() ([]*ssm.Parameter, error) {
return SsmParameters(a.Session, a.ConfigPrefix())
func (a *App) GetConfig() (ConfigVariables, error) {
prefix := a.ConfigPrefix()
parameters, err := SsmParameters(a.Session, prefix)
if err != nil {
return nil, err
}
return NewConfigVariables(parameters), nil
}

// GetConfigWithManaged returns a list of config parameters for the app with managed value populated
func (a *App) GetConfigWithManaged() (ConfigVariables, error) {
configVars, err := a.GetConfig()
if err != nil {
return nil, err
}

ssmSvc := ssm.New(a.Session)
err = configVars.Transform(func(v *ConfigVariable) error {
return v.LoadManaged(ssmSvc.ListTagsForResource)
})
if err != nil {
return nil, err
}

return configVars, nil
}

// SetConfig sets a config value for the app
Expand Down
119 changes: 119 additions & 0 deletions app/config.go
@@ -0,0 +1,119 @@
package app

import (
"bytes"
"fmt"
"sort"
"strings"

"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/juju/ansiterm"
"github.com/sirupsen/logrus"
)

type ConfigVariable struct {
Name string
Value string
Managed bool
parameterName string
}

// LoadManaged loads the Managed value for the ConfigVariable from SSM tags
func (v *ConfigVariable) LoadManaged(ssmListTagsForResource func(*ssm.ListTagsForResourceInput) (*ssm.ListTagsForResourceOutput, error)) error {
logrus.WithFields(logrus.Fields{"parameter": v.parameterName}).Debug("loading parameter tag")
resp, err := ssmListTagsForResource(&ssm.ListTagsForResourceInput{
ResourceId: &v.parameterName,
ResourceType: aws.String(ssm.ResourceTypeForTaggingParameter),
})
if err != nil {
return err
}

for _, tag := range resp.TagList {
if *tag.Key == "aws:cloudformation:stack-id" || *tag.Key == "apppack:cloudformation:stack-id" {
v.Managed = true

return nil
}
}

v.Managed = false

return nil
}

type ConfigVariables []*ConfigVariable

// NewConfigVariables creates a new AppConfigVariables from the provided SSM parameters
func NewConfigVariables(parameters []*ssm.Parameter) ConfigVariables {
var configVars ConfigVariables

for _, parameter := range parameters {
parts := strings.Split(*parameter.Name, "/")
name := parts[len(parts)-1]
configVars = append(configVars, &ConfigVariable{
Name: name,
Value: *parameter.Value,
parameterName: *parameter.Name,
})
}

sort.Slice(configVars, func(i, j int) bool {
return configVars[i].Name < configVars[j].Name
})

return configVars
}

// Transform runs the provided function on each config variable
func (a *ConfigVariables) Transform(transformer func(*ConfigVariable) error) error {
for _, configVar := range *a {
if err := transformer(configVar); err != nil {
return err
}
}

return nil
}

// ToJSON returns a JSON representation of the config variables
func (a *ConfigVariables) ToJSON() (*bytes.Buffer, error) {
results := map[string]string{}

for _, configVar := range *a {
results[configVar.Name] = configVar.Value
}

return toJSON(results)
}

// ToJSONUnmanaged returns a JSON representation of the unmanaged config variables
func (a *ConfigVariables) ToJSONUnmanaged() (*bytes.Buffer, error) {
results := map[string]string{}

for _, configVar := range *a {
if configVar.Managed {
continue
}

results[configVar.Name] = configVar.Value
}

return toJSON(results)
}

// printRow prints a single row of the table to the TabWriter
func printRow(w *ansiterm.TabWriter, name, value string) {
w.SetForeground(ansiterm.Green)
fmt.Fprintf(w, "%s:", name)
w.SetForeground(ansiterm.Default)
fmt.Fprintf(w, "\t%s\n", value)
}

// ToConsole prints the config vars to the console via the TabWriter
func (a *ConfigVariables) ToConsole(w *ansiterm.TabWriter) {
for _, configVar := range *a {
printRow(w, configVar.Name, configVar.Value)
}
}
162 changes: 162 additions & 0 deletions app/config_test.go
@@ -0,0 +1,162 @@
package app_test

import (
"bytes"
"errors"
"testing"

"github.com/apppackio/apppack/app"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/ssm"
"github.com/juju/ansiterm"
)

var errMock = errors.New("mock error")

func TestConfigVariablesToJSON(t *testing.T) {
t.Parallel()

c := app.NewConfigVariables([]*ssm.Parameter{
{Name: aws.String("/apppack/apps/myapp/FOO"), Value: aws.String("bar")},
})

js, err := c.ToJSON()
if err != nil {
t.Error(err)
}

expected := `{
"FOO": "bar"
}`
actual := js.String()

if actual != expected {
t.Errorf("expected %s, got %s", expected, actual)
}
}

func TestConfigVariablesToJSONUnmanaged(t *testing.T) {
t.Parallel()

c := app.ConfigVariables([]*app.ConfigVariable{
{Name: "FOO", Value: "bar", Managed: false},
{Name: "BAZ", Value: "qux", Managed: true},
})
js, err := c.ToJSONUnmanaged()
if err != nil {
t.Error(err)
}

expected := `{
"FOO": "bar"
}`
actual := js.String()

if actual != expected {
t.Errorf("expected %s, got %s", expected, actual)
}
}

func TestConfigVariablesToConsole(t *testing.T) {
t.Parallel()

c := app.NewConfigVariables([]*ssm.Parameter{
{Name: aws.String("/apppack/apps/myapp/config/FOO"), Value: aws.String("bar")},
{Name: aws.String("/apppack/apps/myapp/LONGERVARIABLEFOO"), Value: aws.String("baz")},
})
out := &bytes.Buffer{}
w := ansiterm.NewTabWriter(out, 8, 8, 0, '\t', 0)
c.ToConsole(w)

expected := []byte("FOO:\t\t\tbar\nLONGERVARIABLEFOO:\tbaz\n")

w.Flush()

actual := out.Bytes()
if !bytes.Equal(actual, expected) {
t.Errorf("expected %b, got %b", expected, actual)
}
}

func TestConfigVariablesTransform(t *testing.T) {
t.Parallel()

c := app.NewConfigVariables([]*ssm.Parameter{
{Name: aws.String("/apppack/apps/myapp/config/FOO"), Value: aws.String("bar")},
{Name: aws.String("/apppack/apps/myapp/config/BAZ"), Value: aws.String("qux")},
})
transformedVals := map[string]string{
"FOO": "newvalue_foo",
"BAZ": "newvalue_qux",
}

err := c.Transform(func(v *app.ConfigVariable) error {
v.Value = transformedVals[v.Name]

return nil
})
if err != nil {
t.Error(err)
}

for _, v := range c {
if v.Value != transformedVals[v.Name] {
t.Errorf("expected %s, got %s", transformedVals[v.Name], v.Value)
}
}

err = c.Transform(func(*app.ConfigVariable) error { return errMock })

if !errors.Is(err, errMock) {
t.Errorf("expected %s, got %s", errMock, err)
}
}

func TestConfigVariableLoadManaged(t *testing.T) {
t.Parallel()

managedVar := app.NewConfigVariables([]*ssm.Parameter{
{Name: aws.String("/apppack/apps/myapp/config/FOO"), Value: aws.String("bar")},
})[0]

unmanagedVar := managedVar

managedFunc := func(*ssm.ListTagsForResourceInput) (*ssm.ListTagsForResourceOutput, error) {
return &ssm.ListTagsForResourceOutput{
TagList: []*ssm.Tag{{Key: aws.String("aws:cloudformation:stack-id"), Value: aws.String("stackid")}},
}, nil
}

unmanagedFunc := func(*ssm.ListTagsForResourceInput) (*ssm.ListTagsForResourceOutput, error) {
return &ssm.ListTagsForResourceOutput{TagList: []*ssm.Tag{}}, nil
}

errorFunc := func(*ssm.ListTagsForResourceInput) (*ssm.ListTagsForResourceOutput, error) {
return nil, errMock
}

scenarios := []struct {
cVar *app.ConfigVariable
f func(*ssm.ListTagsForResourceInput) (*ssm.ListTagsForResourceOutput, error)
expected bool
}{
{cVar: managedVar, f: managedFunc, expected: true},
{cVar: unmanagedVar, f: unmanagedFunc, expected: false},
}

for _, s := range scenarios {
err := s.cVar.LoadManaged(s.f)
if err != nil {
t.Error(err)
}

if s.cVar.Managed != s.expected {
t.Errorf("expected %t, got %t", s.expected, s.cVar.Managed)
}
}

err := managedVar.LoadManaged(errorFunc)
if !errors.Is(err, errMock) {
t.Errorf("expected %s, got %s", errMock, err)
}
}
20 changes: 20 additions & 0 deletions app/utils.go
@@ -1,6 +1,8 @@
package app

import (
"bytes"
"encoding/json"
"fmt"
"io"
"strings"
Expand Down Expand Up @@ -45,6 +47,7 @@ func SsmParameters(sess *session.Session, path string) ([]*ssm.Parameter, error)
WithDecryption: aws.Bool(true),
}
err := ssmSvc.GetParametersByPathPages(&input, func(resp *ssm.GetParametersByPathOutput, lastPage bool) bool {
logrus.WithFields(logrus.Fields{"path": *input.Path}).Debug("loading parameter by path page")
for _, parameter := range resp.Parameters {
if parameter == nil {
continue
Expand Down Expand Up @@ -80,3 +83,20 @@ func S3FromURL(sess *session.Session, logURL string) (*strings.Builder, error) {
}
return buf, nil
}

var JSONIndent = " "

func toJSON(v interface{}) (*bytes.Buffer, error) {
j, err := json.Marshal(v)
if err != nil {
return nil, err
}

buf := bytes.NewBuffer([]byte{})

if err = json.Indent(buf, j, "", JSONIndent); err != nil {
return nil, err
}

return buf, nil
}
13 changes: 0 additions & 13 deletions bridge/ssm.go

This file was deleted.

0 comments on commit a329a24

Please sign in to comment.