feat: add `driftctl fmt` command
parent
f2f846de9e
commit
4734fbe5a8
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
169
pkg/cmd/scan.go
169
pkg/cmd/scan.go
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
|
@ -0,0 +1 @@
|
|||
invalid
|
|
@ -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
|
||||
}
|
|
@ -19,6 +19,10 @@ import (
|
|||
"github.com/snyk/driftctl/pkg/resource"
|
||||
)
|
||||
|
||||
type FmtOptions struct {
|
||||
Output output.OutputConfig
|
||||
}
|
||||
|
||||
type ScanOptions struct {
|
||||
Coverage bool
|
||||
Detect bool
|
||||
|
|
Loading…
Reference in New Issue