Skip to content

Commit

Permalink
Allow multiple attributes in CLI flag via comma-separated list
Browse files Browse the repository at this point in the history
  • Loading branch information
jckuester committed May 28, 2020
1 parent 93c2a46 commit eb2da26
Show file tree
Hide file tree
Showing 11 changed files with 279 additions and 31 deletions.
1 change: 1 addition & 0 deletions aws/client.go
Expand Up @@ -5,6 +5,7 @@ package aws
import (
"context"
"fmt"

"github.com/aws/aws-sdk-go-v2/aws/external"
"github.com/aws/aws-sdk-go-v2/service/accessanalyzer"
"github.com/aws/aws-sdk-go-v2/service/acm"
Expand Down
1 change: 1 addition & 0 deletions go.mod
Expand Up @@ -8,6 +8,7 @@ require (
github.com/aws/aws-sdk-go-v2 v0.22.0
github.com/fatih/color v1.9.0
github.com/gobwas/glob v0.2.3
github.com/gruntwork-io/terratest v0.23.0
github.com/jckuester/terradozer v0.0.0-20200523195146-e66de6fa55f3
github.com/onsi/gomega v1.9.0
github.com/pkg/errors v0.9.1
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Expand Up @@ -89,6 +89,7 @@ github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnweb
github.com/bmatcuk/doublestar v1.1.5 h1:2bNwBOmhyFEFcoB3tGvTD5xanq+4kyOZlB8wFYbMjkk=
github.com/bmatcuk/doublestar v1.1.5/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
github.com/boltdb/bolt v1.3.1/go.mod h1:clJnj/oiGkjum5o1McbSZDSLxVThjynRyGBgiAx27Ps=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc h1:biVzkmvwrH8WK8raXaxBx6fRVTlJILwEwQGL1I/ByEI=
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
github.com/cheggaaa/pb v1.0.27/go.mod h1:pQciLPpbU0oxA0h+VJYYLxO+XeDQb5pZijXscXHm81s=
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
Expand Down Expand Up @@ -127,6 +128,7 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
github.com/go-sql-driver/mysql v1.4.1/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gGcHOs=
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.1/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
Expand Down Expand Up @@ -177,6 +179,7 @@ github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmg
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
github.com/grpc-ecosystem/grpc-gateway v1.8.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY=
github.com/gruntwork-io/gruntwork-cli v0.5.1/go.mod h1:IBX21bESC1/LGoV7jhXKUnTQTZgQ6dYRsoj/VqxUSZQ=
github.com/gruntwork-io/terratest v0.23.0 h1:JmGeqO0r5zRLAV55T67NEmPZArz9lN3RKd0moAKhIT4=
github.com/gruntwork-io/terratest v0.23.0/go.mod h1:+fVff0FQYuRzCF3LKpKF9ac+4w384LDcwLZt7O/KmEE=
github.com/hashicorp/aws-sdk-go-base v0.4.0/go.mod h1:eRhlz3c4nhqxFZJAahJEFL7gh6Jyj5rQmQc7F9eHFyQ=
github.com/hashicorp/consul v0.0.0-20171026175957-610f3c86a089/go.mod h1:mFrjN1mfidgJfYP1xrJCF+AfRhr6Eaqhb2+sfyn/OOI=
Expand Down Expand Up @@ -365,6 +368,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI=
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/pquerna/otp v1.2.0 h1:/A3+Jn+cagqayeR3iHs/L62m5ue7710D35zl1zJ1kok=
github.com/pquerna/otp v1.2.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs=
Expand Down
27 changes: 27 additions & 0 deletions internal/attributes_flag.go
@@ -0,0 +1,27 @@
package internal

import (
"fmt"
"strings"
)

type Attributes []string

func (attrs *Attributes) String() string {
return fmt.Sprint(*attrs)
}

// Set is the method to set the flag value, part of the flag.Value interface.
// Set's argument is a string to be parsed to set the flag.
// It's a comma-separated list, so we split it.
func (attrs *Attributes) Set(attributes string) error {
if len(*attrs) > 0 {
return fmt.Errorf("attributes flag already set")
}

for _, attr := range strings.Split(attributes, ",") {
*attrs = append(*attrs, attr)
}

return nil
}
60 changes: 38 additions & 22 deletions main.go
Expand Up @@ -27,7 +27,7 @@ func mainExitCode() int {
var logDebug bool
var profile string
var region string
var attribute string
var attributes internal.Attributes
var version bool

flags := flag.NewFlagSet(os.Args[0], flag.ExitOnError)
Expand All @@ -39,7 +39,7 @@ func mainExitCode() int {
flags.BoolVar(&logDebug, "debug", false, "Enable debug logging")
flags.StringVar(&profile, "profile", "", "Use a specific named profile from your AWS credential file")
flags.StringVar(&region, "region", "", "The region to list resources in")
flags.StringVar(&attribute, "attribute", "", "A Terraform attribute to show for each resource")
flags.Var(&attributes, "attributes", "Comma-separated list of attributes to show for each resource")
flags.BoolVar(&version, "version", false, "Show application version")

_ = flags.Parse(os.Args[1:])
Expand Down Expand Up @@ -130,63 +130,79 @@ func mainExitCode() int {
continue
}

hasAttr, err := resource.HasAttribute(attribute, rType, awsTerraformProvider)
hasAttrs, err := resource.HasAttributes(attributes, rType, awsTerraformProvider)
if err != nil {
fmt.Fprint(os.Stderr, color.RedString("Error: failed to check if resource type has attribute: "+
"%s\n", err))

continue
}

if hasAttr {
if len(hasAttrs) > 0 {
// for performance reasons, only load the state if this resource type has all the attributes
resourcesWithStates := resource.GetStates(resources, awsTerraformProvider)
printResources(resourcesWithStates, hasAttr, attribute)
printResources(resourcesWithStates, hasAttrs, attributes)

continue
}

printResources(resources, hasAttr, attribute)
printResources(resources, hasAttrs, attributes)
}

return 0
}

func printResources(resources []aws.Resource, hasAttr bool, attribute string) {
func printResources(resources []aws.Resource, hasAttrs map[string]bool, attributes []string) {
const padding = 3
w := tabwriter.NewWriter(os.Stdout, 0, 0, padding, ' ', tabwriter.TabIndent)

// header
fmt.Fprintf(w, "TYPE\tID\tREGION\tCREATED\t%s\t\n", strings.ToUpper(attribute))
printHeader(w, attributes)

for _, r := range resources {
fmt.Fprintf(w, "%s\t%s\t%s\t", r.Type, r.ID, r.Region)
fmt.Fprintf(w, "%s\t%s\t%s", r.Type, r.ID, r.Region)

if r.CreatedAt != nil {
fmt.Fprintf(w, "%s\t", r.CreatedAt.Format("2006-01-02 15:04:05"))
fmt.Fprintf(w, "\t%s", r.CreatedAt.Format("2006-01-02 15:04:05"))
} else {
fmt.Fprint(w, "N/A\t")
fmt.Fprint(w, "\tN/A")
}

v := "N/A"
for _, attr := range attributes {
v := "N/A"

if hasAttr {
var err error
v, err = resource.GetAttribute(attribute, &r)
if err != nil {
log.WithFields(log.Fields{
"type": r.Type,
"id": r.ID}).WithError(err).Debug("failed to get attribute")
v = "error"
_, ok := hasAttrs[attr]

if ok {
var err error
v, err = resource.GetAttribute(attr, &r)
if err != nil {
log.WithFields(log.Fields{
"type": r.Type,
"id": r.ID}).WithError(err).Debug("failed to get attribute")
v = "error"
}
}

fmt.Fprintf(w, "\t%s", v)
}

fmt.Fprintf(w, "%s\t\n", v)
fmt.Fprintf(w, "\t\n")
}

w.Flush()
fmt.Println()
}

func printHeader(w *tabwriter.Writer, attributes []string) {
fmt.Fprintf(w, "TYPE\tID\tREGION\tCREATED")

for _, attribute := range attributes {
fmt.Fprintf(w, "\t%s", strings.ToUpper(attribute))
}

fmt.Fprintf(w, "\t\n")
}

func printHelp(fs *flag.FlagSet) {
fmt.Fprintf(os.Stderr, "\n"+strings.TrimSpace(help)+"\n")
fs.PrintDefaults()
Expand Down
18 changes: 12 additions & 6 deletions resource/utils.go
Expand Up @@ -106,18 +106,24 @@ func GetStates(resources []aws.Resource, provider *provider.TerraformProvider) [
return resources
}

func HasAttribute(attrName, terraformType string, provider *provider.TerraformProvider) (bool, error) {
// HasAttributes returns only the attributes that the given Terraform resource type supports out of a given
// list of attributes.
func HasAttributes(attributes []string, terraformType string, provider *provider.TerraformProvider) (map[string]bool, error) {
schema, err := provider.GetSchemaForResource(terraformType)
if err != nil {
return false, err
return nil, err
}

_, ok := schema.Block.Attributes[attrName]
if ok {
return true, nil
result := map[string]bool{}

for _, attr := range attributes {
_, ok := schema.Block.Attributes[attr]
if ok {
result[attr] = true
}
}

return false, nil
return result, nil
}

// GetAttribute returns any Terraform attribute of a resource by name.
Expand Down
87 changes: 84 additions & 3 deletions test/acc_test.go
Expand Up @@ -4,8 +4,10 @@ import (
"bytes"
"fmt"
"os/exec"
"runtime"
"testing"

"github.com/gruntwork-io/terratest/modules/terraform"
"github.com/onsi/gomega/gexec"

"github.com/stretchr/testify/assert"
Expand All @@ -16,6 +18,65 @@ const (
packagePath = "github.com/jckuester/awsls"
)

func TestAcc_Attributes(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test.")
}

env := InitEnv(t)

terraformDir := "./test-fixtures/aws-vpc"

terraformOptions := GetTerraformOptions(terraformDir, env)

defer terraform.Destroy(t, terraformOptions)

terraform.InitAndApply(t, terraformOptions)

actualVpcID := terraform.Output(t, terraformOptions, "vpc_id")
AssertVpcExists(t, actualVpcID, env)

tests := []struct {
name string
attributes []string
expectedLogs []string
}{
{
name: "single attributes",
attributes: []string{"-attributes", "tags", "aws_vpc"},
expectedLogs: []string{
"REGION CREATED TAGS",
"us-west-2 N/A bar=baz,foo=bar",
},
},
{
name: "multiple attributes",
attributes: []string{"-attributes", "tags,cidr_block", "aws_vpc"},
expectedLogs: []string{
"REGION CREATED TAGS CIDR_BLOCK",
"us-west-2 N/A bar=baz,foo=bar 10.0.0.0/16",
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {

logBuffer, err := runBinary(t, tc.attributes...)
require.NoError(t, err)

AssertVpcExists(t, actualVpcID, env)

actualLogs := logBuffer.String()

for _, expectedLogEntry := range tc.expectedLogs {
assert.Contains(t, actualLogs, expectedLogEntry)
}

fmt.Println(actualLogs)
})
}
}

func TestAcc_NonExistingResourceType(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test.")
Expand All @@ -26,7 +87,7 @@ func TestAcc_NonExistingResourceType(t *testing.T) {

actualLogs := logBuffer.String()

assert.Contains(t, actualLogs, "Error: not a valid Terraform AWS resource type: aws_foo")
assert.Contains(t, actualLogs, "Error: no resource type found: aws_foo")

fmt.Println(actualLogs)
}
Expand All @@ -36,12 +97,32 @@ func TestAcc_UnsupportedResourceType(t *testing.T) {
t.Skip("Skipping acceptance test.")
}

logBuffer, err := runBinary(t, "aws_opsworks_mysql_layer")
logBuffer, err := runBinary(t, "-debug", "aws_opsworks_mysql_layer")
require.Error(t, err)

actualLogs := logBuffer.String()

assert.Contains(t, actualLogs, "Error: resource type is not (yet) supported: aws_opsworks_mysql_layer")
assert.Contains(t, actualLogs, "resource type not (yet) supported: aws_opsworks_mysql_layer")
assert.Contains(t, actualLogs, "Error: no resource type found: aws_opsworks_mysql_layer")

fmt.Println(actualLogs)
}

func TestAcc_Version(t *testing.T) {
if testing.Short() {
t.Skip("Skipping acceptance test.")
}

logBuffer, err := runBinary(t, "-version")
require.NoError(t, err)

actualLogs := logBuffer.String()

assert.Contains(t, actualLogs, fmt.Sprintf(`
version: dev
commit: ?
built at: ?
using: %s`, runtime.Version()))

fmt.Println(actualLogs)
}
Expand Down

0 comments on commit eb2da26

Please sign in to comment.