feat: add `driftctl fmt` command

main
Elie 2022-03-04 10:20:24 +01:00
parent f2f846de9e
commit 4734fbe5a8
No known key found for this signature in database
GPG Key ID: 399AF69092C727B6
11 changed files with 764 additions and 401 deletions

View File

@ -69,6 +69,7 @@ func NewDriftctlCmd(build build.BuildInterface) *DriftctlCmd {
cmd.PersistentFlags().BoolP("send-crash-report", "", false, "Enable error reporting. Crash data will be sent to us via Sentry.\nWARNING: may leak sensitive data (please read the documentation for more details)\nThis flag should be used only if an error occurs during execution")
cmd.AddCommand(NewScanCmd(&pkg.ScanOptions{}))
cmd.AddCommand(NewFmtCmd(&pkg.FmtOptions{}))
cmd.AddCommand(NewGenDriftIgnoreCmd())
return cmd

181
pkg/cmd/flags.go Normal file
View File

@ -0,0 +1,181 @@
package cmd
import (
"fmt"
"strings"
"github.com/pkg/errors"
cmderrors "github.com/snyk/driftctl/pkg/cmd/errors"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/pkg/iac/supplier"
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
)
func parseFromFlag(from []string) ([]config.SupplierConfig, error) {
configs := make([]config.SupplierConfig, 0, len(from))
for _, flag := range from {
schemePath := strings.Split(flag, "://")
if len(schemePath) != 2 || schemePath[1] == "" || schemePath[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted schemes are: %s",
strings.Join(supplier.GetSupportedSchemes(), ","),
),
),
"Unable to parse from flag '%s'",
flag,
)
}
scheme := schemePath[0]
path := schemePath[1]
supplierBackend := strings.Split(scheme, "+")
if len(supplierBackend) > 2 {
return nil, errors.Wrapf(
cmderrors.NewUsageError(fmt.Sprintf(
"\nAccepted schemes are: %s",
strings.Join(supplier.GetSupportedSchemes(), ","),
),
),
"Unable to parse from scheme '%s'",
scheme,
)
}
supplierKey := supplierBackend[0]
if !supplier.IsSupplierSupported(supplierKey) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted values are: %s",
strings.Join(supplier.GetSupportedSuppliers(), ","),
),
),
"Unsupported IaC source '%s'",
supplierKey,
)
}
backendString := ""
if len(supplierBackend) == 2 {
backendString = supplierBackend[1]
if !backend.IsSupported(backendString) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted values are: %s",
strings.Join(backend.GetSupportedBackends(), ","),
),
),
"Unsupported IaC backend '%s'",
backendString,
)
}
}
configs = append(configs, config.SupplierConfig{
Key: supplierKey,
Backend: backendString,
Path: path,
})
}
return configs, nil
}
func parseOutputFlags(out []string) ([]output.OutputConfig, error) {
result := make([]output.OutputConfig, 0, len(out))
for _, v := range out {
o, err := parseOutputFlag(v)
if err != nil {
return result, err
}
result = append(result, *o)
}
return result, nil
}
func parseOutputFlag(out string) (*output.OutputConfig, error) {
schemeOpts := strings.Split(out, "://")
if len(schemeOpts) < 2 || schemeOpts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted formats are: %s",
strings.Join(output.SupportedOutputsExample(), ","),
),
),
"Unable to parse output flag '%s'",
out,
)
}
o := &output.OutputConfig{
Key: schemeOpts[0],
}
if !output.IsSupported(o.Key) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nValid formats are: %s",
strings.Join(output.SupportedOutputsExample(), ","),
),
),
"Unsupported output '%s'",
o.Key,
)
}
opts := schemeOpts[1:]
switch o.Key {
case output.JSONOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.JSONOutputType),
),
),
"Invalid json output '%s'",
out,
)
}
o.Path = opts[0]
case output.HTMLOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.HTMLOutputType),
),
),
"Invalid html output '%s'",
out,
)
}
o.Path = opts[0]
case output.PlanOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.PlanOutputType),
),
),
"Invalid plan output '%s'",
out,
)
}
o.Path = opts[0]
}
return o, nil
}

235
pkg/cmd/flags_test.go Normal file
View File

@ -0,0 +1,235 @@
package cmd
import (
"fmt"
"reflect"
"testing"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
"github.com/snyk/driftctl/pkg/iac/config"
)
func Test_parseFromFlag(t *testing.T) {
type args struct {
from []string
}
tests := []struct {
name string
args args
want []config.SupplierConfig
wantErr bool
}{
{
name: "test complete from parsing",
args: args{
from: []string{"tfstate+s3://bucket/path/to/state.tfstate"},
},
want: []config.SupplierConfig{
{
Key: "tfstate",
Backend: "s3",
Path: "bucket/path/to/state.tfstate",
},
},
wantErr: false,
},
{
name: "test complete from parsing with multiples flags",
args: args{
from: []string{"tfstate+s3://bucket/path/to/state.tfstate", "tfstate:///tmp/my-state.tfstate"},
},
want: []config.SupplierConfig{
{
Key: "tfstate",
Backend: "s3",
Path: "bucket/path/to/state.tfstate",
},
{
Key: "tfstate",
Backend: "",
Path: "/tmp/my-state.tfstate",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseFromFlag(tt.args.from)
if (err != nil) != tt.wantErr {
t.Errorf("parseFromFlag() error = %v, err %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseFromFlag() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_parseOutputFlag(t *testing.T) {
type args struct {
out []string
}
tests := []struct {
name string
args args
want []output.OutputConfig
err error
}{
{
name: "test empty output",
args: args{
out: []string{""},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag '': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test empty array",
args: args{
out: []string{},
},
want: []output.OutputConfig{},
err: nil,
},
{
name: "test invalid",
args: args{
out: []string{"sdgjsdgjsdg"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag 'sdgjsdgjsdg': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test invalid",
args: args{
out: []string{"://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag '://': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test unsupported",
args: args{
out: []string{"foobar://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unsupported output 'foobar': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test empty json",
args: args{
out: []string{"json://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Invalid json output 'json://': \nMust be of kind: json://PATH/TO/FILE.json"),
},
{
name: "test valid console",
args: args{
out: []string{"console://"},
},
want: []output.OutputConfig{
{
Key: "console",
},
},
err: nil,
},
{
name: "test valid json",
args: args{
out: []string{"json:///tmp/foobar.json"},
},
want: []output.OutputConfig{
{
Key: "json",
Path: "/tmp/foobar.json",
},
},
err: nil,
},
{
name: "test empty jsonplan",
args: args{
out: []string{"plan://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Invalid plan output 'plan://': \nMust be of kind: plan://PATH/TO/FILE.json"),
},
{
name: "test valid jsonplan",
args: args{
out: []string{"plan:///tmp/foobar.json"},
},
want: []output.OutputConfig{
{
Key: "plan",
Path: "/tmp/foobar.json",
},
},
err: nil,
},
{
name: "test multiple output values",
args: args{
out: []string{"console:///dev/stdout", "json://result.json"},
},
want: []output.OutputConfig{
{
Key: "console",
},
{
Key: "json",
Path: "result.json",
},
},
err: nil,
},
{
name: "test multiple output values with invalid value",
args: args{
out: []string{"console:///dev/stdout", "invalid://result.json"},
},
want: []output.OutputConfig{
{
Key: "console",
},
},
err: fmt.Errorf("Unsupported output 'invalid': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test multiple valid output values",
args: args{
out: []string{"json://result1.json", "json://result2.json", "json://result3.json"},
},
want: []output.OutputConfig{
{
Key: "json",
Path: "result1.json",
},
{
Key: "json",
Path: "result2.json",
},
{
Key: "json",
Path: "result3.json",
},
},
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseOutputFlags(tt.args.out)
if err != nil && err.Error() != tt.err.Error() {
t.Fatalf("got error = '%v', expected '%v'", err, tt.err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseOutputFlag() got = '%v', want '%v'", got, tt.want)
}
})
}
}

68
pkg/cmd/fmt.go Normal file
View File

@ -0,0 +1,68 @@
package cmd
import (
"bufio"
"encoding/json"
"io"
"os"
"strings"
"github.com/pkg/errors"
"github.com/snyk/driftctl/pkg/analyser"
"github.com/spf13/cobra"
"github.com/snyk/driftctl/pkg"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
)
func NewFmtCmd(opts *pkg.FmtOptions) *cobra.Command {
cmd := &cobra.Command{
Use: "fmt",
Long: "Take an analysis results in JSON on stdin and return it in another format",
Hidden: true,
Args: cobra.NoArgs,
PreRunE: func(cmd *cobra.Command, args []string) error {
outputFlag, _ := cmd.Flags().GetStringSlice("output")
if len(outputFlag) > 1 {
return errors.New("Only one output format can be set")
}
out, err := parseOutputFlags(outputFlag)
if err != nil {
return err
}
opts.Output = out[0]
return nil
},
RunE: func(cmd *cobra.Command, args []string) error {
return runFmt(opts, os.Stdin)
},
}
fl := cmd.Flags()
fl.StringSliceP(
"output",
"o",
[]string{output.Example(output.ConsoleOutputType)},
"Output format, by default it will write to the console\n"+
"Accepted formats are: "+strings.Join(output.SupportedOutputsExample(), ",")+"\n",
)
return cmd
}
func runFmt(opts *pkg.FmtOptions, reader io.Reader) error {
var analysisText []byte
scanner := bufio.NewScanner(reader)
for scanner.Scan() {
analysisText = append(analysisText, scanner.Bytes()...)
}
analysis := analyser.NewAnalysis(analyser.AnalyzerOptions{})
err := json.Unmarshal(analysisText, analysis)
if err != nil {
return err
}
return output.GetOutput(opts.Output).Write(analysis)
}

131
pkg/cmd/fmt_test.go Normal file
View File

@ -0,0 +1,131 @@
package cmd
import (
"bytes"
"io"
"os"
"testing"
"github.com/snyk/driftctl/pkg"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
"github.com/snyk/driftctl/test"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_runFmt_InvalidInput(t *testing.T) {
opts := &pkg.FmtOptions{
Output: output.OutputConfig{
Key: output.ConsoleOutputType,
},
}
input, err := os.Open("testdata/fmt/input_stdin_invalid.json")
if err != nil {
t.Fatal(err)
}
defer input.Close()
err = runFmt(opts, input)
require.NotNil(t, err)
assert.Equal(t, "invalid character 'i' looking for beginning of value", err.Error())
}
func Test_runFmt(t *testing.T) {
opts := &pkg.FmtOptions{
Output: output.OutputConfig{
Key: output.ConsoleOutputType,
},
}
input, err := os.Open("testdata/fmt/input_stdin_valid.json")
if err != nil {
t.Fatal(err)
}
defer input.Close()
stdout := os.Stdout // keep backup of the real stdout
stderr := os.Stderr // keep backup of the real stderr
r, w, _ := os.Pipe()
os.Stdout = w
os.Stderr = w
err = runFmt(opts, input)
outC := make(chan []byte)
// copy the output in a separate goroutine so printing can't block indefinitely
go func() {
var buf bytes.Buffer
_, _ = io.Copy(&buf, r)
outC <- buf.Bytes()
}()
// back to normal state
assert.Nil(t, w.Close())
os.Stdout = stdout // restoring the real stdout
os.Stderr = stderr
output := <-outC
if err != nil {
t.Fatal(err)
}
expectedBytes, err := os.ReadFile("testdata/fmt/expected_console.txt")
if err != nil {
t.Fatal(err)
}
assert.Equal(t, string(expectedBytes), string(output))
}
func TestFmtCmd_Valid(t *testing.T) {
rootCmd := &cobra.Command{Use: "root"}
scanCmd := NewFmtCmd(&pkg.FmtOptions{})
scanCmd.RunE = func(_ *cobra.Command, args []string) error { return nil }
rootCmd.AddCommand(scanCmd)
cases := []struct {
args []string
}{
{args: []string{"fmt"}},
{args: []string{"fmt", "-o", "json://test.json"}},
}
for _, tt := range cases {
t.Run("", func(t *testing.T) {
output, err := test.Execute(rootCmd, tt.args...)
if output != "" {
t.Errorf("Unexpected output: %v", output)
}
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
})
}
}
func TestFmtCmd_Invalid(t *testing.T) {
cases := []struct {
args []string
expected string
}{
{args: []string{"fmt", "test"}, expected: `unknown command "test" for "root fmt"`},
{args: []string{"fmt", "-o", "json://test.json", "-o", "html://test.html"}, expected: "Only one output format can be set"},
{args: []string{"fmt", "-o", "foobar://barfoo"}, expected: "Unsupported output 'foobar': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"},
}
for _, tt := range cases {
t.Run("", func(t *testing.T) {
rootCmd := &cobra.Command{Use: "root"}
rootCmd.AddCommand(NewFmtCmd(&pkg.FmtOptions{}))
_, err := test.Execute(rootCmd, tt.args...)
if err == nil {
t.Errorf("Invalid arg should generate error")
}
if err.Error() != tt.expected {
t.Errorf("Expected '%v', got '%v'", tt.expected, err)
}
})
}
}

View File

@ -26,7 +26,6 @@ import (
cmderrors "github.com/snyk/driftctl/pkg/cmd/errors"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
"github.com/snyk/driftctl/pkg/filter"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/pkg/iac/supplier"
"github.com/snyk/driftctl/pkg/iac/terraform/state/backend"
globaloutput "github.com/snyk/driftctl/pkg/output"
@ -354,174 +353,6 @@ func scanRun(opts *pkg.ScanOptions) error {
return nil
}
func parseFromFlag(from []string) ([]config.SupplierConfig, error) {
configs := make([]config.SupplierConfig, 0, len(from))
for _, flag := range from {
schemePath := strings.Split(flag, "://")
if len(schemePath) != 2 || schemePath[1] == "" || schemePath[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted schemes are: %s",
strings.Join(supplier.GetSupportedSchemes(), ","),
),
),
"Unable to parse from flag '%s'",
flag,
)
}
scheme := schemePath[0]
path := schemePath[1]
supplierBackend := strings.Split(scheme, "+")
if len(supplierBackend) > 2 {
return nil, errors.Wrapf(
cmderrors.NewUsageError(fmt.Sprintf(
"\nAccepted schemes are: %s",
strings.Join(supplier.GetSupportedSchemes(), ","),
),
),
"Unable to parse from scheme '%s'",
scheme,
)
}
supplierKey := supplierBackend[0]
if !supplier.IsSupplierSupported(supplierKey) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted values are: %s",
strings.Join(supplier.GetSupportedSuppliers(), ","),
),
),
"Unsupported IaC source '%s'",
supplierKey,
)
}
backendString := ""
if len(supplierBackend) == 2 {
backendString = supplierBackend[1]
if !backend.IsSupported(backendString) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted values are: %s",
strings.Join(backend.GetSupportedBackends(), ","),
),
),
"Unsupported IaC backend '%s'",
backendString,
)
}
}
configs = append(configs, config.SupplierConfig{
Key: supplierKey,
Backend: backendString,
Path: path,
})
}
return configs, nil
}
func parseOutputFlags(out []string) ([]output.OutputConfig, error) {
result := make([]output.OutputConfig, 0, len(out))
for _, v := range out {
o, err := parseOutputFlag(v)
if err != nil {
return result, err
}
result = append(result, *o)
}
return result, nil
}
func parseOutputFlag(out string) (*output.OutputConfig, error) {
schemeOpts := strings.Split(out, "://")
if len(schemeOpts) < 2 || schemeOpts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nAccepted formats are: %s",
strings.Join(output.SupportedOutputsExample(), ","),
),
),
"Unable to parse output flag '%s'",
out,
)
}
o := &output.OutputConfig{
Key: schemeOpts[0],
}
if !output.IsSupported(o.Key) {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nValid formats are: %s",
strings.Join(output.SupportedOutputsExample(), ","),
),
),
"Unsupported output '%s'",
o.Key,
)
}
opts := schemeOpts[1:]
switch o.Key {
case output.JSONOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.JSONOutputType),
),
),
"Invalid json output '%s'",
out,
)
}
o.Path = opts[0]
case output.HTMLOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.HTMLOutputType),
),
),
"Invalid html output '%s'",
out,
)
}
o.Path = opts[0]
case output.PlanOutputType:
if len(opts) != 1 || opts[0] == "" {
return nil, errors.Wrapf(
cmderrors.NewUsageError(
fmt.Sprintf(
"\nMust be of kind: %s",
output.Example(output.PlanOutputType),
),
),
"Invalid plan output '%s'",
out,
)
}
o.Path = opts[0]
}
return o, nil
}
func validateTfProviderVersionString(version string) error {
if version == "" {
return nil

View File

@ -1,18 +1,12 @@
package cmd
import (
"fmt"
"reflect"
"testing"
"github.com/snyk/driftctl/pkg"
"github.com/snyk/driftctl/pkg/cmd/scan/output"
"github.com/stretchr/testify/assert"
"github.com/snyk/driftctl/pkg/iac/config"
"github.com/snyk/driftctl/test"
"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
)
// TODO: Test successful scan
@ -111,231 +105,6 @@ func TestScanCmd_Invalid(t *testing.T) {
}
}
func Test_parseFromFlag(t *testing.T) {
type args struct {
from []string
}
tests := []struct {
name string
args args
want []config.SupplierConfig
wantErr bool
}{
{
name: "test complete from parsing",
args: args{
from: []string{"tfstate+s3://bucket/path/to/state.tfstate"},
},
want: []config.SupplierConfig{
{
Key: "tfstate",
Backend: "s3",
Path: "bucket/path/to/state.tfstate",
},
},
wantErr: false,
},
{
name: "test complete from parsing with multiples flags",
args: args{
from: []string{"tfstate+s3://bucket/path/to/state.tfstate", "tfstate:///tmp/my-state.tfstate"},
},
want: []config.SupplierConfig{
{
Key: "tfstate",
Backend: "s3",
Path: "bucket/path/to/state.tfstate",
},
{
Key: "tfstate",
Backend: "",
Path: "/tmp/my-state.tfstate",
},
},
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseFromFlag(tt.args.from)
if (err != nil) != tt.wantErr {
t.Errorf("parseFromFlag() error = %v, err %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("parseFromFlag() got = %v, want %v", got, tt.want)
}
})
}
}
func Test_parseOutputFlag(t *testing.T) {
type args struct {
out []string
}
tests := []struct {
name string
args args
want []output.OutputConfig
err error
}{
{
name: "test empty output",
args: args{
out: []string{""},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag '': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test empty array",
args: args{
out: []string{},
},
want: []output.OutputConfig{},
err: nil,
},
{
name: "test invalid",
args: args{
out: []string{"sdgjsdgjsdg"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag 'sdgjsdgjsdg': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test invalid",
args: args{
out: []string{"://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unable to parse output flag '://': \nAccepted formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test unsupported",
args: args{
out: []string{"foobar://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Unsupported output 'foobar': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test empty json",
args: args{
out: []string{"json://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Invalid json output 'json://': \nMust be of kind: json://PATH/TO/FILE.json"),
},
{
name: "test valid console",
args: args{
out: []string{"console://"},
},
want: []output.OutputConfig{
{
Key: "console",
},
},
err: nil,
},
{
name: "test valid json",
args: args{
out: []string{"json:///tmp/foobar.json"},
},
want: []output.OutputConfig{
{
Key: "json",
Path: "/tmp/foobar.json",
},
},
err: nil,
},
{
name: "test empty jsonplan",
args: args{
out: []string{"plan://"},
},
want: []output.OutputConfig{},
err: fmt.Errorf("Invalid plan output 'plan://': \nMust be of kind: plan://PATH/TO/FILE.json"),
},
{
name: "test valid jsonplan",
args: args{
out: []string{"plan:///tmp/foobar.json"},
},
want: []output.OutputConfig{
{
Key: "plan",
Path: "/tmp/foobar.json",
},
},
err: nil,
},
{
name: "test multiple output values",
args: args{
out: []string{"console:///dev/stdout", "json://result.json"},
},
want: []output.OutputConfig{
{
Key: "console",
},
{
Key: "json",
Path: "result.json",
},
},
err: nil,
},
{
name: "test multiple output values with invalid value",
args: args{
out: []string{"console:///dev/stdout", "invalid://result.json"},
},
want: []output.OutputConfig{
{
Key: "console",
},
},
err: fmt.Errorf("Unsupported output 'invalid': \nValid formats are: console://,html://PATH/TO/FILE.html,json://PATH/TO/FILE.json,plan://PATH/TO/FILE.json"),
},
{
name: "test multiple valid output values",
args: args{
out: []string{"json://result1.json", "json://result2.json", "json://result3.json"},
},
want: []output.OutputConfig{
{
Key: "json",
Path: "result1.json",
},
{
Key: "json",
Path: "result2.json",
},
{
Key: "json",
Path: "result3.json",
},
},
err: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got, err := parseOutputFlags(tt.args.out)
if err != nil && err.Error() != tt.err.Error() {
t.Fatalf("got error = '%v', expected '%v'", err, tt.err)
}
if !reflect.DeepEqual(got, tt.want) {
t.Fatalf("parseOutputFlag() got = '%v', want '%v'", got, tt.want)
}
})
}
}
func Test_Options(t *testing.T) {
cases := []struct {
name string

View File

@ -0,0 +1,30 @@
Found missing resources:
- testuser1 (aws_iam_user)
- testrole1 (aws_iam_role)
Found resources not covered by IaC:
aws_iam_access_key:
- AKIAXYUOJZ3H5YCXF34G
- AKIAXYUOJZ3HV2LTLXD2
- AKIAXYUOJZ3HUSPPQQ4L
aws_iam_role:
- OrganizationAccountAccessRole
- driftctl_assume\_role
aws_iam_role_policy:
- OrganizationAccountAccessRole:AdministratorAccess
- driftctl_assume_role:driftctl_policy.10
aws_iam_user:
- driftctl
- sundowndev
- test_user
aws_iam_user_policy:
- driftctl:driftctlrole
Found changed resources:
- test-20210416154114486700000001 (aws_s3_bucket):
~ BucketPrefix: "test-" => <nil>
+ Tags.tag2: <nil> => "value"
~ Tags.test: "test" => "test1"
Found 14 resource(s)
- 7% coverage
- 1 resource(s) managed by Terraform
- 11 resource(s) not managed by Terraform
- 2 resource(s) found in a Terraform state but missing on the cloud provider

View File

@ -0,0 +1 @@
invalid

View File

@ -0,0 +1,112 @@
{
"summary": {
"total_resources": 12,
"total_changed": 1,
"total_unmanaged": 11,
"total_missing": 0,
"total_managed": 1
},
"managed": [
{
"id": "test-20210416154114486700000001",
"type": "aws_s3_bucket"
}
],
"unmanaged": [
{
"id": "driftctl",
"type": "aws_iam_user"
},
{
"id": "sundowndev",
"type": "aws_iam_user"
},
{
"id": "test_user",
"type": "aws_iam_user"
},
{
"id": "OrganizationAccountAccessRole:AdministratorAccess",
"type": "aws_iam_role_policy"
},
{
"id": "driftctl_assume_role:driftctl_policy.10",
"type": "aws_iam_role_policy"
},
{
"id": "OrganizationAccountAccessRole",
"type": "aws_iam_role"
},
{
"id": "driftctl_assume\\_role",
"type": "aws_iam_role"
},
{
"id": "driftctl:driftctlrole",
"type": "aws_iam_user_policy"
},
{
"id": "AKIAXYUOJZ3H5YCXF34G",
"type": "aws_iam_access_key"
},
{
"id": "AKIAXYUOJZ3HV2LTLXD2",
"type": "aws_iam_access_key"
},
{
"id": "AKIAXYUOJZ3HUSPPQQ4L",
"type": "aws_iam_access_key"
}
],
"missing": [
{
"id": "testuser1",
"type": "aws_iam_user"
},
{
"id": "testrole1",
"type": "aws_iam_role"
}
],
"differences": [
{
"res": {
"id": "test-20210416154114486700000001",
"type": "aws_s3_bucket"
},
"changelog": [
{
"type": "update",
"path": [
"BucketPrefix"
],
"from": "test-",
"to": null,
"computed": false
},
{
"type": "create",
"path": [
"Tags",
"tag2"
],
"from": null,
"to": "value",
"computed": false
},
{
"type": "update",
"path": [
"Tags",
"test"
],
"from": "test",
"to": "test1",
"computed": false
}
]
}
],
"coverage": 8,
"alerts": null
}

View File

@ -19,6 +19,10 @@ import (
"github.com/snyk/driftctl/pkg/resource"
)
type FmtOptions struct {
Output output.OutputConfig
}
type ScanOptions struct {
Coverage bool
Detect bool