diff --git a/.golangci.yml b/.golangci.yml index 1f33d96b..991915f7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,109 +1,34 @@ --- - run: - # default concurrency is a available CPU number - # concurrency: 4 - - # timeout for analysis, e.g. 30s, 5m, default is 1m timeout: 5m - - # exit code when at least one issue was found, default is 1 issues-exit-code: 1 - - # include test files or not, default is true tests: true - # list of build tags, all linters use it. Default is empty list. - # build-tags: - - # which dirs to skip: issues from them won't be reported; - # can use regexp here: generated.*, regexp is applied on full path; - # default value is empty list, but default dirs are skipped independently - # from this option's value (see skip-dirs-use-default). - # "/" will be replaced by current OS file path separator to properly work - # on Windows. - # skip-dirs: - # - src/external_libs - # - autogenerated_by_my_lib - - # default is true. Enables skipping of directories: - # vendor$, third_party$, testdata$, examples$, Godeps$, builtin$ - skip-dirs-use-default: true - - # which files to skip: they will be analyzed, but issues from them - # won't be reported. Default value is empty list, but there is - # no need to include all autogenerated files, we confidently recognize - # autogenerated files. If it's not please let us know. - # "/" will be replaced by current OS file path separator to properly work - # on Windows. - # skip-files: - # - ".*\\.my\\.go$" - # - lib/bad.go - - # by default isn't set. If set we pass it to "go list -mod={option}". From "go help modules": - # If invoked with -mod=readonly, the go command is disallowed from the implicit - # automatic updating of go.mod described above. Instead, it fails when any changes - # to go.mod are needed. This setting is most useful to check that go.mod does - # not need updates, such as in a continuous integration and testing system. - # If invoked with -mod=vendor, the go command assumes that the vendor - # directory holds the correct copies of dependencies and ignores - # the dependency descriptions in go.mod. - # modules-download-mode: readonly|vendor|mod - - # Allow multiple parallel golangci-lint instances running. - # If false (default) - golangci-lint acquires file lock on start. - allow-parallel-runners: false - - # output configuration options output: # colored-line-number|line-number|json|tab|checkstyle|code-climate|junit-xml|github-actions # default is "colored-line-number" format: colored-line-number - # print lines of code with issue, default is true print-issued-lines: true - # print linter name in the end of issue text, default is true print-linter-name: true - # make issues output unique by line, default is true uniq-by-line: true - # add a prefix to the output file references; default is no prefix # path-prefix: "" - # sorts results by: filepath, line and column sort-results: false - # all available settings of specific linters linters-settings: - errcheck: - # report about not checking of errors in type assertions: `a := b.(MyStruct)`; - # default is false: such cases aren't reported by default. check-type-assertions: true - - # report about assignment of errors to blank identifier: `num, _ := strconv.Atoi(numStr)`; - # default is false: such cases aren't reported by default. check-blank: false - # [deprecated] comma-separated list of pairs of the form pkg:regex - # the regex is used to ignore names within pkg. (default "fmt:.*"). - # see https://github.com/kisielk/errcheck#the-deprecated-method for details - # ignore: fmt:.*,io/ioutil:^Read.* - - # path to a file containing a list of functions to exclude from checking - # see https://github.com/kisielk/errcheck#excluding-functions for details - #exclude: /path/to/file.txt - cyclop: - # the maximal code complexity to report - max-complexity: 35 - # the maximal average package complexity. If it's higher than 0.0 (float) the check is enabled (default 0.0) + max-complexity: 30 package-average: 0.0 - # should ignore tests (default false) skip-tests: false goimports: @@ -112,97 +37,91 @@ linters-settings: local-prefixes: github.com/hashicorp gofumpt: - # Choose whether or not to use the extra rules that are disabled - # by default extra-rules: false - govet: - # report about shadowed variables - check-shadowing: false + gosec: + excludes: + # Don't worry about decompression bombs, not relevant for our usage of zip + - G110 + # Don't worry about zip file traversals + - G305 + config: + G306: + # allow creating files with 0755 permissions + "0755" - # settings per analyzer - #settings: - # printf: # analyzer name, run `go tool vet help` to see all analyzers - # funcs: # run `go tool vet help printf` to see available settings for `printf` analyzer - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - # - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf + interfacebloat: + max: 12 - # enable or disable analyzers by name - # run `go tool vet help` to see all analyzers - #enable: - # - atomicalign - #enable-all: false - #disable: - # - shadow - #disable-all: false + nlreturn: + # Size of the block (including return statement that is still "OK") + block-size: 2 revive: # see https://github.com/mgechev/revive#available-rules for details. ignore-generated-header: true severity: warning - #rules: - # - name: indent-error-flow - # severity: warning - - staticcheck: - # Select the Go version to target. The default is '1.13'. - go: "1.19" - - stylecheck: - # Select the Go version to target. The default is '1.13'. - go: "1.19" - - unused: - # Select the Go version to target. The default is '1.13'. - go: "1.19" linters: - enable: - # defaults + enable-all: true + disable: + - containedctx - deadcode - - errcheck - - gosimple - - govet - - ineffassign - - staticcheck + - depguard + - dogsled + - dupl + - errname + - errorlint + - execinquery + - exhaustivestruct + - exhaustruct + - forbidigo + - forcetypeassert + - funlen + - gci + - ginkgolinter + - gochecknoglobals + - gochecknoinits + - gocognit + - goconst + # disabled for now + - gocritic + # disabled for now + - godox + - goerr113 + - gofmt + - goheader + - golint + - gomnd + - ifshort + - importas + - interfacer + - ireturn + - lll + - loggercheck + - maintidx + - maligned + - musttag + - nakedret + - nestif + - nonamedreturns + - nosprintfhostport + - nosnakecase + - promlinter + - rowserrcheck + - scopelint + - sqlclosecheck - structcheck - - typecheck - - unused + - tagliatelle + - testableexamples + - testpackage + # disabled because it's not friendly with the terraform test helper + - tparallel - varcheck - # others - - gofumpt - - goimports - - asciicheck - - bodyclose - - cyclop - - stylecheck - - revive - disable-all: true + - varnamelen + - wrapcheck + - wsl fast: false severity: - # Default value is empty string. - # Set the default severity for issues. If severity rules are defined and the issues - # do not match or no severity is provided to the rule this will be the default - # severity applied. Severities should match the supported severity names of the - # selected out format. - # - Code climate: https://docs.codeclimate.com/docs/issues#issue-severity - # - Checkstyle: https://checkstyle.sourceforge.io/property_types.html#severity - # - Github: https://help.github.com/en/actions/reference/workflow-commands-for-github-actions#setting-an-error-message - # default-severity: error - - # The default value is false. - # If set to true severity-rules regular expressions become case sensitive. case-sensitive: false - - # Default value is empty list. - # When a list of severity rules are provided, severity information will be added to lint - # issues. Severity rules have the same filtering capability as exclude rules except you - # are allowed to specify one matcher per severity rule. - # Only affects out formats that support setting severity information. - # rules: - # - linters: - # - dupl - # severity: info diff --git a/Makefile b/Makefile index 3c045c99..a2f0c27d 100644 --- a/Makefile +++ b/Makefile @@ -6,7 +6,8 @@ BUILD_BINARY_PATH=${CURRENT_DIRECTORY}/dist/${BINARY} REPO=github.com/hashicorp/enos GO_BUILD_TAGS=-tags osusergo,netgo GO_LD_FLAGS=-ldflags="-extldflags=-static -X ${REPO}/internal/version.Version=${VERSION} -X ${REPO}/internal/version.GitSHA=${GIT_SHA}" -GO_GC_FLAGS=-gcflags="all=-N -l" +GO_GC_FLAGS= +LINT_OUT_FORMAT?=colored-line-number GORACE=GORACE=log_path=/tmp/enos-gorace.log TEST_ACC=ENOS_ACC=1 TEST_ACC_EXT=ENOS_ACC=1 ENOS_EXT=1 @@ -47,9 +48,16 @@ lint: lint-golang lint-proto lint-golang: golangci-lint run -v +.PHONY: lint-fix +lint-fix: lint-fix-golang + +.PHONY: lint-fix-golang +lint-fix-golang: + golangci-lint run -v --out-format=$(LINT_OUT_FORMAT) --fix + .PHONY: lint-proto lint-proto: - pushd proto && buf lint + pushd proto && buf lint --error-format=$(LINT_OUT_FORMAT) .PHONY: fmt fmt: fmt-golang fmt-proto diff --git a/acceptance/acceptance_test.go b/acceptance/acceptance_test.go index f1fff68e..dc6ac485 100644 --- a/acceptance/acceptance_test.go +++ b/acceptance/acceptance_test.go @@ -176,7 +176,7 @@ func skipUnlessExtEnabled() acceptanceRunnerOpt { } } -// acceptanceRunner is the Enos CLI acceptance test runner +// acceptanceRunner is the Enos CLI acceptance test runner. type acceptanceRunner struct { enosBinPath string tfBinPath string @@ -187,11 +187,11 @@ type acceptanceRunner struct { skipUnlessExtEnabled bool } -// run runs an Enos sub-command +// run runs an Enos sub-command. func (r *acceptanceRunner) run(ctx context.Context, subCommand string) ([]byte, error) { path, err := filepath.Abs(r.enosBinPath) if err != nil { - return nil, nil + return nil, err } cmdParts := strings.Split(subCommand, " ") @@ -200,6 +200,7 @@ func (r *acceptanceRunner) run(ctx context.Context, subCommand string) ([]byte, cmd := exec.CommandContext(ctx, path, cmdParts...) cmd.Env = os.Environ() + return cmd.CombinedOutput() } @@ -234,6 +235,8 @@ func sortResponses(r []*pb.Operation_Response) { } func requireEqualOperationResponses(t *testing.T, expected *pb.OperationResponses, out []byte) { + t.Helper() + got := &pb.OperationResponses{} require.NoErrorf(t, protojson.Unmarshal(out, got), string(out)) require.Len(t, got.GetResponses(), len(expected.GetResponses())) @@ -294,6 +297,8 @@ func requireEqualOperationResponses(t *testing.T, expected *pb.OperationResponse } func requireEqualGenerateResponse(t *testing.T, expected, got *pb.Operation_Response_Generate) { + t.Helper() + if expected.GetTerraformModule().GetModulePath() != "" { require.Equal(t, expected.GetTerraformModule().GetModulePath(), got.GetTerraformModule().GetModulePath(), @@ -310,33 +315,45 @@ func requireEqualGenerateResponse(t *testing.T, expected, got *pb.Operation_Resp } func requireEqualInitReponse(t *testing.T, expected, got *pb.Terraform_Command_Init_Response) { + t.Helper() + require.Equal(t, expected.GetStderr(), got.GetStderr()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) } func requireEqualValidate(t *testing.T, expected, got *pb.Terraform_Command_Validate_Response) { + t.Helper() + require.Equal(t, expected.GetValid(), got.GetValid()) require.Equal(t, expected.GetWarningCount(), got.GetWarningCount()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) } func requireEqualPlan(t *testing.T, expected, got *pb.Terraform_Command_Plan_Response) { + t.Helper() + require.Equal(t, expected.GetChangesPresent(), got.GetChangesPresent()) require.Equal(t, expected.GetStderr(), got.GetStderr()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) } func requireEqualApply(t *testing.T, expected, got *pb.Terraform_Command_Apply_Response) { + t.Helper() + require.Equal(t, expected.GetStderr(), got.GetStderr()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) } func requireEqualDestroy(t *testing.T, expected, got *pb.Terraform_Command_Destroy_Response) { + t.Helper() + require.Equal(t, expected.GetStderr(), got.GetStderr()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) } func requireEqualOutput(t *testing.T, expected, got *pb.Terraform_Command_Output_Response) { + t.Helper() + require.Len(t, expected.GetMeta(), len(got.GetMeta())) for i := range expected.GetMeta() { require.Equal(t, expected.GetMeta()[i].GetName(), got.GetMeta()[i].GetName()) @@ -350,6 +367,8 @@ func requireEqualOutput(t *testing.T, expected, got *pb.Terraform_Command_Output } func requireEqualExec(t *testing.T, expected, got *pb.Terraform_Command_Exec_Response) { + t.Helper() + require.Equal(t, expected.GetSubCommand(), got.GetSubCommand()) require.Len(t, expected.GetDiagnostics(), len(got.GetDiagnostics())) // NOTE: we don't check stderr since anything we could test would be brittle diff --git a/acceptance/fmt_test.go b/acceptance/fmt_test.go index 857e0dce..9492981d 100644 --- a/acceptance/fmt_test.go +++ b/acceptance/fmt_test.go @@ -15,6 +15,8 @@ import ( ) func TestAcc_Cmd_Fmt(t *testing.T) { + t.Parallel() + enos := newAcceptanceRunner(t) path, err := filepath.Abs("./scenarios/scenario_generate_pass_0") diff --git a/acceptance/scenario_check_test.go b/acceptance/scenario_check_test.go index fb107aea..ad49f6f7 100644 --- a/acceptance/scenario_check_test.go +++ b/acceptance/scenario_check_test.go @@ -15,11 +15,7 @@ import ( // TestAcc_Cmd_Scenario_Check tests that a scenario can be checked with Terraform. func TestAcc_Cmd_Scenario_Check(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.check") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -38,7 +34,14 @@ func TestAcc_Cmd_Scenario_Check(t *testing.T) { [][]string{{"skip", "keep"}, {"skip", "skip"}}, }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + tmpDir, err := os.MkdirTemp("/tmp", "enos.check") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) outDir := filepath.Join(tmpDir, test.dir) err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) @@ -108,6 +111,8 @@ func TestAcc_Cmd_Scenario_Check(t *testing.T) { // that has warnings can be validated and that the program behaves as it should // when given the --fail-on-warnings flag. func TestAcc_Cmd_Scenario_Check_WithWarnings(t *testing.T) { + t.Parallel() + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI(), skipUnlessExtEnabled(), // since we need the random provider @@ -118,7 +123,9 @@ func TestAcc_Cmd_Scenario_Check_WithWarnings(t *testing.T) { t.Cleanup(func() { os.RemoveAll(tmpDir) }) for _, failOnWarnings := range []bool{true, false} { + failOnWarnings := failOnWarnings t.Run(fmt.Sprintf("fail_on_warnings_%t", failOnWarnings), func(t *testing.T) { + t.Parallel() outDir := filepath.Join(tmpDir, "scenario_generate_has_warnings") err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) @@ -134,6 +141,7 @@ func TestAcc_Cmd_Scenario_Check_WithWarnings(t *testing.T) { out, err := enos.run(context.Background(), cmd) if failOnWarnings { require.Error(t, err, string(out)) + return } require.NoError(t, err, string(out)) diff --git a/acceptance/scenario_destroy_test.go b/acceptance/scenario_destroy_test.go index 7a15c37a..3e20d2ae 100644 --- a/acceptance/scenario_destroy_test.go +++ b/acceptance/scenario_destroy_test.go @@ -16,11 +16,7 @@ import ( // TestAcc_Cmd_Scenario_Destroy tests that a scenario can be generated and validated // with Terraform. func TestAcc_Cmd_Scenario_Destroy(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.destroy") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -45,9 +41,15 @@ func TestAcc_Cmd_Scenario_Destroy(t *testing.T) { false, }, } { + test := test t.Run(fmt.Sprintf("%s %s %s %t", test.dir, test.name, test.variants, test.launch), func(t *testing.T) { + t.Parallel() + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + tmpDir, err := os.MkdirTemp("/tmp", "enos.destroy") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_e2e_aws_test.go b/acceptance/scenario_e2e_aws_test.go index 264be5a6..05118e59 100644 --- a/acceptance/scenario_e2e_aws_test.go +++ b/acceptance/scenario_e2e_aws_test.go @@ -14,18 +14,9 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// TestAcc_Cmd_Scenario_E2E_AWS does an end-to-end test with AWS +// TestAcc_Cmd_Scenario_E2E_AWS does an end-to-end test with AWS. func TestAcc_Cmd_Scenario_E2E_AWS(t *testing.T) { - enos := newAcceptanceRunner(t, - skipUnlessTerraformCLI(), - skipUnlessAWSCredentials(), - skipUnlessEnosPrivateKey(), - skipUnlessExtEnabled(), - ) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.aws.e2e") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -53,9 +44,22 @@ func TestAcc_Cmd_Scenario_E2E_AWS(t *testing.T) { }, }, } { + test := test t.Run(test.dir, func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, + skipUnlessTerraformCLI(), + skipUnlessAWSCredentials(), + skipUnlessEnosPrivateKey(), + skipUnlessExtEnabled(), + ) + + tmpDir, err := os.MkdirTemp("/tmp", "enos.aws.e2e") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_exec_test.go b/acceptance/scenario_exec_test.go index bd4b1e93..3ec3ac75 100644 --- a/acceptance/scenario_exec_test.go +++ b/acceptance/scenario_exec_test.go @@ -16,11 +16,7 @@ import ( // TestAcc_Cmd_Scenario_Exec tests that a raw Terrform command can be passed // to a scenario's Terraform. func TestAcc_Cmd_Scenario_Exec(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.exec") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -42,9 +38,18 @@ func TestAcc_Cmd_Scenario_Exec(t *testing.T) { fmt.Sprintf("%x", sha256.Sum256([]byte("test [foo:matrixbar]"))), }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + + tmpDir, err := os.MkdirTemp("/tmp", "enos.exec") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_generate_test.go b/acceptance/scenario_generate_test.go index e054bf76..b17f1c00 100644 --- a/acceptance/scenario_generate_test.go +++ b/acceptance/scenario_generate_test.go @@ -16,11 +16,7 @@ import ( // TestAcc_Cmd_Scenario_Generate tests that a scenario can generate into the // appropriate terraform module and CLI configuration. func TestAcc_Cmd_Scenario_Generate(t *testing.T) { - enos := newAcceptanceRunner(t) - - tmpDir, err := os.MkdirTemp("", "enos.generate.out") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -72,9 +68,17 @@ func TestAcc_Cmd_Scenario_Generate(t *testing.T) { fmt.Sprintf("%x", sha256.Sum256([]byte("kubernetes"))), }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t) + + tmpDir, err := os.MkdirTemp("", "enos.generate.out") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_launch_test.go b/acceptance/scenario_launch_test.go index f3afa784..33df08ac 100644 --- a/acceptance/scenario_launch_test.go +++ b/acceptance/scenario_launch_test.go @@ -16,11 +16,7 @@ import ( // TestAcc_Cmd_Scenario_Launch tests that a scenario can be generated and validated // with Terraform. func TestAcc_Cmd_Scenario_Launch(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.launch") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -42,9 +38,17 @@ func TestAcc_Cmd_Scenario_Launch(t *testing.T) { fmt.Sprintf("%x", sha256.Sum256([]byte("test [foo:matrixbar]"))), }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + + tmpDir, err := os.MkdirTemp("/tmp", "enos.launch") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_list_test.go b/acceptance/scenario_list_test.go index bc8ed547..47975c19 100644 --- a/acceptance/scenario_list_test.go +++ b/acceptance/scenario_list_test.go @@ -13,8 +13,7 @@ import ( ) func TestAcc_Cmd_Scenario_List(t *testing.T) { - enos := newAcceptanceRunner(t) - + t.Parallel() for _, test := range []struct { dir string out *pb.ListScenariosResponse @@ -97,7 +96,11 @@ func TestAcc_Cmd_Scenario_List(t *testing.T) { fail: true, }, } { + test := test t.Run(test.dir, func(t *testing.T) { + t.Parallel() + enos := newAcceptanceRunner(t) + path, err := filepath.Abs(filepath.Join("./scenarios", test.dir)) require.NoError(t, err) cmd := fmt.Sprintf("scenario list --chdir %s --format json", path) @@ -105,6 +108,7 @@ func TestAcc_Cmd_Scenario_List(t *testing.T) { out, err := enos.run(context.Background(), cmd) if test.fail { require.Error(t, err) + return } diff --git a/acceptance/scenario_output_test.go b/acceptance/scenario_output_test.go index 178a2075..7c7d819e 100644 --- a/acceptance/scenario_output_test.go +++ b/acceptance/scenario_output_test.go @@ -13,13 +13,9 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// TestAcc_Cmd_Scenario_Output tests that a Terraform output command succeeds +// TestAcc_Cmd_Scenario_Output tests that a Terraform output command succeeds. func TestAcc_Cmd_Scenario_Output(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.exec") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -35,9 +31,18 @@ func TestAcc_Cmd_Scenario_Output(t *testing.T) { fmt.Sprintf("%x", sha256.Sum256([]byte("step_vars [arch:arm distro:rhel]"))), }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + + tmpDir, err := os.MkdirTemp("/tmp", "enos.exec") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_run_test.go b/acceptance/scenario_run_test.go index ef15999e..333cc131 100644 --- a/acceptance/scenario_run_test.go +++ b/acceptance/scenario_run_test.go @@ -16,11 +16,7 @@ import ( // TestAcc_Cmd_Scenario_Run tests that a scenario can be generated and validated // with Terraform. func TestAcc_Cmd_Scenario_Run(t *testing.T) { - enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) - - tmpDir, err := os.MkdirTemp("/tmp", "enos.launch") - require.NoError(t, err) - t.Cleanup(func() { os.RemoveAll(tmpDir) }) + t.Parallel() for _, test := range []struct { dir string @@ -42,9 +38,18 @@ func TestAcc_Cmd_Scenario_Run(t *testing.T) { fmt.Sprintf("%x", sha256.Sum256([]byte("test [foo:matrixbar]"))), }, } { + test := test t.Run(fmt.Sprintf("%s %s %s", test.dir, test.name, test.variants), func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t, skipUnlessTerraformCLI()) + + tmpDir, err := os.MkdirTemp("/tmp", "enos.launch") + require.NoError(t, err) + t.Cleanup(func() { os.RemoveAll(tmpDir) }) + outDir := filepath.Join(tmpDir, test.dir) - err := os.MkdirAll(outDir, 0o755) + err = os.MkdirAll(outDir, 0o755) require.NoError(t, err) outDir, err = filepath.EvalSymlinks(outDir) require.NoError(t, err) diff --git a/acceptance/scenario_validate_test.go b/acceptance/scenario_validate_test.go index 556dbfe4..c8fbe397 100644 --- a/acceptance/scenario_validate_test.go +++ b/acceptance/scenario_validate_test.go @@ -13,7 +13,7 @@ import ( ) func TestAcc_Cmd_Scenario_Validate(t *testing.T) { - enos := newAcceptanceRunner(t) + t.Parallel() for _, test := range []struct { dir string @@ -29,7 +29,10 @@ func TestAcc_Cmd_Scenario_Validate(t *testing.T) { fail: true, }, } { + test := test t.Run(test.dir, func(t *testing.T) { + t.Parallel() + enos := newAcceptanceRunner(t) path, err := filepath.Abs(filepath.Join("./scenarios", test.dir)) require.NoError(t, err) cmd := fmt.Sprintf("scenario validate --chdir %s --format json", path) @@ -37,6 +40,7 @@ func TestAcc_Cmd_Scenario_Validate(t *testing.T) { out, err := enos.run(context.Background(), cmd) if test.fail { require.Error(t, err) + return } diff --git a/acceptance/version_test.go b/acceptance/version_test.go index 8a8d878c..f594a2d0 100644 --- a/acceptance/version_test.go +++ b/acceptance/version_test.go @@ -9,7 +9,7 @@ import ( ) func TestAcc_Cmd_Version(t *testing.T) { - enos := newAcceptanceRunner(t) + t.Parallel() for _, test := range []struct { cmd string @@ -25,8 +25,14 @@ func TestAcc_Cmd_Version(t *testing.T) { out: regexp.MustCompile(`Enos version: \d*\.\d*\.\d* sha: \w*`), }, } { - out, err := enos.run(context.Background(), test.cmd) - require.NoError(t, err) - require.True(t, test.out.Match(out)) + test := test + t.Run(test.cmd, func(t *testing.T) { + t.Parallel() + + enos := newAcceptanceRunner(t) + out, err := enos.run(context.Background(), test.cmd) + require.NoError(t, err) + require.True(t, test.out.Match(out)) + }) } } diff --git a/command/enos/main.go b/command/enos/main.go index cb068fac..c199f8b6 100644 --- a/command/enos/main.go +++ b/command/enos/main.go @@ -1,13 +1,9 @@ package main import ( - "math/rand" - "time" - "github.com/hashicorp/enos/internal/command/enos/cmd" ) func main() { - rand.Seed(time.Now().UnixNano()) cmd.Execute() } diff --git a/go.mod b/go.mod index 08da6370..64d9aadf 100644 --- a/go.mod +++ b/go.mod @@ -19,6 +19,7 @@ require ( github.com/spf13/cobra v1.7.0 github.com/stretchr/testify v1.8.4 github.com/zclconf/go-cty v1.13.2 + golang.org/x/net v0.12.0 golang.org/x/term v0.10.0 golang.org/x/text v0.11.0 google.golang.org/grpc v1.56.2 @@ -54,7 +55,7 @@ require ( github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/mattn/go-colorable v0.1.13 // indirect github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.14 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect @@ -64,8 +65,7 @@ require ( github.com/spf13/cast v1.5.1 // indirect github.com/spf13/pflag v1.0.5 // indirect golang.org/x/crypto v0.11.0 // indirect - golang.org/x/net v0.12.0 // indirect golang.org/x/sys v0.10.0 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230717213848-3f92550aa753 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 66577b56..2c6fe738 100644 --- a/go.sum +++ b/go.sum @@ -106,6 +106,7 @@ github.com/kevinburke/ssh_config v0.0.0-20201106050909-4977a11b4351 h1:DowS9hvgy github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= @@ -118,8 +119,8 @@ github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/ github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.14 h1:+xnbZSEeDbOIg5/mE6JF0w6n9duR1l3/WmbinWVwUuU= -github.com/mattn/go-runewidth v0.0.14/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mitchellh/cli v1.1.5 h1:OxRIeJXpAMztws/XHlN2vu6imG5Dpq+j61AzAX5fLng= github.com/mitchellh/cli v1.1.5/go.mod h1:v8+iFts2sPIKUV1ltktPXMCC8fumSKFItNcD2cLtRR4= github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ= @@ -145,6 +146,7 @@ github.com/rivo/uniseg v0.4.4 h1:8TfxU8dW6PdqD27gjM8MVNuicgxIjxpm4K7x4jp8sis= github.com/rivo/uniseg v0.4.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sebdah/goldie v1.0.0/go.mod h1:jXP4hmWywNEwZzhMuv2ccnqTSFpuq8iyQhtQdkkZBH4= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= @@ -169,6 +171,7 @@ github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5t github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20200414173820-0848c9571904/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -219,8 +222,8 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230717213848-3f92550aa753 h1:XUODHrpzJEUeWmVo/jfNTLj0YyVveOo28oE6vkFbkO4= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230717213848-3f92550aa753/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895 h1:co8AMhI481nhd3WBfW2mq5msyQHNBcGn7G9GCEqz45k= +google.golang.org/genproto/googleapis/rpc v0.0.0-20230725213213-b022f6e96895/go.mod h1:TUfxEVdsvPg18p6AslUXFoLdpED4oBnGwyqk3dV1XzM= google.golang.org/grpc v1.56.2 h1:fVRFRnXvU+x6C4IlHZewvJOVHoOv1TUuQyoRsYnB4bI= google.golang.org/grpc v1.56.2/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/internal/client/client.go b/internal/client/client.go index 93e79e33..317fd339 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -11,12 +11,11 @@ import ( "google.golang.org/grpc/credentials/insecure" "google.golang.org/grpc/keepalive" - "github.com/hashicorp/enos/internal/server" "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" "github.com/hashicorp/go-hclog" ) -// Connection is a client connection to the enos server +// Connection is a client connection to the enos server. type Connection struct { Addr net.Addr Client pb.EnosServiceClient @@ -24,42 +23,62 @@ type Connection struct { Level pb.UI_Settings_Level } -// Opt is a client connection option +// Opt is a client connection option. type Opt func(*Connection) error -// WithGRPCListenURL sets the listener address from a URL +// WithGRPCListenURL sets the listener address from a URL. func WithGRPCListenURL(url *url.URL) Opt { return func(c *Connection) error { var err error - c.Addr, err = server.ListenAddr(url) + switch url.Scheme { + case "unix", "unixpacket": + c.Addr, err = net.ResolveUnixAddr(url.Scheme, url.Host) + default: + addr := url.Host + if p := url.Port(); p != "" { + addr = fmt.Sprintf("%s:%s", addr, p) + } + c.Addr, err = net.ResolveTCPAddr("tcp", addr) + } + return err } } -// WithLogger sets client logger +func WithGRPCListenAddr(addr net.Addr) Opt { + return func(c *Connection) error { + c.Addr = addr + + return nil + } +} + +// WithLogger sets client logger. func WithLogger(log hclog.Logger) Opt { return func(c *Connection) error { c.Log = log + return nil } } -// WithLogLevel sets client log level +// WithLogLevel sets client log level. func WithLogLevel(lvl pb.UI_Settings_Level) Opt { return func(c *Connection) error { c.Level = lvl + return nil } } -// Trace writes an hclog.Logger style message at a "trace" level +// Trace writes an hclog.Logger style message at a "trace" level. func (c *Connection) Trace(msg string, args ...any) { if c.Level == pb.UI_Settings_LEVEL_TRACE { c.Log.Debug(msg, args...) } } -// Connect takes a context and options and returns a new connection +// Connect takes a context and options and returns a new connection. func Connect(ctx context.Context, opts ...Opt) (*Connection, error) { c := &Connection{ Log: hclog.NewNullLogger().Named("client"), diff --git a/internal/client/operation_stream.go b/internal/client/operation_stream.go index b076ee12..f13d43fd 100644 --- a/internal/client/operation_stream.go +++ b/internal/client/operation_stream.go @@ -38,11 +38,7 @@ func (c *Connection) StreamOperations( return res } - var err error - res.Responses, err = c.streamResponses(ctx, opRes.GetOperations(), ui) - if err != nil { - res.Diagnostics = append(res.GetDiagnostics(), diagnostics.FromErr(err)...) - } + res.Responses = c.streamResponses(ctx, opRes.GetOperations(), ui) return res } @@ -54,10 +50,7 @@ func (c *Connection) streamResponses( ctx context.Context, refs []*pb.Ref_Operation, ui uipkg.View, -) ( - []*pb.Operation_Response, - error, -) { +) []*pb.Operation_Response { mu := sync.Mutex{} wg := sync.WaitGroup{} res := []*pb.Operation_Response{} @@ -86,6 +79,7 @@ func (c *Connection) streamResponses( Op: ref, }) mu.Unlock() + return } @@ -100,6 +94,7 @@ func (c *Connection) streamResponses( eventRes, err := stream.Recv() if err != nil { errC <- err + return } @@ -127,6 +122,7 @@ func (c *Connection) streamResponses( ) } } + break LOOP case <-ticker.C: if lastEvent != nil { @@ -173,5 +169,5 @@ func (c *Connection) streamResponses( wg.Wait() - return res, nil + return res } diff --git a/internal/command/enos/cmd/fmt.go b/internal/command/enos/cmd/fmt.go index e7adf258..5ce47168 100644 --- a/internal/command/enos/cmd/fmt.go +++ b/internal/command/enos/cmd/fmt.go @@ -133,6 +133,7 @@ func runFmtCmd(cmd *cobra.Command, args []string) error { bytes, err := io.ReadAll(cmd.InOrStdin()) if err != nil { res.Diagnostics = diagnostics.FromErr(err) + return ui.ShowFormat(fmtCfg, res) } req.Files = []*pb.FormatRequest_File{ diff --git a/internal/command/enos/cmd/root.go b/internal/command/enos/cmd/root.go index db0f84d8..ce45cbe2 100644 --- a/internal/command/enos/cmd/root.go +++ b/internal/command/enos/cmd/root.go @@ -53,7 +53,7 @@ var rootState = &rootStateS{ // the view. var ui uipkg.View -// Execute executes enos +// Execute executes enos. func Execute() { rootCmd.AddCommand(newVersionCmd()) rootCmd.AddCommand(newScenarioCmd()) @@ -193,6 +193,9 @@ func rootCmdPreRun(cmd *cobra.Command, args []string) error { return err } + // If we're this far they've given use valid usage and we'll handle it + cmd.SilenceUsage = true + // Create our gRPC server and client rootState.enosServer, rootState.enosConnection, err = startServer( context.Background(), @@ -202,9 +205,6 @@ func rootCmdPreRun(cmd *cobra.Command, args []string) error { return err } - // If we're this far they've given use valid usage and we'll handle it - cmd.SilenceUsage = true - return err } diff --git a/internal/command/enos/cmd/scenario.go b/internal/command/enos/cmd/scenario.go index ef53883b..3eded222 100644 --- a/internal/command/enos/cmd/scenario.go +++ b/internal/command/enos/cmd/scenario.go @@ -18,7 +18,7 @@ import ( "github.com/hashicorp/hcl/v2" ) -// scenarioConfig is the 'scenario' sub-command configuration type +// scenarioConfig is the 'scenario' sub-command configuration type. type scenarioStateType struct { baseDir string outDir string @@ -30,12 +30,12 @@ type scenarioStateType struct { varsFilesPaths []string } -// scenarioState is the 'scenario' sub-command configuration +// scenarioState is the 'scenario' sub-command configuration. var scenarioState = scenarioStateType{ tfConfig: terraform.NewConfig(), } -// scenarioFilterDesc scenario sub-command filter description +// scenarioFilterDesc scenario sub-command filter description. var scenarioFilterDesc = ` A SCENARIO FILTER or FILTER must be a single string value which @@ -51,7 +51,7 @@ VARIANT SUBFILTER = '[!]KEY:PATTERN|WILDCARD|ABSOLUTE' FILTER = '[SCENARIO NAME] [...VARIANT SUBFILTER]'` -// newScenarioCmd returns a new instance of the 'scenario' sub-command +// newScenarioCmd returns a new instance of the 'scenario' sub-command. func newScenarioCmd() *cobra.Command { scenarioCmd := &cobra.Command{ Use: "scenario", @@ -99,6 +99,7 @@ func scenarioCmdPreRun(cmd *cobra.Command, args []string) error { // Load the configuration from our working dir scenarioState.protoFp, err = readFlightPlanConfig(scenarioState.baseDir, scenarioState.varsFilesPaths) + return err } @@ -108,7 +109,7 @@ func scenarioCmdPostRun(cmd *cobra.Command, args []string) { rootCmdPostRun(cmd, args) } -// setupDefaultScenarioCfg sets up default scenario configuration +// setupDefaultScenarioCfg sets up default scenario configuration. func setupDefaultScenarioCfg() error { var err error @@ -134,8 +135,8 @@ func setupDefaultScenarioCfg() error { return nil } -// decodeFlightPlan decodes the flight plan -func decodeFlightPlan(cmd *cobra.Command) error { +// decodeFlightPlan decodes the flight plan. +func decodeFlightPlan(ctx context.Context, cmd *cobra.Command) error { diags := hcl.Diagnostics{} pfp, err := readFlightPlanConfig(scenarioState.baseDir, scenarioState.varsFilesPaths) @@ -164,7 +165,7 @@ func decodeFlightPlan(cmd *cobra.Command) error { } } - fp, moreDiags := decoder.Decode() + fp, moreDiags := decoder.Decode(ctx) diags = diags.Extend(moreDiags) scenarioState.fp = fp @@ -189,7 +190,9 @@ func scenarioNameCompletion(cmd *cobra.Command, args []string, toComplete string return nil, cobra.ShellCompDirectiveNoFileComp } - err := decodeFlightPlan(cmd) + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + err := decodeFlightPlan(ctx, cmd) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -262,6 +265,7 @@ func prepareScenarioOpReq( Diagnostics: diagnostics.FromErr(err), Value: &pb.Operation_Event_Decode{}, }) + return nil, nil, err } diff --git a/internal/command/enos/cmd/scenario_check.go b/internal/command/enos/cmd/scenario_check.go index 6ba44a7a..cc09e71e 100644 --- a/internal/command/enos/cmd/scenario_check.go +++ b/internal/command/enos/cmd/scenario_check.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioCheckCmd returns a new 'scenario check' sub-command +// newScenarioCheckCmd returns a new 'scenario check' sub-command. func newScenarioCheckCmd() *cobra.Command { cmd := &cobra.Command{ Use: "check [FILTER]", @@ -34,7 +34,7 @@ func newScenarioCheckCmd() *cobra.Command { return cmd } -// runScenarioCheckCmd is the function that checks scenarios +// runScenarioCheckCmd is the function that checks scenarios. func runScenarioCheckCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_destroy.go b/internal/command/enos/cmd/scenario_destroy.go index ae4d79de..81d723d0 100644 --- a/internal/command/enos/cmd/scenario_destroy.go +++ b/internal/command/enos/cmd/scenario_destroy.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioDestroyCmd returns a new `scenario destroy` sub-command +// newScenarioDestroyCmd returns a new `scenario destroy` sub-command. func newScenarioDestroyCmd() *cobra.Command { cmd := &cobra.Command{ Use: "destroy [FILTER]", @@ -32,7 +32,7 @@ func newScenarioDestroyCmd() *cobra.Command { return cmd } -// runScenarioDestroyCmd is the function that destroys scenarios +// runScenarioDestroyCmd is the function that destroys scenarios. func runScenarioDestroyCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_exec.go b/internal/command/enos/cmd/scenario_exec.go index b631ef12..7579b558 100644 --- a/internal/command/enos/cmd/scenario_exec.go +++ b/internal/command/enos/cmd/scenario_exec.go @@ -25,7 +25,7 @@ func newScenarioExecCmd() *cobra.Command { return cmd } -// runScenarioExecCmd is the function that launchs scenarios +// runScenarioExecCmd is the function that launchs scenarios. func runScenarioExecCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_generate.go b/internal/command/enos/cmd/scenario_generate.go index 1008c696..3be1aead 100644 --- a/internal/command/enos/cmd/scenario_generate.go +++ b/internal/command/enos/cmd/scenario_generate.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioGenerateCmd returns a new 'scenario generate' sub-command +// newScenarioGenerateCmd returns a new 'scenario generate' sub-command. func newScenarioGenerateCmd() *cobra.Command { cmd := &cobra.Command{ Use: "generate [FILTER]", @@ -22,7 +22,7 @@ func newScenarioGenerateCmd() *cobra.Command { return cmd } -// runScenarioGenerateCmd is the function that generates scenarios +// runScenarioGenerateCmd is the function that generates scenarios. func runScenarioGenerateCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_launch.go b/internal/command/enos/cmd/scenario_launch.go index 72954d56..402508c2 100644 --- a/internal/command/enos/cmd/scenario_launch.go +++ b/internal/command/enos/cmd/scenario_launch.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioLaunchCmd returns a new 'scenario run' command +// newScenarioLaunchCmd returns a new 'scenario run' command. func newScenarioLaunchCmd() *cobra.Command { cmd := &cobra.Command{ Use: "launch [FILTER]", @@ -34,7 +34,7 @@ func newScenarioLaunchCmd() *cobra.Command { return cmd } -// runScenarioLaunchCmd is the function that launches scenarios +// runScenarioLaunchCmd is the function that launches scenarios. func runScenarioLaunchCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_list.go b/internal/command/enos/cmd/scenario_list.go index fd96ff07..5b15fa7c 100644 --- a/internal/command/enos/cmd/scenario_list.go +++ b/internal/command/enos/cmd/scenario_list.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioListCmd returns a new 'scenario list' sub-command +// newScenarioListCmd returns a new 'scenario list' sub-command. func newScenarioListCmd() *cobra.Command { return &cobra.Command{ Use: "list [FILTER]", @@ -19,7 +19,7 @@ func newScenarioListCmd() *cobra.Command { } } -// runScenarioListCmd runs a scenario list +// runScenarioListCmd runs a scenario list. func runScenarioListCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_output.go b/internal/command/enos/cmd/scenario_output.go index dd4570be..e0db8abe 100644 --- a/internal/command/enos/cmd/scenario_output.go +++ b/internal/command/enos/cmd/scenario_output.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/go-multierror" ) -// newScenarioOutputCmd returns a new 'scenario output' command +// newScenarioOutputCmd returns a new 'scenario output' command. func newScenarioOutputCmd() *cobra.Command { cmd := &cobra.Command{ Use: "output [FILTER]", @@ -26,7 +26,7 @@ func newScenarioOutputCmd() *cobra.Command { return cmd } -// runScenarioOutputCmd is the function that returns scenario output +// runScenarioOutputCmd is the function that returns scenario output. func runScenarioOutputCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_run.go b/internal/command/enos/cmd/scenario_run.go index 6641bec7..65e0084b 100644 --- a/internal/command/enos/cmd/scenario_run.go +++ b/internal/command/enos/cmd/scenario_run.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// newScenarioRunCmd returns a new 'scenario run' sub-command +// newScenarioRunCmd returns a new 'scenario run' sub-command. func newScenarioRunCmd() *cobra.Command { cmd := &cobra.Command{ Use: "run [FILTER]", @@ -34,7 +34,7 @@ func newScenarioRunCmd() *cobra.Command { return cmd } -// runScenarioRunCmd is the function that runs scenarios +// runScenarioRunCmd is the function that runs scenarios. func runScenarioRunCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/scenario_validate_config.go b/internal/command/enos/cmd/scenario_validate_config.go index 2bac0b84..63f1d520 100644 --- a/internal/command/enos/cmd/scenario_validate_config.go +++ b/internal/command/enos/cmd/scenario_validate_config.go @@ -18,7 +18,7 @@ func newScenarioValidateConfigCmd() *cobra.Command { } } -// runScenarioValidateCfgCmd is the function that validates all flight plan configuration +// runScenarioValidateCfgCmd is the function that validates all flight plan configuration. func runScenarioValidateCfgCmd(cmd *cobra.Command, args []string) error { ctx, cancel := scenarioTimeoutContext() defer cancel() diff --git a/internal/command/enos/cmd/server.go b/internal/command/enos/cmd/server.go index a4524ebf..aafadcfd 100644 --- a/internal/command/enos/cmd/server.go +++ b/internal/command/enos/cmd/server.go @@ -68,40 +68,29 @@ func startServer( return nil, nil, err } - resolvedURL := &url.URL{Host: cfg.ListenAddr.String()} - - switch cfg.ListenAddr.Network() { - case "tcp", "tcp4", "tcp6": - resolvedURL.Scheme = "http" - case "unix", "unixpacket": - resolvedURL.Scheme = "unix" - default: - return nil, nil, fmt.Errorf("unable to determine listen network from address: %s", cfg.ListenAddr.String()) - } - - waitCtx, cancel := context.WithTimeout(context.Background(), timeout) + waitCtx, cancel := context.WithTimeout(ctx, timeout) defer cancel() ticker := time.NewTicker(2 * time.Millisecond) for { select { case <-waitCtx.Done(): - return nil, nil, fmt.Errorf("waiting for server to start: %w", err) + return nil, nil, fmt.Errorf("waiting for server to start on address %s: %w", cfg.ListenAddr.String(), err) default: } select { case <-waitCtx.Done(): - return nil, nil, fmt.Errorf("waiting for server to start: %w", err) + return nil, nil, fmt.Errorf("waiting for server to start on address %s: %w", cfg.ListenAddr.String(), err) case <-ticker.C: - ctx, cancel := context.WithTimeout( - context.Background(), - 1*time.Millisecond, + connCtx, cancel := context.WithTimeout( + ctx, + 50*time.Millisecond, ) defer cancel() var enosConnection *client.Connection - enosConnection, err = client.Connect(ctx, - client.WithGRPCListenURL(resolvedURL), + enosConnection, err = client.Connect(connCtx, + client.WithGRPCListenAddr(cfg.ListenAddr), client.WithLogger(clientLog), ) if err == nil { diff --git a/internal/diagnostics/diagnostics.go b/internal/diagnostics/diagnostics.go index b67a2ef4..fe10d094 100644 --- a/internal/diagnostics/diagnostics.go +++ b/internal/diagnostics/diagnostics.go @@ -67,7 +67,7 @@ func hasSeverity(sev pb.Diagnostic_Severity, diags ...[]*pb.Diagnostic) bool { return false } -// Concat takes one-or-more sets of daignostics and returns a combined set +// Concat takes one-or-more sets of daignostics and returns a combined set. func Concat(diags ...[]*pb.Diagnostic) []*pb.Diagnostic { combined := []*pb.Diagnostic{} for _, diag := range diags { @@ -77,7 +77,7 @@ func Concat(diags ...[]*pb.Diagnostic) []*pb.Diagnostic { return combined } -// FromErr takes a standard go error and returns proto diagnostics +// FromErr takes a standard go error and returns proto diagnostics. func FromErr(err error) []*pb.Diagnostic { if err == nil { return nil @@ -89,7 +89,7 @@ func FromErr(err error) []*pb.Diagnostic { }} } -// FromTFJSON takes terraform-json Diagnostics and returns them as proto diagnostics +// FromTFJSON takes terraform-json Diagnostics and returns them as proto diagnostics. func FromTFJSON(in []tfjson.Diagnostic) []*pb.Diagnostic { if len(in) < 1 { return nil @@ -107,6 +107,8 @@ func FromTFJSON(in []tfjson.Diagnostic) []*pb.Diagnostic { d.Severity = pb.Diagnostic_SEVERITY_ERROR case tfjson.DiagnosticSeverityWarning: d.Severity = pb.Diagnostic_SEVERITY_WARNING + case tfjson.DiagnosticSeverityUnknown: + d.Severity = pb.Diagnostic_SEVERITY_UNKNOWN default: d.Severity = pb.Diagnostic_SEVERITY_UNKNOWN } @@ -163,6 +165,8 @@ func FromTFJSON(in []tfjson.Diagnostic) []*pb.Diagnostic { // FromHCL takes a map of hcl.Files and hcl.Diagnostics and returns pb diagnostics. // When possible it will attempt to create a valid snippet. +// +//nolint:gocylo,cyclop // converting snippets from HCL to our wire format is complexity we can't avoid func FromHCL(files map[string]*hcl.File, diags hcl.Diagnostics) []*pb.Diagnostic { if len(diags) < 1 { return nil @@ -180,6 +184,8 @@ func FromHCL(files map[string]*hcl.File, diags hcl.Diagnostics) []*pb.Diagnostic pbDiag.Severity = pb.Diagnostic_SEVERITY_ERROR case hcl.DiagWarning: pbDiag.Severity = pb.Diagnostic_SEVERITY_WARNING + case hcl.DiagInvalid: + pbDiag.Severity = pb.Diagnostic_SEVERITY_UNKNOWN default: pbDiag.Severity = pb.Diagnostic_SEVERITY_UNKNOWN } @@ -289,6 +295,7 @@ func FromHCL(files map[string]*hcl.File, diags hcl.Diagnostics) []*pb.Diagnostic // already have the same error in our diagnostics set // already. traversal = traversal[:len(traversal)-1] + continue } @@ -333,24 +340,24 @@ type stringOptConfig struct { uiSettings *pb.UI_Settings } -// StringOpt is an option to the string formatter +// StringOpt is an option to the string formatter. type StringOpt func(*stringOptConfig) -// WithStringSnippetEnabled enables or diables the snippet in the formatting +// WithStringSnippetEnabled enables or diables the snippet in the formatting. func WithStringSnippetEnabled(enabled bool) StringOpt { return func(cfg *stringOptConfig) { cfg.showSnippet = enabled } } -// WithStringUISettings passes UI settings to the string formatter +// WithStringUISettings passes UI settings to the string formatter. func WithStringUISettings(settings *pb.UI_Settings) StringOpt { return func(cfg *stringOptConfig) { cfg.uiSettings = settings } } -// WithStringColor passes color settings to the formatter +// WithStringColor passes color settings to the formatter. func WithStringColor(color *colorstring.Colorize) StringOpt { return func(cfg *stringOptConfig) { cfg.color = color @@ -399,6 +406,8 @@ func String(diag *pb.Diagnostic, opts ...StringOpt) string { leftRuleStart = cfg.color.Color("[yellow]╷[reset]") leftRuleEnd = cfg.color.Color("[yellow]╵[reset]") leftRuleWidth = 4 + case pb.Diagnostic_SEVERITY_UNSPECIFIED, pb.Diagnostic_SEVERITY_UNKNOWN: + buf.WriteString(cfg.color.Color("\n[reset]")) default: buf.WriteString(cfg.color.Color("\n[reset]")) } @@ -454,6 +463,7 @@ func String(diag *pb.Diagnostic, opts ...StringOpt) string { func appendSourceSnippets(buf *bytes.Buffer, diag *pb.Diagnostic, color *colorstring.Colorize) { if diag.GetRange() == nil { fmt.Fprintf(buf, " (source code not available)\n") + return } @@ -585,9 +595,11 @@ func compactValueStr(val cty.Value) string { if val.True() { return "true" } + return "false" case ty == cty.Number: bf := val.AsBigFloat() + return bf.Text('g', 10) case ty == cty.String: // Go string syntax is not exactly the same as HCL native string syntax, @@ -615,6 +627,7 @@ func compactValueStr(val cty.Value) string { for k := range atys { name = k } + return fmt.Sprintf("object with 1 attribute %q", name) default: return fmt.Sprintf("object with %d attributes", l) @@ -651,5 +664,6 @@ func traversalStr(traversal hcl.Traversal) string { buf.WriteByte(']') } } + return buf.String() } diff --git a/internal/diagnostics/operation.go b/internal/diagnostics/operation.go index 4c74ec9d..a72e43b8 100644 --- a/internal/diagnostics/operation.go +++ b/internal/diagnostics/operation.go @@ -15,27 +15,27 @@ func Status(failOnWarn bool, diags ...*pb.Diagnostic) pb.Operation_Status { return status } -// OpResFailed checks an operation response for failure diagnostics +// OpResFailed checks an operation response for failure diagnostics. func OpResFailed(failOnWarn bool, res *pb.Operation_Response) bool { return OpResErrors(res) || (failOnWarn && OpResWarnings(res)) } -// OpResErrors checks an operation response failure diagnostics +// OpResErrors checks an operation response failure diagnostics. func OpResErrors(res *pb.Operation_Response) bool { return HasErrors(resDiags(res)) } -// OpResWarnings checks an operation response warning diagnostics +// OpResWarnings checks an operation response warning diagnostics. func OpResWarnings(res *pb.Operation_Response) bool { return HasWarnings(resDiags(res)) } -// OpEventErrors returns whether the event has errors +// OpEventErrors returns whether the event has errors. func OpEventErrors(e *pb.Operation_Event) bool { return HasErrors(eventDiags(e)) } -// OpEventWarnings returns whether the event has warnings +// OpEventWarnings returns whether the event has warnings. func OpEventWarnings(e *pb.Operation_Event) bool { return HasWarnings(eventDiags(e)) } @@ -51,7 +51,7 @@ func OperationStatus(failOnWarn bool, res *pb.Operation_Response) pb.Operation_S return status } -// resDiags returns all of the diagnostics that might be included in a response +// resDiags returns all of the diagnostics that might be included in a response. func resDiags(res *pb.Operation_Response) []*pb.Diagnostic { return Concat( res.GetDiagnostics(), @@ -82,7 +82,7 @@ func resDiags(res *pb.Operation_Response) []*pb.Diagnostic { ) } -// eventDiags returns all of the diagnosticsthat might be included in an event +// eventDiags returns all of the diagnosticsthat might be included in an event. func eventDiags(e *pb.Operation_Event) []*pb.Diagnostic { return Concat( e.GetDiagnostics(), diff --git a/internal/flightplan/decoder.go b/internal/flightplan/decoder.go index ee57c284..3a332ede 100644 --- a/internal/flightplan/decoder.go +++ b/internal/flightplan/decoder.go @@ -1,6 +1,7 @@ package flightplan import ( + "context" "fmt" "path/filepath" @@ -18,16 +19,16 @@ import ( type DecodeMode int const ( - // Decode and fully evaluate the entire flight plan + // Decode and fully evaluate the entire flight plan. DecodeModeFull = iota - // Decode scenarios to the reference level + // Decode scenarios to the reference level. DecodeModeRef ) -// DecoderOpt is a functional option for a new flight plan +// DecoderOpt is a functional option for a new flight plan. type DecoderOpt func(*Decoder) error -// NewDecoder takes functional options and returns a new flight plan +// NewDecoder takes functional options and returns a new flight plan. func NewDecoder(opts ...DecoderOpt) (*Decoder, error) { d := &Decoder{ FPParser: hclparse.NewParser(), @@ -44,51 +45,57 @@ func NewDecoder(opts ...DecoderOpt) (*Decoder, error) { return d, nil } -// WithDecoderFPFiles sets the flight plan contents as raw bytes +// WithDecoderFPFiles sets the flight plan contents as raw bytes. func WithDecoderFPFiles(files RawFiles) DecoderOpt { return func(fp *Decoder) error { fp.fpFiles = files + return nil } } -// WithDecoderVarFiles sets the flight plan variable contents as raw bytes +// WithDecoderVarFiles sets the flight plan variable contents as raw bytes. func WithDecoderVarFiles(files RawFiles) DecoderOpt { return func(fp *Decoder) error { fp.varFiles = files + return nil } } -// WithDecoderEnv sets flight plan variables from env vars +// WithDecoderEnv sets flight plan variables from env vars. func WithDecoderEnv(vars []string) DecoderOpt { return func(fp *Decoder) error { fp.varEnvVars = vars + return nil } } -// WithDecoderBaseDir sets the flight plan base directory +// WithDecoderBaseDir sets the flight plan base directory. func WithDecoderBaseDir(path string) DecoderOpt { return func(fp *Decoder) error { var err error fp.dir, err = filepath.Abs(path) + return err } } -// WithDecoderDecodeMode sets the decoding mode +// WithDecoderDecodeMode sets the decoding mode. func WithDecoderDecodeMode(mode DecodeMode) DecoderOpt { return func(fp *Decoder) error { fp.mode = mode + return nil } } -// WithDecoderScenarioFilter sets the scenario decoding filter +// WithDecoderScenarioFilter sets the scenario decoding filter. func WithDecoderScenarioFilter(filter *ScenarioFilter) DecoderOpt { return func(fp *Decoder) error { fp.filter = filter + return nil } } @@ -241,7 +248,7 @@ func (d *Decoder) baseEvalContext() *hcl.EvalContext { } // Decode decodes the HCL into a flight plan. -func (d *Decoder) Decode() (*FlightPlan, hcl.Diagnostics) { +func (d *Decoder) Decode(ctx context.Context) (*FlightPlan, hcl.Diagnostics) { var diags hcl.Diagnostics fp, err := NewFlightPlan(WithFlightPlanBaseDirectory(d.dir)) @@ -260,9 +267,9 @@ func (d *Decoder) Decode() (*FlightPlan, hcl.Diagnostics) { }) } - ctx := d.baseEvalContext() - if ctx == nil { - ctx = &hcl.EvalContext{ + evalCtx := d.baseEvalContext() + if evalCtx == nil { + evalCtx = &hcl.EvalContext{ Variables: map[string]cty.Value{}, Functions: map[string]function.Function{}, } @@ -283,15 +290,15 @@ func (d *Decoder) Decode() (*FlightPlan, hcl.Diagnostics) { // Decode and validate our variables. They'll be added to eval context for // access in later decoding. - diags = diags.Extend(fp.decodeVariables(ctx, varsFiles, d.varEnvVars)) + diags = diags.Extend(fp.decodeVariables(evalCtx, varsFiles, d.varEnvVars)) // Decode child blocks. Each child block decoder is responsible for // extending the evaluation context. - diags = diags.Extend(fp.decodeTerraformSettings(ctx)) - diags = diags.Extend(fp.decodeTerraformCLIs(ctx)) - diags = diags.Extend(fp.decodeProviders(ctx)) - diags = diags.Extend(fp.decodeModules(ctx)) - diags = diags.Extend(fp.decodeScenarios(ctx, d.mode, d.filter)) + diags = diags.Extend(fp.decodeTerraformSettings(evalCtx)) + diags = diags.Extend(fp.decodeTerraformCLIs(evalCtx)) + diags = diags.Extend(fp.decodeProviders(evalCtx)) + diags = diags.Extend(fp.decodeModules(evalCtx)) + diags = diags.Extend(fp.decodeScenarios(ctx, evalCtx, d.mode, d.filter)) return fp, diags } diff --git a/internal/flightplan/decoder_test.go b/internal/flightplan/decoder_test.go index 04c1ec85..ce83ae13 100644 --- a/internal/flightplan/decoder_test.go +++ b/internal/flightplan/decoder_test.go @@ -6,10 +6,12 @@ import ( "path/filepath" "strings" "testing" + "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/zclconf/go-cty/cty" + "golang.org/x/net/context" hcl "github.com/hashicorp/hcl/v2" ) @@ -30,6 +32,7 @@ func testDiagsToError(files map[string]*hcl.File, diags hcl.Diagnostics) error { func testDecodeHCL(t *testing.T, hcl []byte, env ...string) (*FlightPlan, error) { t.Helper() + cwd, err := os.Getwd() require.NoError(t, err) decoder, err := NewDecoder( @@ -40,12 +43,16 @@ func testDecodeHCL(t *testing.T, hcl []byte, env ...string) (*FlightPlan, error) require.NoError(t, err) _, diags := decoder.FPParser.ParseHCL(hcl, "decoder-test.hcl") require.False(t, diags.HasErrors(), testDiagsToError(decoder.ParserFiles(), diags)) - fp, diags := decoder.Decode() + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + fp, diags := decoder.Decode(ctx) + return fp, testDiagsToError(decoder.ParserFiles(), diags) } func testRequireEqualFP(t *testing.T, fp, expected *FlightPlan) { t.Helper() + require.Len(t, fp.Modules, len(expected.Modules)) require.Len(t, fp.Scenarios, len(expected.Scenarios)) require.Len(t, fp.Providers, len(expected.Providers)) @@ -148,6 +155,7 @@ func testMostlyEqualStepVar(t *testing.T, expected cty.Value, got cty.Value) { aAttr, ok := aVal.Traversal[i].(hcl.TraverseRoot) require.True(t, ok) require.EqualValues(t, eAttr.Name, aAttr.Name) + continue } eAttr, ok := eVal.Traversal[i].(hcl.TraverseAttr) @@ -158,7 +166,7 @@ func testMostlyEqualStepVar(t *testing.T, expected cty.Value, got cty.Value) { } } -// Test_Decode_FlightPlan tests decoding a flight plan +// Test_Decode_FlightPlan tests decoding a flight plan. func Test_Decode_FlightPlan(t *testing.T) { t.Helper() t.Parallel() @@ -250,10 +258,14 @@ scenario "backend" { `, modulePath), }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/file_finder.go b/internal/flightplan/file_finder.go index 264b48a1..cfc284ba 100644 --- a/internal/flightplan/file_finder.go +++ b/internal/flightplan/file_finder.go @@ -9,17 +9,17 @@ import ( "regexp" ) -// FlightPlanFileNamePattern is what file names match valid enos configuration files +// FlightPlanFileNamePattern is what file names match valid enos configuration files. var ( FlightPlanFileNamePattern = regexp.MustCompile(`^enos[-\w]*?\.hcl$`) VariablesNamePattern = regexp.MustCompile(`^enos[-\w]*?\.vars\.hcl$`) ) -// RawFiles are a map of flightplan configuration files and their contents +// RawFiles are a map of flightplan configuration files and their contents. type RawFiles map[string][]byte // FindRawFiles scans a directory for files matching the given pattern and -// returns the loaded raw files +// returns the loaded raw files. func FindRawFiles(dir string, pattern *regexp.Regexp) (RawFiles, error) { var err error files := RawFiles{} @@ -59,7 +59,7 @@ func FindRawFiles(dir string, pattern *regexp.Regexp) (RawFiles, error) { return files, err } -// LoadRawFiles takes a slice of paths and returns the loaded raw files +// LoadRawFiles takes a slice of paths and returns the loaded raw files. func LoadRawFiles(paths []string) (RawFiles, error) { rawFiles := RawFiles{} var err error diff --git a/internal/flightplan/flightplan.go b/internal/flightplan/flightplan.go index 1d9ece57..463b1dcb 100644 --- a/internal/flightplan/flightplan.go +++ b/internal/flightplan/flightplan.go @@ -1,11 +1,13 @@ package flightplan import ( + "context" "fmt" "path/filepath" "regexp" "sort" "strings" + "sync" "github.com/zclconf/go-cty/cty" @@ -50,7 +52,7 @@ var flightPlanSchema = &hcl.BodySchema{ }, } -// NewFlightPlan returns a new instance of a FlightPlan +// NewFlightPlan returns a new instance of a FlightPlan. func NewFlightPlan(opts ...Opt) (*FlightPlan, error) { fp := &FlightPlan{ Files: map[string]*hcl.File{}, @@ -77,11 +79,12 @@ func WithFlightPlanBaseDirectory(dir string) Opt { return func(fp *FlightPlan) error { var err error fp.BaseDir, err = filepath.Abs(dir) + return err } } -// Opt is a flight plan option +// Opt is a flight plan option. type Opt func(*FlightPlan) error // FlightPlan represents our flight plan, the main configuration of Enos. @@ -314,6 +317,7 @@ func (fp *FlightPlan) decodeProviders(ctx *hcl.EvalContext) hcl.Diagnostics { Subject: hcl.RangeBetween(block.LabelRanges[0], block.LabelRanges[1]).Ptr(), Context: block.DefRange.Ptr(), }) + continue } @@ -361,62 +365,42 @@ func (fp *FlightPlan) decodeModules(ctx *hcl.EvalContext) hcl.Diagnostics { return diags } -// decodeScenario decodes a single scenario block according to the mode and -// filter. It returns whether or not it matched any filters if the mode calls -// for filtering and any diagnostics encountered. func decodeScenario( ctx *hcl.EvalContext, - block *hcl.Block, - scenario *Scenario, + vec *Vector, mode DecodeMode, - filter *ScenarioFilter, -) (bool, hcl.Diagnostics) { - switch mode { - case DecodeModeRef: - diags := scenario.decode(block, ctx.NewChild(), DecodeModeRef) - if diags.HasErrors() { - return false, diags - } - - // If we have a filter make sure it matched - if filter.String() != "" && !scenario.Match(filter) { - return false, diags - } - - return true, diags - case DecodeModeFull: - // If we don't have a filter with configuration we'll decode it immediately - if filter.String() == "" { - return true, scenario.decode(block, ctx.NewChild(), DecodeModeFull) - } - - // Decode it shallow and see if it matched the filter - matched, diags := decodeScenario(ctx.NewChild(), block, scenario, DecodeModeRef, filter) - if !matched || diags.HasErrors() { - return matched, diags - } + block *hcl.Block, +) (bool, *Scenario, hcl.Diagnostics) { + scenario := NewScenario() + var diags hcl.Diagnostics - // We have a match so fully decode it - moreDiags := scenario.decode(block, ctx.NewChild(), DecodeModeFull) - diags = diags.Extend(moreDiags) - if moreDiags.HasErrors() { - return false, diags + if vec != nil { + scenario.Variants = vec + matrixCtx := ctx.NewChild() + matrixCtx.Variables = map[string]cty.Value{ + "matrix": vec.CtyVal(), } + ctx = matrixCtx + } - return true, diags + switch mode { + case DecodeModeRef, DecodeModeFull: + diags = scenario.decode(block, ctx.NewChild(), mode) default: - var diags hcl.Diagnostics - return false, diags.Append(&hcl.Diagnostic{ + diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: fmt.Sprintf("unknown filter mode %d", mode), }) } + + return !diags.HasErrors(), scenario, diags } // decodeScenarios decodes the "scenario" blocks that are defined in the // top-level schema. func (fp *FlightPlan) decodeScenarios( - ctx *hcl.EvalContext, + ctx context.Context, + evalCtx *hcl.EvalContext, mode DecodeMode, filter *ScenarioFilter, ) hcl.Diagnostics { @@ -429,82 +413,178 @@ func (fp *FlightPlan) decodeScenarios( continue } - // Decode the matrix block and expand the scenarios. - matrix, moreDiags := decodeMatrix(ctx, block) + // If we've got a filter that includes a name and our scenario block doesn't + // match we don't need to decode anything. + if filter != nil && filter.Name != "" && block.Labels[0] != filter.Name { + continue + } + + // Decode the matrix block if there is one. + matrix, _, moreDiags := decodeMatrix(evalCtx, block) diags = diags.Extend(moreDiags) if moreDiags.HasErrors() { continue } - if matrix == nil || len(matrix.Vectors) == 0 { - // Our scenario doesn't include a matrix - scenario := NewScenario() - - keep, moreDiags := decodeScenario(ctx, block, scenario, mode, filter) - diags = diags.Extend(moreDiags) - if moreDiags.HasErrors() { - continue - } + // Reduce our matrix + if matrix != nil && filter != nil { + matrix = matrix.Filter(filter) + } - if keep { - fp.Scenarios = append(fp.Scenarios, scenario) + var scenarios []*Scenario + if matrix == nil || len(matrix.Vectors) < 1 { + scenarios, moreDiags = decodeScenarios(evalCtx, nil, mode, block) + } else { + switch mode { + case DecodeModeRef: + switch { + case len(matrix.Vectors) < 10_000: + scenarios, moreDiags = decodeScenarios(evalCtx, matrix.Vectors, mode, block) + default: + scenarios, moreDiags = decodeScenariosConcurrent(ctx, evalCtx, matrix.Vectors, mode, block) + } + case DecodeModeFull: + switch { + case len(matrix.Vectors) < 100: + scenarios, moreDiags = decodeScenarios(evalCtx, matrix.Vectors, mode, block) + default: + scenarios, moreDiags = decodeScenariosConcurrent(ctx, evalCtx, matrix.Vectors, mode, block) + } + default: + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "unknown scenario decode mode", + Detail: fmt.Sprintf("%v is not a known decode mode", mode), + Subject: block.TypeRange.Ptr(), + Context: block.DefRange.Ptr(), + }) } + } + fp.Scenarios = append(fp.Scenarios, scenarios...) + diags = diags.Extend(moreDiags) + if moreDiags.HasErrors() { continue } + } - for _, vec := range matrix.Vectors { - // Create a scenario for every matrix variant combination - scenario := NewScenario() - scenario.Variants = vec - matrixCtx := ctx.NewChild() - matrixCtx.Variables = map[string]cty.Value{ - "matrix": vec.CtyVal(), - } + sort.Slice(fp.Scenarios, func(i, j int) bool { + return fp.Scenarios[i].String() < fp.Scenarios[j].String() + }) - keep, moreDiags := decodeScenario(matrixCtx, block, scenario, mode, filter) - diags = diags.Extend(moreDiags) - if moreDiags.HasErrors() { - // Exit early rather than continuing to decode all scenarios. - // We do this to avoid spamming the output with identical - // diagnostics for common errors that will be found in all - // of the scenarios. - return diags - } + return diags +} - if keep { - fp.Scenarios = append(fp.Scenarios, scenario) +// decodeScenarios decodes scenario variants serially. When we don't have lots of scenarios or we're +// in reference decode mode this can be faster than the overhead of goroutines. +func decodeScenarios( + ctx *hcl.EvalContext, + vecs []*Vector, + mode DecodeMode, + block *hcl.Block, +) ([]*Scenario, hcl.Diagnostics) { + // Handle not matrix vectors + if vecs == nil || len(vecs) < 1 { + keep, scenario, diags := decodeScenario(ctx, nil, mode, block) + if keep { + return []*Scenario{scenario}, diags + } + + return nil, diags + } + + // Decode a scenario for all matrix vectors + scenarios := []*Scenario{} + diags := hcl.Diagnostics{} + for i := range vecs { + keep, scenario, moreDiags := decodeScenario(ctx, vecs[i], mode, block) + diags = diags.Extend(moreDiags) + if keep { + scenarios = append(scenarios, scenario) + } + } + + return scenarios, diags +} + +// decodeScenariosConcurrent decodes scenario variants concurrently. This is for improved speeds +// when fully decoding lots of scenarios. +func decodeScenariosConcurrent( + ctx context.Context, + evalCtx *hcl.EvalContext, + vecs []*Vector, + mode DecodeMode, + block *hcl.Block, +) ([]*Scenario, hcl.Diagnostics) { + if vecs == nil || len(vecs) < 1 { + return decodeScenarios(evalCtx, nil, mode, block) + } + + collectCtx, cancel := context.WithCancel(ctx) + defer cancel() + + diagC := make(chan hcl.Diagnostics) + scenarioC := make(chan *Scenario) + wg := sync.WaitGroup{} + scenarios := []*Scenario{} + diags := hcl.Diagnostics{} + doneC := make(chan struct{}) + + collect := func() { + for { + select { + case <-collectCtx.Done(): + close(doneC) + + return + case diag := <-diagC: + diags = diags.Extend(diag) + case scenario := <-scenarioC: + scenarios = append(scenarios, scenario) } } + } + go collect() + + for i := range vecs { + wg.Add(1) + go func(vec *Vector) { + defer wg.Done() + keep, scenario, diags := decodeScenario(evalCtx, vec, mode, block) + diagC <- diags + if keep { + scenarioC <- scenario + } + }(vecs[i]) } - sort.Slice(fp.Scenarios, func(i, j int) bool { - return fp.Scenarios[i].String() < fp.Scenarios[j].String() - }) + wg.Wait() + cancel() + <-doneC - return diags + return scenarios, diags } // decodeMatrix takes an eval context and scenario blocks and decodes only the // matrix block. It returns a unique matrix with vectors for all unique variant // value combinations. -func decodeMatrix(ctx *hcl.EvalContext, block *hcl.Block) (*Matrix, hcl.Diagnostics) { +func decodeMatrix(ctx *hcl.EvalContext, block *hcl.Block) (*Matrix, *hcl.Block, hcl.Diagnostics) { mContent, diags := block.Body.Content(scenarioSchema) if diags.HasErrors() { - return nil, diags + return nil, block, diags } mBlocks := mContent.Blocks.OfType(blockTypeMatrix) switch len(mBlocks) { case 0: // We have no matrix block defined - return nil, diags + return nil, block, diags case 1: // Continue break default: - return nil, diags.Append(&hcl.Diagnostic{ + return nil, block, diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "scenarios has more than one matrix block defined", Detail: fmt.Sprintf("up to one matrix block is expected, found %d", len(mBlocks)), @@ -608,7 +688,7 @@ func decodeMatrix(ctx *hcl.EvalContext, block *hcl.Block) (*Matrix, hcl.Diagnost }) diags = diags.Extend(moreDiags) if moreDiags.HasErrors() { - return nil, diags + return nil, block, diags } diags = diags.Extend(verifyBodyOnlyHasBlocksWithLabels( remain, blockTypeMatrixInclude, blockTypeMatrixExclude, @@ -681,13 +761,14 @@ func decodeMatrix(ctx *hcl.EvalContext, block *hcl.Block) (*Matrix, hcl.Diagnost Subject: mBlock.TypeRange.Ptr(), Context: mBlock.DefRange.Ptr(), }) + continue } } // Return our matrix but do one final pass removing any duplicates that might // have been introduced during our inclusions. - return matrix.UniqueValues(), diags + return matrix.UniqueValues(), block, diags } // filterTerraformMetaAttrs does our best to ensure that the given set of @@ -757,6 +838,7 @@ func verifyBodyOnlyHasBlocksWithLabels(in hcl.Body, allowed ...string) hcl.Diagn for _, allowed := range allowed { if block.Type == allowed { isAllowed = true + break } } @@ -805,7 +887,7 @@ func verifyBlockLabelsAreValidIdentifiers(block *hcl.Block) hcl.Diagnostics { // Make sure it also adheres to Enos block name rules r := regexp.MustCompile(`^[\w]+$`) - if !r.Match([]byte(label)) { + if !r.MatchString(label) { diags = diags.Append(&hcl.Diagnostic{ Severity: hcl.DiagError, Summary: "block label is invalid", diff --git a/internal/flightplan/funcs/filesystem.go b/internal/flightplan/funcs/filesystem.go index d95db52f..109c25fc 100644 --- a/internal/flightplan/funcs/filesystem.go +++ b/internal/flightplan/funcs/filesystem.go @@ -30,12 +30,13 @@ func AbsPathFunc(basePath string) function.Function { path = filepath.Join(basePath, path) } absPath, err := filepath.Abs(path) + return cty.StringVal(filepath.ToSlash(absPath)), err }, }) } -// JoinPathFunc constructs a function that converts joins two paths +// JoinPathFunc constructs a function that converts joins two paths. var JoinPathFunc = function.New(&function.Spec{ Params: []function.Parameter{ { @@ -67,6 +68,7 @@ var FileFunc = function.New(&function.Spec{ Type: function.StaticReturnType(cty.String), Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { f, err := os.ReadFile(args[0].AsString()) + return cty.StringVal(string(f)), err }, }) diff --git a/internal/flightplan/funcs/semver_test.go b/internal/flightplan/funcs/semver_test.go index 96d5762d..8ba9404c 100644 --- a/internal/flightplan/funcs/semver_test.go +++ b/internal/flightplan/funcs/semver_test.go @@ -26,7 +26,10 @@ func TestServerConstraint(t *testing.T) { true, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() + val, err := SemverConstraint.Call([]cty.Value{ cty.StringVal(test.version), cty.StringVal(test.constraint), }) diff --git a/internal/flightplan/matrix.go b/internal/flightplan/matrix.go index 44a9ec10..6c1edb39 100644 --- a/internal/flightplan/matrix.go +++ b/internal/flightplan/matrix.go @@ -10,7 +10,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// An Element is an Element of a Matrix Vector +// An Element is an Element of a Matrix Vector. type Element struct { Key string Val string @@ -36,18 +36,18 @@ type Matrix struct { Vectors []*Vector } -// An Exclude is a filter to removing Elements from the Matrix's Vector combined +// An Exclude is a filter to removing Elements from the Matrix's Vector combined. type Exclude struct { Mode pb.Scenario_Filter_Exclude_Mode Vector *Vector } -// NewElement takes an element key and value and returns a new Element +// NewElement takes an element key and value and returns a new Element. func NewElement(key string, val string) Element { return Element{Key: key, Val: val} } -// NewMatrix returns a pointer to a new instance of Matrix +// NewMatrix returns a pointer to a new instance of Matrix. func NewMatrix() *Matrix { return &Matrix{} } @@ -61,6 +61,8 @@ func NewExclude(mode pb.Scenario_Filter_Exclude_Mode, vec *Vector) (*Exclude, er case pb.Scenario_Filter_Exclude_MODE_EXACTLY, pb.Scenario_Filter_Exclude_MODE_EQUAL_UNORDERED, pb.Scenario_Filter_Exclude_MODE_CONTAINS: + case pb.Scenario_Filter_Exclude_MODE_UNSPECIFIED: + return ex, fmt.Errorf("exclusion mode was not specified") default: return ex, fmt.Errorf("unknown exclusion mode: %d", mode) } @@ -68,7 +70,7 @@ func NewExclude(mode pb.Scenario_Filter_Exclude_Mode, vec *Vector) (*Exclude, er return ex, nil } -// String returns the element as a string +// String returns the element as a string. func (e Element) String() string { // Matrix and vector comparison operations often required the element as // a string. We'll cache it to speed up those operations. @@ -81,12 +83,12 @@ func (e Element) String() string { return e.formattedString } -// Proto returns the element as a proto message +// Proto returns the element as a proto message. func (e Element) Proto() *pb.Scenario_Filter_Element { return &pb.Scenario_Filter_Element{Key: e.Key, Value: e.Val} } -// Equals compares the element with another +// Equals compares the element with another. func (e Element) Equal(other Element) bool { if e.Key != other.Key { return false @@ -103,7 +105,7 @@ func NewVector() *Vector { return &Vector{} } -// String returns the vector as a string +// String returns the vector as a string. func (v *Vector) String() string { elmStrings := []string{} for _, elm := range v.elements { @@ -113,7 +115,7 @@ func (v *Vector) String() string { return fmt.Sprintf("[%s]", strings.Join(elmStrings, " ")) } -// Equal returns true if both Vectors have Equal values and Equal value ordering +// Equal returns true if both Vectors have Equal values and Equal value ordering. func (v *Vector) Equal(other *Vector) bool { if v == nil && other == nil { return true @@ -200,6 +202,7 @@ func (v *Vector) ContainsUnordered(other *Vector) bool { for vi := range v.elements { if other.elements[oi].Equal(v.elements[vi]) { match = true + break } } @@ -222,7 +225,7 @@ func (v *Vector) CtyVal() cty.Value { return cty.ObjectVal(vals) } -// Proto returns the vector as a proto message +// Proto returns the vector as a proto message. func (v *Vector) Proto() *pb.Scenario_Filter_Vector { pbv := &pb.Scenario_Filter_Vector{Elements: []*pb.Scenario_Filter_Element{}} @@ -240,7 +243,7 @@ func (v *Vector) Proto() *pb.Scenario_Filter_Vector { return pbv } -// Add adds an element to the Vector +// Add adds an element to the Vector. func (v *Vector) Add(e Element) { if v.elements == nil { v.elements = []Element{} @@ -254,7 +257,7 @@ func (v *Vector) Add(e Element) { } } -// Copy creates a new Copy of the Vector +// Copy creates a new Copy of the Vector. func (v *Vector) Copy() *Vector { vecC := NewVector() @@ -274,7 +277,7 @@ func (v *Vector) Copy() *Vector { return vecC } -// Elements returns a list of the vectors elements +// Elements returns a list of the vectors elements. func (v *Vector) Elements() []Element { return v.elements } @@ -315,6 +318,7 @@ func NewVectorFromProto(pbv *pb.Scenario_Filter_Vector) *Vector { for _, elm := range pbv.GetElements() { v.Add(NewElement(elm.GetKey(), elm.GetValue())) } + return v } @@ -331,17 +335,32 @@ func (m *Matrix) AddVector(vec *Vector) { m.Vectors = append(m.Vectors, vec) } +// Copy creates a new Copy of the Matrix. +func (m *Matrix) Copy() *Matrix { + nm := NewMatrix() + + for i := range m.Vectors { + nm.AddVector(m.Vectors[i].Copy()) + } + + return nm +} + // Exclude takes exclude vararg exclude directives as instances of Exclude. It // returns a new matrix with all exclude directives having been processed on -// on the parent matrix. +// the parent matrix. func (m *Matrix) Exclude(Excludes ...*Exclude) *Matrix { - nm := NewMatrix() + if len(Excludes) < 1 { + return m.Copy() + } + nm := NewMatrix() for _, vec := range m.Vectors { skip := false for _, ex := range Excludes { if ex.Match(vec) { skip = true + break } } @@ -460,7 +479,39 @@ func (m *Matrix) UniqueValues() *Matrix { return nm } -// Match determines if Exclude directive matches the vector +// Filter takes a scenario filter returns a new filtered matrix. +func (m *Matrix) Filter(filter *ScenarioFilter) *Matrix { + if filter == nil { + return nil + } + + if filter.SelectAll { + return m.Copy() + } + + var nm *Matrix + if filter.Include != nil && len(filter.Include.elements) > 0 { + // If we have an include filter we'll generate a new sub-matrix with matching vectors + nm = NewMatrix() + for i := range m.Vectors { + if m.Vectors[i].ContainsUnordered(filter.Include) { + nm.AddVector(m.Vectors[i]) + } + } + } else { + // If we don't have an include and we're not selecting all we need to start with our + // entire matrix and then process excludes. + nm = m.Copy() + } + + if filter.Exclude != nil && len(filter.Exclude) > 0 { + nm = nm.Exclude(filter.Exclude...) + } + + return nm +} + +// Match determines if Exclude directive matches the vector. func (ex *Exclude) Match(vec *Vector) bool { if vec == nil { return false @@ -479,13 +530,16 @@ func (ex *Exclude) Match(vec *Vector) bool { if vec.ContainsUnordered(ex.Vector) { return true } + case pb.Scenario_Filter_Exclude_MODE_UNSPECIFIED: + return false default: + return false } return false } -// Proto returns the exclude as a proto message +// Proto returns the exclude as a proto message. func (ex *Exclude) Proto() *pb.Scenario_Filter_Exclude { return &pb.Scenario_Filter_Exclude{ Vector: ex.Vector.Proto(), @@ -493,7 +547,7 @@ func (ex *Exclude) Proto() *pb.Scenario_Filter_Exclude { } } -// FromProto unmarshals a proto Scenario_Filter_Exclude into itself +// FromProto unmarshals a proto Scenario_Filter_Exclude into itself. func (ex *Exclude) FromProto(pfe *pb.Scenario_Filter_Exclude) { if pfe == nil { return diff --git a/internal/flightplan/matrix_test.go b/internal/flightplan/matrix_test.go index 952b2d59..bed1e4c6 100644 --- a/internal/flightplan/matrix_test.go +++ b/internal/flightplan/matrix_test.go @@ -3,6 +3,7 @@ package flightplan import ( "fmt" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -11,7 +12,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// Test_Decode_Scenario_Matrix tests decoding of a matrix in scenarios +// Test_Decode_Scenario_Matrix tests decoding of a matrix in scenarios. func Test_Decode_Scenario_Matrix(t *testing.T) { t.Parallel() @@ -301,10 +302,14 @@ scenario "nighttime" { }, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) @@ -314,6 +319,8 @@ scenario "nighttime" { } func Test_Matrix_Vector_Equal(t *testing.T) { + t.Parallel() + for desc, test := range map[string]struct { in *Vector other *Vector @@ -335,13 +342,17 @@ func Test_Matrix_Vector_Equal(t *testing.T) { false, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() require.Equal(t, test.equal, test.in.Equal(test.other)) }) } } func Test_Matrix_Vector_ContainsUnordered(t *testing.T) { + t.Parallel() + for desc, test := range map[string]struct { in *Vector other *Vector @@ -373,13 +384,17 @@ func Test_Matrix_Vector_ContainsUnordered(t *testing.T) { false, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() require.Equal(t, test.match, test.in.ContainsUnordered(test.other)) }) } } func Test_Matrix_Vector_EqualUnordered(t *testing.T) { + t.Parallel() + for desc, test := range map[string]struct { in *Vector other *Vector @@ -401,13 +416,17 @@ func Test_Matrix_Vector_EqualUnordered(t *testing.T) { true, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() require.Equal(t, test.equal, test.in.EqualUnordered(test.other)) }) } } func Test_Matrix_CartesianProduct(t *testing.T) { + t.Parallel() + for desc, test := range map[string]struct { in *Matrix expected *Matrix @@ -466,13 +485,17 @@ func Test_Matrix_CartesianProduct(t *testing.T) { }}, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() require.Equal(t, test.expected.Vectors, test.in.CartesianProduct().Vectors) }) } } func Test_Matrix_CartesianProduct_empty_vector(t *testing.T) { + t.Parallel() + m := NewMatrix() m.AddVector(NewVector()) m.AddVector(NewVector()) @@ -481,6 +504,8 @@ func Test_Matrix_CartesianProduct_empty_vector(t *testing.T) { } func Test_Matrix_UniqueValues(t *testing.T) { + t.Parallel() + m1 := NewMatrix() m1.AddVector(&Vector{elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul"), NewElement("backend", "mssql")}}) m1.AddVector(&Vector{elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}) @@ -504,6 +529,8 @@ func Test_Matrix_UniqueValues(t *testing.T) { } func Test_Matrix_Unique(t *testing.T) { + t.Parallel() + m1 := NewMatrix() m1.AddVector(&Vector{elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}) m1.AddVector(&Vector{elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}) @@ -524,12 +551,162 @@ func Test_Matrix_Unique(t *testing.T) { require.Equal(t, m2, m1.Unique()) } +func Test_Matrix_Filter(t *testing.T) { + t.Parallel() + + for desc, test := range map[string]struct { + in *Matrix + filterStr string + expected *Matrix + }{ + "all": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + "", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + }, + "include-some": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + "backend:raft", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + }}, + }, + "include-one": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + "backend:raft arch:amd64", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + }}, + }, + "exclude-one": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + "!arch:amd64", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + }, + "exclude-some": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + }}, + "!arch:amd64 !backend:consul", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + }}, + }, + "include-and-exclude-one": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "aarch64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "aarch64")}}, + }}, + "!arch:amd64 backend:raft", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "aarch64")}}, + }}, + }, + "include-and-exclude-some": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "aarch64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "amd64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "aarch64")}}, + }}, + "!arch:amd64 !backend:raft arch:aarch64", + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "consul"), NewElement("arch", "aarch64")}}, + }}, + }, + } { + test := test + t.Run(desc, func(t *testing.T) { + t.Parallel() + + f, err := NewScenarioFilter(WithScenarioFilterParse(strings.Split(test.filterStr, " "))) + require.NoError(t, err) + require.Equal(t, test.expected, test.in.Filter(f)) + }) + } +} + func Test_Matrix_Exclude(t *testing.T) { + t.Parallel() + for desc, test := range map[string]struct { in *Matrix Excludes []*Exclude expected *Matrix }{ + "nil": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("backend", "raft")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + }}, + nil, + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("backend", "raft")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + }}, + }, + "empty": { + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("backend", "raft")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + }}, + []*Exclude{}, + &Matrix{Vectors: []*Vector{ + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, + {elements: []Element{NewElement("backend", "consul"), NewElement("backend", "raft")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + }}, + }, "exact": { &Matrix{Vectors: []*Vector{ {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul")}}, @@ -582,6 +759,8 @@ func Test_Matrix_Exclude(t *testing.T) { {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "mysql"), NewElement("backend", "mssql")}}, {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "arm32")}}, {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64"), NewElement("arch", "arm32")}}, }}, []*Exclude{ { @@ -596,10 +775,14 @@ func Test_Matrix_Exclude(t *testing.T) { &Matrix{Vectors: []*Vector{ {elements: []Element{NewElement("backend", "raft"), NewElement("backend", "consul"), NewElement("backend", "mssql")}}, {elements: []Element{NewElement("arch", "amd64"), NewElement("arch", "arm64"), NewElement("arch", "ppc64")}}, + {elements: []Element{NewElement("backend", "raft"), NewElement("arch", "arm64")}}, }}, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() + require.Equal(t, test.expected.Vectors, test.in.Exclude(test.Excludes...).Vectors) }) } diff --git a/internal/flightplan/module.go b/internal/flightplan/module.go index 77902a0a..c2c1c755 100644 --- a/internal/flightplan/module.go +++ b/internal/flightplan/module.go @@ -16,7 +16,7 @@ var moduleSchema = &hcl.BodySchema{ }, } -// Module represents an Enos Terraform module block +// Module represents an Enos Terraform module block. type Module struct { Name string Source string @@ -24,7 +24,7 @@ type Module struct { Attrs map[string]cty.Value } -// NewModule returns a new Module +// NewModule returns a new Module. func NewModule() *Module { return &Module{ Attrs: map[string]cty.Value{}, @@ -155,7 +155,7 @@ func (m *Module) ToCtyValue() cty.Value { } // FromCtyValue takes a cty.Value and unmarshals it onto itself. It expects -// a valid object created from ToCtyValue() +// a valid object created from ToCtyValue(). func (m *Module) FromCtyValue(val cty.Value) error { if val.IsNull() { return nil diff --git a/internal/flightplan/module_test.go b/internal/flightplan/module_test.go index c87f21e4..abd03c4f 100644 --- a/internal/flightplan/module_test.go +++ b/internal/flightplan/module_test.go @@ -35,7 +35,10 @@ func Test_Module_EvalContext_Functions(t *testing.T) { expected: "something", }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + hcl := fmt.Sprintf(` module "backend" { source = "%s" @@ -57,7 +60,7 @@ scenario "basic" { } } -// Test_Decode_Module tests module decoding +// Test_Decode_Module tests module decoding. func Test_Decode_Module(t *testing.T) { t.Parallel() @@ -182,10 +185,14 @@ scenario "backend" { `, modulePath), }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/provider.go b/internal/flightplan/provider.go index dec94a49..15e0c38e 100644 --- a/internal/flightplan/provider.go +++ b/internal/flightplan/provider.go @@ -8,14 +8,14 @@ import ( hcl "github.com/hashicorp/hcl/v2" ) -// Provider is a Enos transport configuration +// Provider is a Enos transport configuration. type Provider struct { Type string `cty:"type"` Alias string `cty:"alias"` Config *SchemalessBlock `cty:"config"` } -// NewProvider returns a new Provider +// NewProvider returns a new Provider. func NewProvider() *Provider { return &Provider{ Config: NewSchemalessBlock(), @@ -51,7 +51,7 @@ func (p *Provider) ToCtyValue() cty.Value { } // FromCtyValue takes a cty.Value and unmarshals it onto itself. It expects -// a valid object created from ToCtyValue() +// a valid object created from ToCtyValue(). func (p *Provider) FromCtyValue(val cty.Value) error { var err error diff --git a/internal/flightplan/provider_test.go b/internal/flightplan/provider_test.go index 0350cdaa..fae7cf36 100644 --- a/internal/flightplan/provider_test.go +++ b/internal/flightplan/provider_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_Provider tests transport +// Test_Decode_Provider tests transport. func Test_Decode_Provider(t *testing.T) { t.Parallel() @@ -578,10 +578,14 @@ scenario "k8s" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) @@ -591,6 +595,8 @@ scenario "k8s" { } func Test_Provider_Cty_RoundTrip(t *testing.T) { + t.Parallel() + for _, p := range []struct { testName string provider *Provider @@ -653,7 +659,10 @@ func Test_Provider_Cty_RoundTrip(t *testing.T) { }, }, } { + p := p t.Run(p.testName, func(t *testing.T) { + t.Parallel() + clone := NewProvider() require.NoError(t, clone.FromCtyValue(p.provider.ToCtyValue())) require.EqualValues(t, p.provider, clone) diff --git a/internal/flightplan/scenario.go b/internal/flightplan/scenario.go index a6461a0d..3765462a 100644 --- a/internal/flightplan/scenario.go +++ b/internal/flightplan/scenario.go @@ -29,7 +29,7 @@ var scenarioSchema = &hcl.BodySchema{ }, } -// Scenario represents an Enos scenario +// Scenario represents an Enos scenario. type Scenario struct { Name string Variants *Vector @@ -40,7 +40,7 @@ type Scenario struct { Outputs []*ScenarioOutput } -// NewScenario returns a new Scenario +// NewScenario returns a new Scenario. func NewScenario() *Scenario { return &Scenario{ TerraformCLI: NewTerraformCLI(), @@ -50,7 +50,7 @@ func NewScenario() *Scenario { } } -// String returns the scenario identifiers as a string +// String returns the scenario identifiers as a string. func (s *Scenario) String() string { str := s.Name if s.Variants != nil && len(s.Variants.elements) > 0 { @@ -60,12 +60,12 @@ func (s *Scenario) String() string { return str } -// UID returns a unique identifier from the name and variants +// UID returns a unique identifier from the name and variants. func (s *Scenario) UID() string { return fmt.Sprintf("%x", sha256.Sum256([]byte(s.String()))) } -// Ref returns the proto reference +// Ref returns the proto reference. func (s *Scenario) Ref() *pb.Ref_Scenario { return &pb.Ref_Scenario{ Id: &pb.Scenario_ID{ @@ -76,7 +76,7 @@ func (s *Scenario) Ref() *pb.Ref_Scenario { } } -// FromRef takes a unmarshals a scenario reference into itself +// FromRef takes a unmarshals a scenario reference into itself. func (s *Scenario) FromRef(ref *pb.Ref_Scenario) { if ref == nil { return @@ -102,8 +102,8 @@ func (s *Scenario) Match(filter *ScenarioFilter) bool { return false } - // If our scenario doesn't have any variants make make sure we don't have - // a filter with includes or excludes. + // If our scenario doesn't have any variants make sure we don't have a filter with includes + // or excludes. if s.Variants == nil || len(s.Variants.elements) == 0 { if filter.Include != nil && len(filter.Include.elements) > 0 { return false @@ -131,7 +131,7 @@ func (s *Scenario) Match(filter *ScenarioFilter) bool { return true } -// decode takes an HCL block and an evalutaion context and it decodes itself +// decode takes an HCL block and an evaluation context and it decodes itself // from the block. Any errors that are encountered during decoding will be // returned as hcl diagnostics. func (s *Scenario) decode(block *hcl.Block, ctx *hcl.EvalContext, mode DecodeMode) hcl.Diagnostics { @@ -296,6 +296,7 @@ func (s *Scenario) decodeAndValidateTerraformCLIAttribute( if err != nil { diag.Summary = "unable to convert default terraform_cli from eval context to object" diag.Detail = err.Error() + return diags.Append(diag) } @@ -308,6 +309,7 @@ func (s *Scenario) decodeAndValidateTerraformCLIAttribute( // terraform_cli which we'll get from the eval context. moreDiags := findAndLoadCLI("default") diags = diags.Extend(moreDiags) + return diags } @@ -321,8 +323,8 @@ func (s *Scenario) decodeAndValidateTerraformCLIAttribute( // Our value has been set to a string address. moreDiags := findAndLoadCLI(terraformCliVal.AsString()) diags = diags.Extend(moreDiags) - return diags + return diags } // Decode our terraform_cli from the eval context. If it hasn't been defined @@ -599,6 +601,7 @@ func (s *Scenario) decodeAndValidateProvidersAttribute( Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -621,10 +624,12 @@ func (s *Scenario) decodeAndValidateProvidersAttribute( Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } s.Providers = append(s.Providers, provider) + continue } @@ -639,6 +644,7 @@ func (s *Scenario) decodeAndValidateProvidersAttribute( Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -680,6 +686,7 @@ func (s *Scenario) decodeAndValidateStepBlocks( Subject: childBlock.TypeRange.Ptr(), Context: hcl.RangeBetween(childBlock.TypeRange, childBlock.DefRange).Ptr(), }) + continue } @@ -728,6 +735,7 @@ func (s *Scenario) decodeAndValidateOutputBlocks( Subject: childBlock.TypeRange.Ptr(), Context: hcl.RangeBetween(childBlock.TypeRange, childBlock.DefRange).Ptr(), }) + continue } diff --git a/internal/flightplan/scenario_filter.go b/internal/flightplan/scenario_filter.go index bd46030c..77b37c8c 100644 --- a/internal/flightplan/scenario_filter.go +++ b/internal/flightplan/scenario_filter.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ScenarioFilter is a filter for scenarios +// ScenarioFilter is a filter for scenarios. type ScenarioFilter struct { Name string Include *Vector @@ -15,7 +15,7 @@ type ScenarioFilter struct { SelectAll bool } -// String returns the scenario filter as a string +// String returns the scenario filter as a string. func (sf *ScenarioFilter) String() string { if sf == nil { return "" @@ -40,10 +40,10 @@ func (sf *ScenarioFilter) String() string { return str } -// ScenarioFilterOpt is a scenario filter constructor functional option +// ScenarioFilterOpt is a scenario filter constructor functional option. type ScenarioFilterOpt func(*ScenarioFilter) error -// NewScenarioFilter takes in options and returns a new filter +// NewScenarioFilter takes in options and returns a new filter. func NewScenarioFilter(opts ...ScenarioFilterOpt) (*ScenarioFilter, error) { f := &ScenarioFilter{ Include: NewVector(), @@ -59,18 +59,20 @@ func NewScenarioFilter(opts ...ScenarioFilterOpt) (*ScenarioFilter, error) { return f, nil } -// WithScenarioFilterName sets the filter name +// WithScenarioFilterName sets the filter name. func WithScenarioFilterName(name string) ScenarioFilterOpt { return func(f *ScenarioFilter) error { f.Name = name + return nil } } -// WithScenarioFilterSelectAll makes the filter select all +// WithScenarioFilterSelectAll makes the filter select all. func WithScenarioFilterSelectAll() ScenarioFilterOpt { return func(f *ScenarioFilter) error { f.SelectAll = true + return nil } } @@ -80,11 +82,12 @@ func WithScenarioFilterSelectAll() ScenarioFilterOpt { func WithScenarioFilterMatchingVariants(vec *Vector) ScenarioFilterOpt { return func(f *ScenarioFilter) error { f.Include = vec + return nil } } -// WithScenarioFilterParse parses the given filter +// WithScenarioFilterParse parses the given filter. func WithScenarioFilterParse(args []string) ScenarioFilterOpt { return func(f *ScenarioFilter) error { nf, err := ParseScenarioFilter(args) @@ -101,10 +104,11 @@ func WithScenarioFilterParse(args []string) ScenarioFilterOpt { } } -// WithScenarioFilterDecode decodes a filter from a proto Filter +// WithScenarioFilterDecode decodes a filter from a proto Filter. func WithScenarioFilterDecode(filter *pb.Scenario_Filter) ScenarioFilterOpt { return func(f *ScenarioFilter) error { f.FromProto(filter) + return nil } } @@ -114,6 +118,7 @@ func WithScenarioFilterDecode(filter *pb.Scenario_Filter) ScenarioFilterOpt { func WithScenarioFilterFromScenarioRef(ref *pb.Ref_Scenario) ScenarioFilterOpt { return func(f *ScenarioFilter) error { f.FromScenarioRef(ref) + return nil } } @@ -127,8 +132,9 @@ func ParseScenarioFilter(args []string) (*ScenarioFilter, error) { } // No filter args means everything - if len(args) == 0 { + if len(args) == 0 || len(args) == 1 && args[0] == "" { f.SelectAll = true + return f, nil } @@ -141,6 +147,7 @@ func ParseScenarioFilter(args []string) (*ScenarioFilter, error) { return f, fmt.Errorf("invalid variant filter: already found variant name %s and given another %s", f.Name, arg) } f.Name = arg + continue } @@ -160,6 +167,7 @@ func ParseScenarioFilter(args []string) (*ScenarioFilter, error) { return f, fmt.Errorf("invalid variant filter: %w", err) } f.Exclude = append(f.Exclude, ex) + continue } @@ -170,7 +178,7 @@ func ParseScenarioFilter(args []string) (*ScenarioFilter, error) { return f, nil } -// Proto returns the scenario filter as a proto filter +// Proto returns the scenario filter as a proto filter. func (sf *ScenarioFilter) Proto() *pb.Scenario_Filter { pbf := &pb.Scenario_Filter{ Name: sf.Name, @@ -191,8 +199,12 @@ func (sf *ScenarioFilter) Proto() *pb.Scenario_Filter { return pbf } -// FromProto unmarshals a proto filter into itself +// FromProto unmarshals a proto filter into itself. func (sf *ScenarioFilter) FromProto(filter *pb.Scenario_Filter) { + if filter == nil { + return + } + sf.Name = filter.GetName() if i := filter.GetInclude(); i != nil { @@ -213,7 +225,7 @@ func (sf *ScenarioFilter) FromProto(filter *pb.Scenario_Filter) { } } -// FromScenarioRef takes a reference to a scenario and returns a filter for it +// FromScenarioRef takes a reference to a scenario and returns a filter for it. func (sf *ScenarioFilter) FromScenarioRef(ref *pb.Ref_Scenario) { sf.Name = ref.GetId().GetName() sf.Include = NewVectorFromProto(ref.GetId().GetVariants()) @@ -222,6 +234,10 @@ func (sf *ScenarioFilter) FromScenarioRef(ref *pb.Ref_Scenario) { // ScenariosSelect takes a scenario filter and returns a slice of matching // scenarios. func (fp *FlightPlan) ScenariosSelect(f *ScenarioFilter) []*Scenario { + if f == nil { + return nil + } + if f.SelectAll { return fp.Scenarios } diff --git a/internal/flightplan/scenario_filter_test.go b/internal/flightplan/scenario_filter_test.go index 11099142..b964f14e 100644 --- a/internal/flightplan/scenario_filter_test.go +++ b/internal/flightplan/scenario_filter_test.go @@ -11,6 +11,8 @@ import ( // Test_ScenarioFilter_WithScenarioFilterFromScenarioRef tests filtering a // scenario that was created from a scenario reference. func Test_ScenarioFilter_WithScenarioFilterFromScenarioRef(t *testing.T) { + t.Parallel() + ref := &pb.Ref_Scenario{ Id: &pb.Scenario_ID{ Name: "foo", @@ -122,7 +124,10 @@ func Test_ScenarioFilter_ScenariosSelect(t *testing.T) { []*Scenario{}, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp := &FlightPlan{ Scenarios: test.scenarios, } @@ -179,7 +184,10 @@ func Test_ScenarioFilter_Parse(t *testing.T) { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + filter, err := NewScenarioFilter(WithScenarioFilterParse(test.filterArg)) require.NoError(t, err) require.EqualValues(t, test.expected, filter) diff --git a/internal/flightplan/scenario_output.go b/internal/flightplan/scenario_output.go index 05b28cb8..eb67cd19 100644 --- a/internal/flightplan/scenario_output.go +++ b/internal/flightplan/scenario_output.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/hcl/v2/hcldec" ) -// ScenarioOutput represents an "output" block in a scenario +// ScenarioOutput represents an "output" block in a scenario. type ScenarioOutput struct { Name string Description string @@ -15,7 +15,7 @@ type ScenarioOutput struct { Value cty.Value } -// NewScenarioOutput returns a new Output +// NewScenarioOutput returns a new Output. func NewScenarioOutput() *ScenarioOutput { return &ScenarioOutput{Value: cty.NilVal} } diff --git a/internal/flightplan/scenario_output_test.go b/internal/flightplan/scenario_output_test.go index 1f356ace..4dd9c76f 100644 --- a/internal/flightplan/scenario_output_test.go +++ b/internal/flightplan/scenario_output_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_Scenario_Output tests decoding of scenario outputs +// Test_Decode_Scenario_Output tests decoding of scenario outputs. func Test_Decode_Scenario_Output(t *testing.T) { t.Parallel() @@ -189,10 +189,14 @@ scenario "backend" { `, modulePath), }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl), "ENOS_VAR_input=fromenv") if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/scenario_step.go b/internal/flightplan/scenario_step.go index 9f9f6a17..dc7e92c9 100644 --- a/internal/flightplan/scenario_step.go +++ b/internal/flightplan/scenario_step.go @@ -26,7 +26,7 @@ var scenarioStepSchema = &hcl.BodySchema{ }, } -// ScenarioStep is a step in an Enos scenario +// ScenarioStep is a step in an Enos scenario. type ScenarioStep struct { Name string Module *Module @@ -35,7 +35,7 @@ type ScenarioStep struct { Skip bool } -// NewScenarioStep returns a new Scenario step +// NewScenarioStep returns a new Scenario step. func NewScenarioStep() *ScenarioStep { return &ScenarioStep{ Module: NewModule(), @@ -109,8 +109,8 @@ func (ss *ScenarioStep) decode(block *hcl.Block, ctx *hcl.EvalContext) hcl.Diagn return diags } -// decodeSkip decodes the the "skip_step" attribute and returns a boolean and -// diagnostics of whether or not the step should be skipped. +// decodeSkip decodes the "skip_step" attribute and returns a boolean and diagnostics of whether +// or not the step should be skipped. func (ss *ScenarioStep) decodeSkip( content *hcl.BodyContent, ctx *hcl.EvalContext, @@ -220,7 +220,7 @@ func (ss *ScenarioStep) decodeModuleAttribute( // Validate that our module configuration references an existing module. // We only care that the name and source match. All other variables and - // and attributes we'll carry over. + // attributes we'll carry over. mod, ok := modules.AsValueMap()[val.AsString()] if !ok { return module, diags.Append(&hcl.Diagnostic{ @@ -352,7 +352,7 @@ func (ss *ScenarioStep) validateModuleAttributeReference(module *hcl.Attribute, // Validate that our module configuration references an existing module. // We only care that the name and source match. All other variables and - // and attributes we'll carry over. + // attributes we'll carry over. for _, mod := range modules.AsValueSlice() { name, ok := mod.AsValueMap()["name"] if !ok { @@ -387,10 +387,12 @@ func (ss *ScenarioStep) validateModuleAttributeReference(module *hcl.Attribute, Subject: module.Expr.Range().Ptr(), Context: hcl.RangeBetween(module.NameRange, module.Expr.Range()).Ptr(), }) + break } moduleVal = mod + break } @@ -410,7 +412,7 @@ func (ss *ScenarioStep) validateModuleAttributeReference(module *hcl.Attribute, } // decodeAndValidateDependsOn decodess the depends_on attribute and ensures that -// that the values reference known steps. +// the values reference known steps. func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx *hcl.EvalContext) hcl.Diagnostics { var diags hcl.Diagnostics @@ -465,6 +467,7 @@ func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx Subject: depends.Expr.Range().Ptr(), Context: depends.Range.Ptr(), }) + continue } @@ -476,15 +479,17 @@ func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx Subject: depends.Expr.Range().Ptr(), Context: depends.Range.Ptr(), }) + continue } dependsOnSet[depName] = struct{}{} + continue } - // We've been given some other value. Make sure it's a refernce to an - // an existing step, which exists in the eval context as a module value. + // We've been given some other value. Make sure it's a references to an + // existing step, which exists in the eval context as a module value. step := NewModule() err := step.FromCtyValue(depV) if err != nil { @@ -495,6 +500,7 @@ func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx Subject: depends.Expr.Range().Ptr(), Context: depends.Range.Ptr(), }) + continue } @@ -506,12 +512,13 @@ func (ss *ScenarioStep) decodeAndValidateDependsOn(content *hcl.BodyContent, ctx Subject: depends.Expr.Range().Ptr(), Context: depends.Range.Ptr(), }) + continue } dependsOnSet[step.Name] = struct{}{} - continue + continue } for name := range dependsOnSet { @@ -625,6 +632,7 @@ func (ss *ScenarioStep) decodeAndValidateProvidersAttribute(content *hcl.BodyCon Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -649,6 +657,7 @@ func (ss *ScenarioStep) decodeAndValidateProvidersAttribute(content *hcl.BodyCon Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -663,10 +672,12 @@ func (ss *ScenarioStep) decodeAndValidateProvidersAttribute(content *hcl.BodyCon Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } ss.Providers[providerImportName] = provider + continue } @@ -679,6 +690,7 @@ func (ss *ScenarioStep) decodeAndValidateProvidersAttribute(content *hcl.BodyCon Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -701,6 +713,7 @@ func (ss *ScenarioStep) decodeAndValidateProvidersAttribute(content *hcl.BodyCon Subject: providers.Expr.Range().Ptr(), Context: providers.Range.Ptr(), }) + continue } @@ -726,6 +739,7 @@ func (ss *ScenarioStep) copyModuleAttributes(module cty.Value) { return true } } + return false } diff --git a/internal/flightplan/scenario_step_test.go b/internal/flightplan/scenario_step_test.go index 0e4d515c..903d943f 100644 --- a/internal/flightplan/scenario_step_test.go +++ b/internal/flightplan/scenario_step_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_Scenario_Step tests decoding of scenario steps +// Test_Decode_Scenario_Step tests decoding of scenario steps. func Test_Decode_Scenario_Step(t *testing.T) { t.Parallel() @@ -687,10 +687,14 @@ scenario "step_vars" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/scenario_test.go b/internal/flightplan/scenario_test.go index af058059..bb10775b 100644 --- a/internal/flightplan/scenario_test.go +++ b/internal/flightplan/scenario_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_Scenario tests decoding a scenario +// Test_Decode_Scenario tests decoding a scenario. func Test_Decode_Scenario(t *testing.T) { t.Parallel() @@ -132,10 +132,14 @@ scenario "backend" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/schemaless_block.go b/internal/flightplan/schemaless_block.go index 8b5995de..7c4f9b12 100644 --- a/internal/flightplan/schemaless_block.go +++ b/internal/flightplan/schemaless_block.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/hcl/v2/hclsyntax" ) -// SchemalessBlock is our value on HCL block that has no known schema +// SchemalessBlock is our value on HCL block that has no known schema. type SchemalessBlock struct { Type string `cty:"type"` Labels []string `cty:"labels"` @@ -104,7 +104,7 @@ func (s *SchemalessBlock) ToCtyValue() cty.Value { } // FromCtyValue takes a cty.Value and unmarshals it onto itself. It expects -// a valid object created from ToCtyValue() +// a valid object created from ToCtyValue(). func (s *SchemalessBlock) FromCtyValue(val cty.Value) error { if val.IsNull() { return nil diff --git a/internal/flightplan/schemaless_block_test.go b/internal/flightplan/schemaless_block_test.go index a975f2c9..1ced0d10 100644 --- a/internal/flightplan/schemaless_block_test.go +++ b/internal/flightplan/schemaless_block_test.go @@ -96,7 +96,10 @@ provider "is" "anything" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + file, diags := hclsyntax.ParseConfig([]byte(test.body), "in.hcl", hcl.InitialPos) if diags.HasErrors() { t.Fatal(diags.Error()) @@ -178,7 +181,10 @@ func Test_SchemalessBlock_Roundtrip(t *testing.T) { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + val := test.expected.ToCtyValue() got := NewSchemalessBlock() require.NoError(t, got.FromCtyValue(val)) diff --git a/internal/flightplan/step_variable.go b/internal/flightplan/step_variable.go index 419bf50c..3b502976 100644 --- a/internal/flightplan/step_variable.go +++ b/internal/flightplan/step_variable.go @@ -14,17 +14,17 @@ import ( // StepVariableType is a cty capsule type that represents "step" variables. // Step variables might be known values or unknown references to -// to module outputs. Due to the complex nature of these values we have our +// module outputs. Due to the complex nature of these values we have our // cty Type to carry this information for us. var StepVariableType cty.Type -// StepVariable is the type encapsulated in StepVariableType +// StepVariable is the type encapsulated in StepVariableType. type StepVariable struct { Value cty.Value Traversal hcl.Traversal } -// StepVariableVal returns a new cty.Value of type StepVariableType +// StepVariableVal returns a new cty.Value of type StepVariableType. func StepVariableVal(stepVar *StepVariable) cty.Value { return cty.CapsuleVal(StepVariableType, stepVar) } @@ -100,6 +100,7 @@ func absTraversalForExpr(expr hcl.Expression, ctx *hcl.EvalContext) (hcl.Travers if moreDiags.HasErrors() { // Return the core expression diags to help troubleshooting _, moreDiags := expr.Value(ctx) + return traversal, diags.Extend(moreDiags) } @@ -143,6 +144,7 @@ func absTraversalForExpr(expr hcl.Expression, ctx *hcl.EvalContext) (hcl.Travers // easier for the author we'll return both the expression and // absolute traversal diagnostics to ease in solving the problem. _, exprDiags := expr.Value(ctx) + return traversal, exprDiags.Extend(diags) } } @@ -168,7 +170,7 @@ func init() { // known values and static analysis for unknown module // output references. We'll do our best to support complex // references but they have to be absolute traversals to - // to "step"'s in the evaluation context. + // "step"'s in the evaluation context. var diags hcl.Diagnostics stepVar := &StepVariable{ Value: cty.NilVal, @@ -188,6 +190,7 @@ func init() { } stepVar.Value = absVal + return StepVariableVal(stepVar), diags } @@ -245,6 +248,7 @@ func init() { } stepVar.Traversal = traversal + return StepVariableVal(stepVar), diags }, ) @@ -257,11 +261,13 @@ func init() { }, GoString: func(raw any) string { stepVar, _ := raw.(*StepVariable) + return fmt.Sprintf("flightplan.StepVariable(%#v)", stepVar) }, RawEquals: func(a, b any) bool { stepVarA, _ := a.(*StepVariable) stepVarB, _ := b.(*StepVariable) + return (stepVarA.Value == stepVarB.Value) && reflect.DeepEqual(stepVarA.Traversal, stepVarB.Traversal) }, diff --git a/internal/flightplan/step_variable_test.go b/internal/flightplan/step_variable_test.go index 53ca74e0..ce1297cb 100644 --- a/internal/flightplan/step_variable_test.go +++ b/internal/flightplan/step_variable_test.go @@ -17,6 +17,7 @@ func testMakeStepVarTraversal(parts ...string) cty.Value { for i, part := range parts { if i == 0 { traversal = append(traversal, hcl.TraverseRoot{Name: part}) + continue } traversal = append(traversal, hcl.TraverseAttr{Name: part}) @@ -159,7 +160,10 @@ func Test_StepVariableType_Decode(t *testing.T) { value: testMakeStepVarValue(cty.ListVal([]cty.Value{cty.StringVal("*")})), }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + file, diags := hclsyntax.ParseConfig([]byte(test.body), "in.hcl", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { t.Fatal(diags.Error()) diff --git a/internal/flightplan/terraform_cli.go b/internal/flightplan/terraform_cli.go index 576d704b..c7fc67bd 100644 --- a/internal/flightplan/terraform_cli.go +++ b/internal/flightplan/terraform_cli.go @@ -120,7 +120,7 @@ var terraformCLISpec = hcldec.ObjectSpec{ }, } -// terraformCLISchema are the pieces of terraform CLI configuration we care about +// terraformCLISchema are the pieces of terraform CLI configuration we care about. var terraformCLISchema = &hcl.BodySchema{ Attributes: []hcl.AttributeSchema{ {Name: "path"}, @@ -128,15 +128,15 @@ var terraformCLISchema = &hcl.BodySchema{ }, } -// TerraformCLI is a Terraform CLI configuration +// TerraformCLI is a Terraform CLI configuration. type TerraformCLI struct { - Name string `hcl:"name" cty:"name"` - Path string `hcl:"path,optional" cty:"path"` - Env map[string]string `hcl:"env,optional" cty:"env"` - ConfigVal cty.Value `hcl:"config,optional" cty:"config"` + Name string `cty:"name" hcl:"name"` + Path string `cty:"path" hcl:"path,optional"` + Env map[string]string `cty:"env" hcl:"env,optional"` + ConfigVal cty.Value `cty:"config" hcl:"config,optional"` } -// NewTerraformCLI returns a new TerraformCLI +// NewTerraformCLI returns a new TerraformCLI. func NewTerraformCLI() *TerraformCLI { return &TerraformCLI{ Env: map[string]string{}, @@ -152,6 +152,7 @@ func DefaultTerraformCLI() *TerraformCLI { cli.Name = "default" cli.ConfigVal = cty.NilVal cli.Path, _ = exec.LookPath("terraform") + return cli } diff --git a/internal/flightplan/terraform_cli_test.go b/internal/flightplan/terraform_cli_test.go index 670d0adf..527f2f4f 100644 --- a/internal/flightplan/terraform_cli_test.go +++ b/internal/flightplan/terraform_cli_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_TerraformCLI +// Test_Decode_TerraformCLI. func Test_Decode_TerraformCLI(t *testing.T) { t.Parallel() @@ -432,10 +432,14 @@ scenario "ref" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) diff --git a/internal/flightplan/terraform_setting.go b/internal/flightplan/terraform_setting.go index cedb7515..323f8bf2 100644 --- a/internal/flightplan/terraform_setting.go +++ b/internal/flightplan/terraform_setting.go @@ -9,7 +9,7 @@ import ( "github.com/hashicorp/hcl/v2/hcldec" ) -// TerraformSetting is a terraform settings configuration +// TerraformSetting is a terraform settings configuration. type TerraformSetting struct { Name string RequiredVersion cty.Value @@ -22,14 +22,14 @@ type TerraformSetting struct { Cloud cty.Value } -// TerraformSettingBackend is the "backend" +// TerraformSettingBackend is the "backend". type TerraformSettingBackend struct { Name string Attrs map[string]cty.Value Workspaces cty.Value } -// NewTerraformSetting returns a new TerraformSetting +// NewTerraformSetting returns a new TerraformSetting. func NewTerraformSetting() *TerraformSetting { return &TerraformSetting{ RequiredVersion: cty.NullVal(cty.String), @@ -41,7 +41,7 @@ func NewTerraformSetting() *TerraformSetting { } } -// NewTerraformSettingBackend returns a new TerraformSettingBackend +// NewTerraformSettingBackend returns a new TerraformSettingBackend. func NewTerraformSettingBackend() *TerraformSettingBackend { return &TerraformSettingBackend{ Attrs: map[string]cty.Value{}, @@ -166,7 +166,7 @@ func (t *TerraformSetting) decodeExperiments(ctx *hcl.EvalContext, body hcl.Body return remain, diags } -// decodeCloud decodes the "cloud" block +// decodeCloud decodes the "cloud" block. func (t *TerraformSetting) decodeCloud(ctx *hcl.EvalContext, body hcl.Body) (hcl.Body, hcl.Diagnostics) { var diags hcl.Diagnostics var remain hcl.Body @@ -283,7 +283,7 @@ func (t *TerraformSetting) decodeRequiredProviders(ctx *hcl.EvalContext, content return diags } -// decodeProviderMeta decodes the "provider_meta" block +// decodeProviderMeta decodes the "provider_meta" block. func (t *TerraformSetting) decodeProviderMeta(ctx *hcl.EvalContext, content *hcl.BodyContent) hcl.Diagnostics { var diags hcl.Diagnostics @@ -312,7 +312,7 @@ func (t *TerraformSetting) decodeProviderMeta(ctx *hcl.EvalContext, content *hcl return diags } -// decodeBackend decodes the "backend" block +// decodeBackend decodes the "backend" block. func (t *TerraformSetting) decodeBackend(ctx *hcl.EvalContext, content *hcl.BodyContent) hcl.Diagnostics { var diags hcl.Diagnostics @@ -325,6 +325,7 @@ func (t *TerraformSetting) decodeBackend(ctx *hcl.EvalContext, content *hcl.Body Subject: block.TypeRange.Ptr(), Context: block.DefRange.Ptr(), }) + continue } diff --git a/internal/flightplan/terraform_setting_test.go b/internal/flightplan/terraform_setting_test.go index 3fc49130..7193d545 100644 --- a/internal/flightplan/terraform_setting_test.go +++ b/internal/flightplan/terraform_setting_test.go @@ -9,7 +9,7 @@ import ( "github.com/zclconf/go-cty/cty" ) -// Test_Decode_TerraformSettings +// Test_Decode_TerraformSettings. func Test_Decode_TerraformSettings(t *testing.T) { t.Parallel() @@ -897,10 +897,14 @@ scenario "default" { }, }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + fp, err := testDecodeHCL(t, []byte(test.hcl)) if test.fail { require.Error(t, err) + return } require.NoError(t, err) @@ -910,6 +914,8 @@ scenario "default" { } func Test_TerraformSettings_Cty_RoundTrip(t *testing.T) { + t.Parallel() + setting := &TerraformSetting{ Name: "default", RequiredVersion: cty.StringVal(">= 1.1.0"), diff --git a/internal/flightplan/variable.go b/internal/flightplan/variable.go index 4299470d..6bab03af 100644 --- a/internal/flightplan/variable.go +++ b/internal/flightplan/variable.go @@ -35,7 +35,7 @@ type Variable struct { ConstraintType cty.Type } -// VariableValue is a user supplied variable value +// VariableValue is a user supplied variable value. type VariableValue struct { Expr hcl.Expression Range hcl.Range @@ -53,13 +53,12 @@ const ( const EnvVarPrefix = "ENOS_VAR_" -// NewVariable returns a new Variable +// NewVariable returns a new Variable. func NewVariable() *Variable { return &Variable{} } -// decode takes in an HCL block of a variable and any set variable values and -// and decodes itself. +// decode takes in an HCL block of a variable and any set variable values and decodes itself. func (v *Variable) decode(block *hcl.Block, values map[string]*VariableValue) hcl.Diagnostics { var diags hcl.Diagnostics @@ -138,13 +137,33 @@ func (v *Variable) decode(block *hcl.Block, values map[string]*VariableValue) hc v.SetValue = cty.StringVal(setVal.EnvVarRaw) } } - default: + case VariableValueSourceVarsFile: // We have a value that isn't from the environment so we'll get // the value of the expression. val, moreDiags := setVal.Expr.Value(nil) diags = diags.Extend(moreDiags) if moreDiags.HasErrors() { v.SetValue = cty.DynamicVal + + return diags + } + v.SetValue = val + case VariableValueSourceUnknown: + // This should never happen but we'll try and evaluate the expression. + val, moreDiags := setVal.Expr.Value(nil) + diags = diags.Extend(moreDiags) + if moreDiags.HasErrors() { + v.SetValue = cty.DynamicVal + + return diags + } + v.SetValue = val + default: + val, moreDiags := setVal.Expr.Value(nil) + diags = diags.Extend(moreDiags) + if moreDiags.HasErrors() { + v.SetValue = cty.DynamicVal + return diags } v.SetValue = val diff --git a/internal/flightplan/variable_test.go b/internal/flightplan/variable_test.go index 7df5002c..5efbbc8e 100644 --- a/internal/flightplan/variable_test.go +++ b/internal/flightplan/variable_test.go @@ -11,6 +11,8 @@ import ( ) func Test_Decode_Variable(t *testing.T) { + t.Parallel() + fakeRng := hcl.Range{ Filename: "notreal", Start: hcl.Pos{Line: 1, Column: 1}, @@ -151,7 +153,10 @@ variable "complex" { }), }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + parser := hclparse.NewParser() f, diags := parser.ParseHCL([]byte(test.enosCfg), "variable.hcl") require.False(t, diags.HasErrors(), diags.Error()) diff --git a/internal/generate/generate.go b/internal/generate/generate.go index dac20647..74dfab92 100644 --- a/internal/generate/generate.go +++ b/internal/generate/generate.go @@ -15,10 +15,10 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" ) -// Opt is a generate module option +// Opt is a generate module option. type Opt func(*Generator) error -// Generator is a request to generate a Terraform module +// Generator is a request to generate a Terraform module. type Generator struct { Scenario *flightplan.Scenario BaseDir string @@ -26,7 +26,7 @@ type Generator struct { UI cli.Ui } -// NewGenerator takes options and returns a new validated generator +// NewGenerator takes options and returns a new validated generator. func NewGenerator(opts ...Opt) (*Generator, error) { req := &Generator{UI: cli.NewMockUi()} @@ -61,6 +61,7 @@ func WithOutBaseDirectory(dir string) Opt { } req.OutDir = a + return nil } } @@ -73,22 +74,25 @@ func WithScenarioBaseDirectory(dir string) Opt { return err } req.BaseDir = a + return nil } } -// WithScenario is the scenario to generate into a module +// WithScenario is the scenario to generate into a module. func WithScenario(s *flightplan.Scenario) Opt { return func(req *Generator) error { req.Scenario = s + return nil } } -// WithUI is the UI to use for outputing information +// WithUI is the UI to use for outputing information. func WithUI(ui cli.Ui) Opt { return func(req *Generator) error { req.UI = ui + return nil } } @@ -127,7 +131,7 @@ func absoluteNoSymlinks(path string) (string, error) { return filepath.EvalSymlinks(a) } -// Generate converts the Scenario into a terraform module +// Generate converts the Scenario into a terraform module. func (g *Generator) Generate() error { err := g.generateCLIConfig() if err != nil { @@ -138,18 +142,18 @@ func (g *Generator) Generate() error { } // TerraformRCPath is where the generated terraform.rc configuration file will -// be written +// be written. func (g *Generator) TerraformRCPath() string { return filepath.Join(g.TerraformModuleDir(), "terraform.rc") } // TerraformModulePath is where the generated Terraform module file will -// be written +// be written. func (g *Generator) TerraformModulePath() string { return filepath.Join(g.TerraformModuleDir(), "scenario.tf") } -// TerraformModuleDir is the directory where the generated Terraform +// TerraformModuleDir is the directory where the generated Terraform. func (g *Generator) TerraformModuleDir() string { return filepath.Join(g.OutDir, g.Scenario.UID()) } @@ -290,11 +294,13 @@ func (g *Generator) generateModule() error { func (g *Generator) ensureOutDir() error { _, err := ensureDir(g.TerraformModuleDir()) + return err } // maybeWriteTerraformSettings writes any configured "terraform" settings -// nolint:cyclop +// +//nolint:cyclop,gocyclo // writing out our terraform settings is complicated. func (g *Generator) maybeWriteTerraformSettings(rootBody *hclwrite.Body) { s := g.Scenario.TerraformSetting if s == nil { @@ -497,6 +503,7 @@ func (g *Generator) convertStepsToModules(rootBody *hclwrite.Body) error { // Use the absolute value if stepVar.Value != cty.NilVal { body.SetAttributeValue(k, stepVar.Value) + continue } @@ -552,6 +559,7 @@ func (g *Generator) maybeWriteOutputs(rootBody *hclwrite.Body) error { // Use the absolute value if it exists if stepVar.Value != cty.NilVal { body.SetAttributeValue("value", stepVar.Value) + return nil } @@ -590,6 +598,7 @@ func (g *Generator) write(path string, bytes []byte) error { defer file.Close() _, err = file.Write(hclwrite.Format(bytes)) + return err } @@ -732,6 +741,7 @@ func dependsOnTokens(names []string) hclwrite.Tokens { Bytes: []byte{'['}, }) tokens = append(tokens, moduleRef(names[0])...) + return append(tokens, &hclwrite.Token{ Type: hclsyntax.TokenOBrack, Bytes: []byte{']'}, @@ -867,7 +877,7 @@ func relativePath(from, to string) (string, error) { } // stepToModuleTraversal takes a "step" traversal and updates the root of the -// the traversal to "module" +// traversal to "module". func stepToModuleTraversal(in hcl.Traversal) error { if len(in) == 0 { return nil diff --git a/internal/generate/generate_test.go b/internal/generate/generate_test.go index 0f65f426..fd7fe3a2 100644 --- a/internal/generate/generate_test.go +++ b/internal/generate/generate_test.go @@ -18,7 +18,9 @@ func Test_MaybeUpdateRelativeSourcePaths(t *testing.T) { tmpDir, err := os.MkdirTemp("", "update_source_paths") require.NoError(t, err) - defer os.RemoveAll(tmpDir) + t.Cleanup(func() { + defer os.RemoveAll(tmpDir) + }) baseDir := filepath.Join(tmpDir, "scenarios/test") moduleDir := filepath.Join(baseDir, "modules/foo") @@ -114,7 +116,10 @@ func Test_MaybeUpdateRelativeSourcePaths(t *testing.T) { expected: "gcs::https://www.googleapis.com/storage/v1/modules/foomodule.zip", }, } { + test := test t.Run(test.desc, func(t *testing.T) { + t.Parallel() + bd := baseDir if test.baseDir != "" { bd = test.baseDir diff --git a/internal/operation/command/command.go b/internal/operation/command/command.go index b14b3592..4713749c 100644 --- a/internal/operation/command/command.go +++ b/internal/operation/command/command.go @@ -10,21 +10,21 @@ import ( "github.com/hashicorp/enos/internal/ui/terminal" ) -// Command is a functional options wrapper around exec.Cmd +// Command is a functional options wrapper around exec.Cmd. type Command struct { Name string EnvPassthrough bool ExecOpts []ExecOpt } -// Opt is a functional options wrapper around Command +// Opt is a functional options wrapper around Command. type Opt func(*Command) // ExecOpt is a functional options wrapper around *exec.Cmd. These options // are applied to the Cmd instance before it is run. type ExecOpt func(*exec.Cmd) -// NewCommand creates a new command +// NewCommand creates a new command. func NewCommand(name string, opts ...Opt) *Command { cmd := &Command{ Name: name, @@ -50,7 +50,7 @@ func WithUI(ui *terminal.UI) Opt { } } -// WithArgs sets the command arguments +// WithArgs sets the command arguments. func WithArgs(args ...string) Opt { return func(cmd *Command) { cmd.ExecOpts = append(cmd.ExecOpts, func(ecmd *exec.Cmd) { @@ -59,7 +59,7 @@ func WithArgs(args ...string) Opt { } } -// WithDir sets the command directory +// WithDir sets the command directory. func WithDir(dir string) Opt { return func(cmd *Command) { cmd.ExecOpts = append(cmd.ExecOpts, func(ecmd *exec.Cmd) { @@ -68,7 +68,7 @@ func WithDir(dir string) Opt { } } -// WithEnv sets the command envrionment variables +// WithEnv sets the command environment variables. func WithEnv(vars map[string]string) Opt { return func(cmd *Command) { env := []string{} @@ -82,15 +82,16 @@ func WithEnv(vars map[string]string) Opt { } } -// WithEnvPassthrough passes the current process environment to the command +// WithEnvPassthrough passes the current process environment to the command. func WithEnvPassthrough() Opt { return func(cmd *Command) { cmd.EnvPassthrough = true } } -// Cmd takes a context and returns an instance of *exec.Cmd +// Cmd takes a context and returns an instance of *exec.Cmd. func (c *Command) Cmd(ctx context.Context) *exec.Cmd { + //nolint:gosec // G204 We know we're passing through to exec. cmd := exec.CommandContext(ctx, c.Name) if c.EnvPassthrough { cmd.Env = os.Environ() @@ -98,6 +99,7 @@ func (c *Command) Cmd(ctx context.Context) *exec.Cmd { for _, opt := range c.ExecOpts { opt(cmd) } + return cmd } @@ -106,5 +108,6 @@ func (c *Command) Cmd(ctx context.Context) *exec.Cmd { func (c *Command) Run(ctx context.Context) (*exec.Cmd, error) { cmd := c.Cmd(ctx) err := cmd.Run() + return cmd, err } diff --git a/internal/operation/event_publisher.go b/internal/operation/event_publisher.go index 05554e36..72b02872 100644 --- a/internal/operation/event_publisher.go +++ b/internal/operation/event_publisher.go @@ -8,20 +8,20 @@ import ( "github.com/hashicorp/go-hclog" ) -// Unsubscriber is a func that unsubscribes that subscriber from the publisher +// Unsubscriber is a func that unsubscribes that subscriber from the publisher. type Unsubscriber func() -// Publisher is the operation event publisher +// Publisher is the operation event publisher. type Publisher struct { subscribers map[string]Subscribers // operation id -> subscribers mu sync.RWMutex log hclog.Logger } -// PublisherOpt is a NewPublisher option +// PublisherOpt is a NewPublisher option. type PublisherOpt func(*Publisher) -// NewPublisher returns a new instance of the publisher +// NewPublisher returns a new instance of the publisher. func NewPublisher(opts ...PublisherOpt) *Publisher { p := &Publisher{ subscribers: map[string]Subscribers{}, @@ -36,7 +36,7 @@ func NewPublisher(opts ...PublisherOpt) *Publisher { return p } -// WithPublisherLog sets the logger on the publisher +// WithPublisherLog sets the logger on the publisher. func WithPublisherLog(log hclog.Logger) PublisherOpt { return func(p *Publisher) { p.log = log @@ -65,7 +65,7 @@ func (p *Publisher) Subscribe(s *Subscriber) Unsubscriber { } } -// Unsubscribe unsubscribes a subscriber +// Unsubscribe unsubscribes a subscriber. func (p *Publisher) Unsubscribe(s *Subscriber) { p.mu.Lock() defer p.mu.Unlock() @@ -82,7 +82,7 @@ func (p *Publisher) Unsubscribe(s *Subscriber) { ) } -// Publish publishes an operation event to listners for the operationID +// Publish publishes an operation event to listeners for the operationID. func (p *Publisher) Publish(event *pb.Operation_Event) error { if event == nil { return nil @@ -98,6 +98,7 @@ func (p *Publisher) Publish(event *pb.Operation_Event) error { s.mu.RLock() if !s.active { s.mu.RUnlock() + return nil } s.mu.RUnlock() @@ -108,6 +109,7 @@ func (p *Publisher) Publish(event *pb.Operation_Event) error { err := proto.Copy(event, newEvent) if err != nil { log.Error("unable to copy operation", "err", err) + return err } @@ -119,7 +121,7 @@ func (p *Publisher) Publish(event *pb.Operation_Event) error { return nil } -// Stop closes all subscribers and removes and clears the subscribers list +// Stop closes all subscribers and removes and clears the subscribers list. func (p *Publisher) Stop() { p.mu.Lock() defer p.mu.Unlock() diff --git a/internal/operation/event_subscriber.go b/internal/operation/event_subscriber.go index 3abf378e..92e816e2 100644 --- a/internal/operation/event_subscriber.go +++ b/internal/operation/event_subscriber.go @@ -9,10 +9,10 @@ import ( "github.com/hashicorp/go-hclog" ) -// Subscribers are operation event subscribers +// Subscribers are operation event subscribers. type Subscribers map[string]*Subscriber -// Subscriber is an event subscriber +// Subscriber is an event subscriber. type Subscriber struct { mu sync.RWMutex ID string @@ -23,10 +23,10 @@ type Subscriber struct { once sync.Once } -// SubscriberOpt is a new subscriber option +// SubscriberOpt is a new subscriber option. type SubscriberOpt func(*Subscriber) -// NewSubscriber takes an operation request and returns a new subscriber instance +// NewSubscriber takes an operation request and returns a new subscriber instance. func NewSubscriber( ref *pb.Ref_Operation, opts ...SubscriberOpt, @@ -56,14 +56,14 @@ func NewSubscriber( return s, nil } -// WithSubscriberLog sets the subscriber logger +// WithSubscriberLog sets the subscriber logger. func WithSubscriberLog(log hclog.Logger) SubscriberOpt { return func(s *Subscriber) { s.log = log } } -// Close closes the subscribers +// Close closes the subscribers. func (s *Subscriber) Close() { s.mu.Lock() defer s.mu.Unlock() diff --git a/internal/operation/flightplan.go b/internal/operation/flightplan.go index 3841a108..49a43e37 100644 --- a/internal/operation/flightplan.go +++ b/internal/operation/flightplan.go @@ -1,6 +1,7 @@ package operation import ( + "context" "path/filepath" "github.com/hashicorp/enos/internal/diagnostics" @@ -20,7 +21,7 @@ func outDirForWorkspace(w *pb.Workspace) string { return filepath.Join(w.GetFlightplan().GetBaseDir(), ".enos") } -func decodeFlightPlan(pfp *pb.FlightPlan) (*flightplan.FlightPlan, *pb.DecodeResponse) { +func decodeFlightPlan(ctx context.Context, pfp *pb.FlightPlan) (*flightplan.FlightPlan, *pb.DecodeResponse) { res := &pb.DecodeResponse{ Diagnostics: []*pb.Diagnostic{}, } @@ -33,6 +34,7 @@ func decodeFlightPlan(pfp *pb.FlightPlan) (*flightplan.FlightPlan, *pb.DecodeRes ) if err != nil { res.Diagnostics = diagnostics.FromErr(err) + return nil, res } @@ -45,7 +47,7 @@ func decodeFlightPlan(pfp *pb.FlightPlan) (*flightplan.FlightPlan, *pb.DecodeRes return nil, res } - fp, hclDiags := dec.Decode() + fp, hclDiags := dec.Decode(ctx) if len(hclDiags) > 0 { res.Diagnostics = append(res.Diagnostics, diagnostics.FromHCL(dec.ParserFiles(), hclDiags)...) } diff --git a/internal/operation/operation.go b/internal/operation/operation.go index b348df26..7e80967e 100644 --- a/internal/operation/operation.go +++ b/internal/operation/operation.go @@ -225,6 +225,7 @@ func ResponseTypeString(op *pb.Operation_Response) string { } } +//nolint:unparam // right now all callers use pb.Operation_STATUS_RUNNING but that's not guaranteed. func newEvent( ref *pb.Ref_Operation, status pb.Operation_Status, @@ -305,7 +306,6 @@ func NewEventFromResponse(res *pb.Operation_Response) (*pb.Operation_Event, erro newEvent.GetDiagnostics(), diagnostics.FromErr(err)..., ) - } return newEvent, merr.ErrorOrNil() @@ -379,6 +379,8 @@ func hasFailedStatus(s pb.Operation_Status) bool { switch s { case pb.Operation_STATUS_CANCELLED, pb.Operation_STATUS_FAILED: return true + case pb.Operation_STATUS_UNSPECIFIED, pb.Operation_STATUS_UNKNOWN, pb.Operation_STATUS_QUEUED, pb.Operation_STATUS_WAITING, pb.Operation_STATUS_RUNNING, pb.Operation_STATUS_RUNNING_WARNING, pb.Operation_STATUS_COMPLETED, pb.Operation_STATUS_COMPLETED_WARNING: + return false default: return false } diff --git a/internal/operation/operator.go b/internal/operation/operator.go index 48ce11aa..9b16e57c 100644 --- a/internal/operation/operator.go +++ b/internal/operation/operator.go @@ -8,7 +8,7 @@ import ( ) var ( - // These have all been made up and have no scientific backing + // These have all been made up and have no scientific backing. // DefaultOperatorWorkerCount is how many workers to run. The number of // parallel operations is limited to the number of workers. diff --git a/internal/operation/operator_local.go b/internal/operation/operator_local.go index 55c06d45..423c008e 100644 --- a/internal/operation/operator_local.go +++ b/internal/operation/operator_local.go @@ -17,7 +17,7 @@ import ( var _ Operator = (*LocalOperator)(nil) -// LocalOperator is an in-memory implementation of the server Operator +// LocalOperator is an in-memory implementation of the server Operator. type LocalOperator struct { mu sync.Mutex state state.State @@ -30,10 +30,10 @@ type LocalOperator struct { publisher *Publisher } -// LocalOperatorOpt is a functional option to configure a new LocalOperator +// LocalOperatorOpt is a functional option to configure a new LocalOperator. type LocalOperatorOpt func(*LocalOperator) -// NewLocalOperator returns a new instance of a LocalOperator +// NewLocalOperator returns a new instance of a LocalOperator. func NewLocalOperator(opts ...LocalOperatorOpt) *LocalOperator { l := &LocalOperator{ mu: sync.Mutex{}, @@ -53,14 +53,14 @@ func NewLocalOperator(opts ...LocalOperatorOpt) *LocalOperator { return l } -// WithLocalOperatorState is a state setter for a new LocalOperator +// WithLocalOperatorState is a state setter for a new LocalOperator. func WithLocalOperatorState(s state.State) LocalOperatorOpt { return func(l *LocalOperator) { l.state = s } } -// WithLocalOperatorLog is a log setter for a new LocalOperator +// WithLocalOperatorLog is a log setter for a new LocalOperator. func WithLocalOperatorLog(log hclog.Logger) LocalOperatorOpt { return func(l *LocalOperator) { l.log = log @@ -95,7 +95,6 @@ func (o *LocalOperator) Start(ctx context.Context) error { for i := int32(0); i < o.workerCount; i++ { go newWorker( - ctx, fmt.Sprintf("%d", i), o.workRequests, o.workEvents, @@ -103,7 +102,7 @@ func (o *LocalOperator) Start(ctx context.Context) error { func(res *pb.Operation_Response) error { return o.state.UpsertOperationResponse(res) }, - ).run() + ).run(ctx) } o.running = true @@ -121,16 +120,17 @@ func (o *LocalOperator) Stop() error { o.ctxCancel() // Cancel in-flight operations, kill operation workers, drain the event queue } o.publisher.Stop() // Turn off event publisher + return nil } -// State returns the operators state +// State returns the operators state. func (o *LocalOperator) State() state.State { return o.state } // Dispatch takes an operation request and attempts to dispatch it for execution -// by the operators worker pool. If the request is successfully coverted into +// by the operators worker pool. If the request is successfully converted into // a work operation and queued it will return a reference for the operation to // the caller. func (o *LocalOperator) Dispatch( @@ -144,12 +144,14 @@ func (o *LocalOperator) Dispatch( ref, err := NewReferenceFromRequest(req) if err != nil { log.Error("failed to generate scenario reference from request", "error", err) + return ref, diagnostics.FromErr(err) } if !o.running { err = fmt.Errorf("unable to dispatch new operations as operator is not running") log.Error("failed to dispatch operation", "error", err) + return ref, diagnostics.FromErr(err) } @@ -159,6 +161,7 @@ func (o *LocalOperator) Dispatch( opUUID, err := uuid.NewRandom() if err != nil { log.Error("failed to generate operation id", "error", err) + return ref, diagnostics.FromErr(err) } @@ -170,6 +173,7 @@ func (o *LocalOperator) Dispatch( workReq, err := newWorkReqForOpReq(req) if err != nil { log.Error("failed to determine operation func for request", "error", err) + return ref, diagnostics.FromErr(err) } @@ -178,6 +182,7 @@ func (o *LocalOperator) Dispatch( queueRes, err := NewResponseFromRequest(req) if err != nil { log.Error("unable to create new response for request") + return ref, diagnostics.FromErr(err) } queueRes.Op = ref @@ -185,6 +190,7 @@ func (o *LocalOperator) Dispatch( err = o.state.UpsertOperationResponse(queueRes) if err != nil { log.Error("failed to commit operation response", "error", err) + return ref, diagnostics.FromErr(err) } @@ -212,13 +218,14 @@ func (o *LocalOperator) Dispatch( // the operation, an unsubscriber function, and an error. The subscriber can be // used to publish and receive events from the event stream. When the unsubscriber // function is called the operators event handler will stop publishing events to -// to the subscriber. +// the subscriber. func (o *LocalOperator) Stream(op *pb.Ref_Operation) (*Subscriber, Unsubscriber, error) { sub, err := NewSubscriber(op, WithSubscriberLog(o.log.Named("subscriber").Named(op.GetId())), ) if err != nil { o.log.Error("unable to create new operation subscriber", "error", err) + return nil, nil, err } @@ -245,7 +252,7 @@ func (o *LocalOperator) Stream(op *pb.Ref_Operation) (*Subscriber, Unsubscriber, return sub, unsubscribe, nil } -// Response takes a reference to an operation and retuns the response. If no +// Response takes a reference to an operation and returns the response. If no // response is found nil will be returned. func (o *LocalOperator) Response(op *pb.Ref_Operation) (*pb.Operation_Response, error) { return o.state.GetOperationResponse(op) @@ -278,6 +285,7 @@ func (o *LocalOperator) drainWorkReqQueue() { event, err := NewEventFromResponse(res) if err != nil { log.Error("unable to create event from response", "error", err) + return } @@ -296,6 +304,7 @@ func (o *LocalOperator) drainWorkReqQueue() { select { case <-gracefulCtx.Done(): o.log.Error("failed to drain work request queue") + return default: } @@ -303,6 +312,7 @@ func (o *LocalOperator) drainWorkReqQueue() { select { case <-gracefulCtx.Done(): o.log.Error("failed to drain work request queue") + return case event := <-o.workRequests: cancelWorkReq(event) @@ -340,6 +350,7 @@ func (o *LocalOperator) startEventHandler(ctx context.Context) { select { case <-gracefulCtx.Done(): log.Error("failed to drain event queue, stopping") + return default: } @@ -347,11 +358,13 @@ func (o *LocalOperator) startEventHandler(ctx context.Context) { select { case <-gracefulCtx.Done(): log.Error("failed to drain event queue, stopping") + return case event := <-o.workEvents: handleEvent(event) default: log.Debug("stopped") + return } } diff --git a/internal/operation/runner.go b/internal/operation/runner.go index 460d8238..b28afa81 100644 --- a/internal/operation/runner.go +++ b/internal/operation/runner.go @@ -9,17 +9,17 @@ import ( "github.com/hashicorp/go-hclog" ) -// RunnerOpt is a validate module option +// RunnerOpt is a validate module option. type RunnerOpt func(*Runner) -// Runner is a Terraform command runner +// Runner is a Terraform command runner. type Runner struct { TFConfig *terraform.Config Module *pb.Terraform_Module log hclog.Logger } -// NewTextOutput returns a new TextOutput +// NewTextOutput returns a new TextOutput. func NewTextOutput() *TextOutput { return &TextOutput{ // Stdout is currently discarded because we don't do anything with // terraform's raw output. @@ -28,13 +28,13 @@ func NewTextOutput() *TextOutput { } } -// TextOutput is a terraform text output collector +// TextOutput is a terraform text output collector. type TextOutput struct { Stdout io.Writer Stderr *strings.Builder } -// NewRunner takes options and returns a new validated generator +// NewRunner takes options and returns a new validated generator. func NewRunner(opts ...RunnerOpt) *Runner { ex := &Runner{ log: hclog.NewNullLogger(), @@ -55,7 +55,7 @@ func WithRunnerTerraformConfig(cfg *pb.Terraform_Runner_Config) RunnerOpt { } } -// WithLogger sets the logger +// WithLogger sets the logger. func WithLogger(log hclog.Logger) RunnerOpt { return func(ex *Runner) { ex.log = log diff --git a/internal/operation/runner_module_generate.go b/internal/operation/runner_module_generate.go index 4dc86721..a06e3f57 100644 --- a/internal/operation/runner_module_generate.go +++ b/internal/operation/runner_module_generate.go @@ -31,6 +31,7 @@ func (r *Runner) moduleGenerate( if err != nil { resVal.Generate.Diagnostics = append(resVal.Generate.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return resVal } @@ -41,6 +42,7 @@ func (r *Runner) moduleGenerate( if err = events.Publish(event); err != nil { resVal.Generate.Diagnostics = append(resVal.Generate.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to send event", "error", err) + return resVal } @@ -62,16 +64,18 @@ func (r *Runner) moduleGenerate( select { case <-ctx.Done(): notifyFail(diagnostics.FromErr(ctx.Err())) + return resVal default: } // Decode our scenario and create our module generator - gen, scenario, diags := scenarioAndModuleGeneratorForReq(req) + gen, scenario, diags := scenarioAndModuleGeneratorForReq(ctx, req) if diagnostics.HasFailed( req.GetWorkspace().GetTfExecCfg().GetFailOnWarnings(), diags) { notifyFail(diags) + return resVal } @@ -79,6 +83,7 @@ func (r *Runner) moduleGenerate( err = gen.Generate() if err != nil { notifyFail(diagnostics.FromErr(err)) + return resVal } @@ -107,7 +112,7 @@ func (r *Runner) moduleGenerate( return resVal } -func scenarioAndModuleGeneratorForReq(req *pb.Operation_Request) ( +func scenarioAndModuleGeneratorForReq(ctx context.Context, req *pb.Operation_Request) ( *generate.Generator, *flightplan.Scenario, []*pb.Diagnostic, @@ -123,7 +128,7 @@ func scenarioAndModuleGeneratorForReq(req *pb.Operation_Request) ( } // Decode our flight plan, find our scenario - fp, decRes := decodeFlightPlan(ws.GetFlightplan()) + fp, decRes := decodeFlightPlan(ctx, ws.GetFlightplan()) if diagnostics.HasFailed( ws.GetTfExecCfg().GetFailOnWarnings(), decRes.GetDiagnostics(), @@ -181,8 +186,8 @@ func scenarioAndModuleGeneratorForReq(req *pb.Operation_Request) ( // moduleForReq returns a Terraform module for the request. It does not generate // it, it can only refer to where it would/should exist. -func moduleForReq(req *pb.Operation_Request) (*pb.Terraform_Module, []*pb.Diagnostic) { - gen, scenario, diags := scenarioAndModuleGeneratorForReq(req) +func moduleForReq(ctx context.Context, req *pb.Operation_Request) (*pb.Terraform_Module, []*pb.Diagnostic) { + gen, scenario, diags := scenarioAndModuleGeneratorForReq(ctx, req) if diagnostics.HasErrors(diags) { return nil, diags } diff --git a/internal/operation/runner_scenario_check.go b/internal/operation/runner_scenario_check.go index 56106fb4..0009233f 100644 --- a/internal/operation/runner_scenario_check.go +++ b/internal/operation/runner_scenario_check.go @@ -32,6 +32,7 @@ func CheckScenario(req *pb.Operation_Request) WorkFunc { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to send event", "error", err) } + return res } @@ -67,7 +68,7 @@ func CheckScenario(req *pb.Operation_Request) WorkFunc { } } -// scenarioCheck initializes, validates and plans the generated Terraform module +// scenarioCheck initializes, validates and plans the generated Terraform module. func (r *Runner) scenarioCheck( ctx context.Context, req *pb.Operation_Request, diff --git a/internal/operation/runner_scenario_destroy.go b/internal/operation/runner_scenario_destroy.go index b194eca7..d1e5f6a7 100644 --- a/internal/operation/runner_scenario_destroy.go +++ b/internal/operation/runner_scenario_destroy.go @@ -31,6 +31,7 @@ func DestroyScenario(req *pb.Operation_Request) WorkFunc { if err = events.PublishResponse(res); err != nil { log.Error("failed to send event", "error", err) } + return res } diff --git a/internal/operation/runner_scenario_exec.go b/internal/operation/runner_scenario_exec.go index 77434a28..86de948b 100644 --- a/internal/operation/runner_scenario_exec.go +++ b/internal/operation/runner_scenario_exec.go @@ -30,6 +30,7 @@ func ExecScenario(req *pb.Operation_Request) WorkFunc { if err = events.PublishResponse(res); err != nil { log.Error("failed to send event", "error", err) } + return res } @@ -47,7 +48,7 @@ func ExecScenario(req *pb.Operation_Request) WorkFunc { // it doesn't exist the sub-command failure will be reported. // Try and configure the runner with the module - mod, diags := moduleForReq(req) + mod, diags := moduleForReq(ctx, req) if len(diags) > 0 { // Rewrite failure diags to warnings since we might not need the module diff --git a/internal/operation/runner_scenario_generate.go b/internal/operation/runner_scenario_generate.go index a53fd43c..918a16bd 100644 --- a/internal/operation/runner_scenario_generate.go +++ b/internal/operation/runner_scenario_generate.go @@ -27,6 +27,7 @@ func GenerateScenario(req *pb.Operation_Request) WorkFunc { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to send event", "error", err) } + return res } diff --git a/internal/operation/runner_scenario_launch.go b/internal/operation/runner_scenario_launch.go index 8f4b4969..d7d013b6 100644 --- a/internal/operation/runner_scenario_launch.go +++ b/internal/operation/runner_scenario_launch.go @@ -27,6 +27,7 @@ func LaunchScenario(req *pb.Operation_Request) WorkFunc { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to send event", "error", err) } + return res } @@ -73,7 +74,7 @@ func LaunchScenario(req *pb.Operation_Request) WorkFunc { } } -// scenarioLaunch initializes, validates, plans, and applies the generated Terraform module +// scenarioLaunch initializes, validates, plans, and applies the generated Terraform module. func (r *Runner) scenarioLaunch( ctx context.Context, req *pb.Operation_Request, diff --git a/internal/operation/runner_scenario_output.go b/internal/operation/runner_scenario_output.go index f7042db8..60064d3d 100644 --- a/internal/operation/runner_scenario_output.go +++ b/internal/operation/runner_scenario_output.go @@ -30,6 +30,7 @@ func OutputScenario(req *pb.Operation_Request) WorkFunc { if err = events.PublishResponse(res); err != nil { log.Error("failed to send event", "error", err) } + return res } res.Value = resVal @@ -41,7 +42,7 @@ func OutputScenario(req *pb.Operation_Request) WorkFunc { // Configure the runner with the existing Terraform module. If it doesn't // exit there's nothing to output. - mod, diags := moduleForReq(req) + mod, diags := moduleForReq(ctx, req) resVal.Output.Diagnostics = append(resVal.Output.GetDiagnostics(), diags...) res.Status = diagnostics.Status(runner.TFConfig.FailOnWarnings, resVal.Output.Output.GetDiagnostics()...) @@ -50,6 +51,7 @@ func OutputScenario(req *pb.Operation_Request) WorkFunc { if err = events.PublishResponse(res); err != nil { log.Error("failed to send event", "error", err) } + return res } diff --git a/internal/operation/runner_scenario_run.go b/internal/operation/runner_scenario_run.go index 8042a438..c55dae07 100644 --- a/internal/operation/runner_scenario_run.go +++ b/internal/operation/runner_scenario_run.go @@ -27,6 +27,7 @@ func RunScenario(req *pb.Operation_Request) WorkFunc { if err = events.PublishResponse(res); err != nil { log.Error("failed to send event", "error", err) } + return res } @@ -75,7 +76,7 @@ func RunScenario(req *pb.Operation_Request) WorkFunc { } // scenarioRun initializes, validates, plans, applies and destroys the generatedTerraform -// Terraform module +// Terraform module. func (r *Runner) scenarioRun( ctx context.Context, req *pb.Operation_Request, diff --git a/internal/operation/runner_terraform_apply.go b/internal/operation/runner_terraform_apply.go index 90c0dac0..47ebb06c 100644 --- a/internal/operation/runner_terraform_apply.go +++ b/internal/operation/runner_terraform_apply.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformApply applys a Terraform module +// terraformApply applys a Terraform module. func (r *Runner) terraformApply( ctx context.Context, req *pb.Operation_Request, @@ -22,6 +22,7 @@ func (r *Runner) terraformApply( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -52,6 +53,7 @@ func (r *Runner) terraformApply( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -63,6 +65,7 @@ func (r *Runner) terraformApply( res.Stderr = applyOut.Stderr.String() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_destroy.go b/internal/operation/runner_terraform_destroy.go index 95d3ada7..e2bca040 100644 --- a/internal/operation/runner_terraform_destroy.go +++ b/internal/operation/runner_terraform_destroy.go @@ -8,7 +8,7 @@ import ( tfjson "github.com/hashicorp/terraform-json" ) -// terraformDestroy destroys resources created by the Terraform module +// terraformDestroy destroys resources created by the Terraform module. func (r *Runner) terraformDestroy( ctx context.Context, req *pb.Operation_Request, @@ -24,6 +24,7 @@ func (r *Runner) terraformDestroy( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -75,6 +76,7 @@ func (r *Runner) terraformDestroy( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -85,6 +87,7 @@ func (r *Runner) terraformDestroy( res.Stderr = destroyOut.Stderr.String() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_exec.go b/internal/operation/runner_terraform_exec.go index 7cb2c46e..f07255b0 100644 --- a/internal/operation/runner_terraform_exec.go +++ b/internal/operation/runner_terraform_exec.go @@ -25,6 +25,7 @@ func (r *Runner) terraformExec( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -67,6 +68,7 @@ func (r *Runner) terraformExec( res.Stderr = execOut.Stderr.String() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_init.go b/internal/operation/runner_terraform_init.go index 11a90249..f00f2593 100644 --- a/internal/operation/runner_terraform_init.go +++ b/internal/operation/runner_terraform_init.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformInit initializes a Terraform module +// terraformInit initializes a Terraform module. func (r *Runner) terraformInit( ctx context.Context, req *pb.Operation_Request, @@ -22,6 +22,7 @@ func (r *Runner) terraformInit( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) r.log.Error("failed to create reference from request", "error", err) + return res } @@ -52,6 +53,7 @@ func (r *Runner) terraformInit( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -63,6 +65,7 @@ func (r *Runner) terraformInit( res.Stderr = initOut.Stderr.String() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_output.go b/internal/operation/runner_terraform_output.go index b845cd1a..44206c59 100644 --- a/internal/operation/runner_terraform_output.go +++ b/internal/operation/runner_terraform_output.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformOutput renders the Terraform output +// terraformOutput renders the Terraform output. func (r *Runner) terraformOutput( ctx context.Context, req *pb.Operation_Request, @@ -23,6 +23,7 @@ func (r *Runner) terraformOutput( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -53,14 +54,16 @@ func (r *Runner) terraformOutput( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } // Configure our Terraform executor to use the module that should have // already been generated. - module, diags := moduleForReq(req) + module, diags := moduleForReq(ctx, req) if diagnostics.HasFailed(r.TFConfig.FailOnWarnings, diags) { notifyFail(diags) + return res } else { res.Diagnostics = append(res.GetDiagnostics(), diags...) @@ -76,6 +79,7 @@ func (r *Runner) terraformOutput( metas, err := tf.Output(ctx, r.TFConfig.OutputOptions()...) if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -83,6 +87,7 @@ func (r *Runner) terraformOutput( meta, found := metas[r.TFConfig.OutputName] if !found { notifyFail(diagnostics.FromErr(fmt.Errorf("no output with key %s", r.TFConfig.OutputName))) + return res } @@ -93,7 +98,6 @@ func (r *Runner) terraformOutput( Sensitive: meta.Sensitive, Stderr: outText.Stderr.String(), }) - } else { for name, meta := range metas { res.Meta = append(res.Meta, &pb.Terraform_Command_Output_Response_Meta{ diff --git a/internal/operation/runner_terraform_plan.go b/internal/operation/runner_terraform_plan.go index 8c464c27..d08c07d8 100644 --- a/internal/operation/runner_terraform_plan.go +++ b/internal/operation/runner_terraform_plan.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformPlan plans a Terraform module +// terraformPlan plans a Terraform module. func (r *Runner) terraformPlan( ctx context.Context, req *pb.Operation_Request, @@ -22,6 +22,7 @@ func (r *Runner) terraformPlan( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -52,6 +53,7 @@ func (r *Runner) terraformPlan( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -64,6 +66,7 @@ func (r *Runner) terraformPlan( res.Stderr = planOut.Stderr.String() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_show.go b/internal/operation/runner_terraform_show.go index e5fd43d7..56bcf1cb 100644 --- a/internal/operation/runner_terraform_show.go +++ b/internal/operation/runner_terraform_show.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformShow returns gets the Terraform State for a module +// terraformShow returns gets the Terraform State for a module. func (r *Runner) terraformShow( ctx context.Context, req *pb.Operation_Request, @@ -23,6 +23,7 @@ func (r *Runner) terraformShow( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -53,6 +54,7 @@ func (r *Runner) terraformShow( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -63,12 +65,14 @@ func (r *Runner) terraformShow( state, err := tf.Show(ctx, r.TFConfig.ShowOptions()...) if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } stateEnc, err := json.Marshal(state) if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/runner_terraform_validate.go b/internal/operation/runner_terraform_validate.go index bf365352..afeb6355 100644 --- a/internal/operation/runner_terraform_validate.go +++ b/internal/operation/runner_terraform_validate.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// terraformValidate validates a Terraform module +// terraformValidate validates a Terraform module. func (r *Runner) terraformValidate( ctx context.Context, req *pb.Operation_Request, @@ -23,6 +23,7 @@ func (r *Runner) terraformValidate( if err != nil { res.Diagnostics = append(res.Diagnostics, diagnostics.FromErr(err)...) log.Error("failed to create reference from request", "error", err) + return res } @@ -53,6 +54,7 @@ func (r *Runner) terraformValidate( tf, err := r.TFConfig.Terraform() if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } @@ -75,6 +77,7 @@ func (r *Runner) terraformValidate( } if err != nil { notifyFail(diagnostics.FromErr(err)) + return res } diff --git a/internal/operation/terraform/format/format.go b/internal/operation/terraform/format/format.go index f3f66a44..5cd72d34 100644 --- a/internal/operation/terraform/format/format.go +++ b/internal/operation/terraform/format/format.go @@ -1,5 +1,5 @@ // Package format provides functions for formatting Terraform and cty as -// as text. The implemenation comes from a modified version of the repl package +// text. The implemenation comes from a modified version of the repl package // in Terraform. package format @@ -15,7 +15,7 @@ import ( ) // TerraformOutput takes a terraform executor output metadata and returns it as -// as human friendly formatted string. +// human friendly formatted string. func TerraformOutput(out *pb.Terraform_Command_Output_Response_Meta, indent int) (string, error) { if out.GetSensitive() { return "(sensitive)", nil @@ -72,14 +72,17 @@ func Value(v cty.Value, indent int) string { if formatted, isMultiline := formatMultilineString(v, indent); isMultiline { return formatted } + return strconv.Quote(v.AsString()) case cty.Number: bf := v.AsBigFloat() + return bf.Text('f', -1) case cty.Bool: if v.True() { return "true" } + return "false" } case ty.IsObjectType(): @@ -123,6 +126,7 @@ OUTER: // If the delimiter matches a line, extend it and start again if strings.TrimSpace(line) == delimiter { delimiter = delimiter + "_" + continue OUTER } } @@ -168,6 +172,7 @@ func formatMappingValue(v cty.Value, indent int) string { buf.WriteString(strings.Repeat(" ", indent)) } buf.WriteByte('}') + return buf.String() } @@ -190,5 +195,6 @@ func formatSequenceValue(v cty.Value, indent int) string { buf.WriteString(strings.Repeat(" ", indent)) } buf.WriteByte(']') + return buf.String() } diff --git a/internal/operation/terraform/terraform.go b/internal/operation/terraform/terraform.go index e83158d6..37ee5218 100644 --- a/internal/operation/terraform/terraform.go +++ b/internal/operation/terraform/terraform.go @@ -16,20 +16,20 @@ import ( "github.com/hashicorp/terraform-exec/tfexec" ) -// Config is the Terraform CLI executor configuration +// Config is the Terraform CLI executor configuration. type Config struct { UI *terminal.UI // UI to use for input/output BinPath string // where terraform binary is ConfigPath string // where the terraformrc config is DirPath string // what directory to execute the command in - Env map[string]string // envrionment variables + Env map[string]string // environment variables ExecSubCmd string // raw command to run OutputName string // output name FailOnWarnings bool // fail on warning diagnostics Flags *pb.Terraform_Runner_Config_Flags } -// Proto returns the instance of config as proto terraform executor config +// Proto returns the instance of config as proto terraform executor config. func (c *Config) Proto() *pb.Terraform_Runner_Config { return &pb.Terraform_Runner_Config{ Flags: c.Flags, @@ -95,7 +95,7 @@ func (c *Config) tfEnv() map[string]string { } // NewExecSubCmd creates a new instance of a command to run a terraform -// sub-command +// sub-command. func (c *Config) NewExecSubCmd() *command.Command { execPath, err := c.tfPath() if err != nil { @@ -118,7 +118,7 @@ func (c *Config) NewExecSubCmd() *command.Command { return command.NewCommand(execPath, opts...) } -// Terraform returns a new instance of a configured *tfexec.Terraform +// Terraform returns a new instance of a configured *tfexec.Terraform. func (c *Config) Terraform() (*tfexec.Terraform, error) { var err error var tf *tfexec.Terraform @@ -146,17 +146,17 @@ func (c *Config) Terraform() (*tfexec.Terraform, error) { return tf, nil } -// ConfigOpt is a Terraform CLI executor configuration option +// ConfigOpt is a Terraform CLI executor configuration option. type ConfigOpt func(*Config) type ( - // Command is a Terraform sub-command + // Command is a Terraform sub-command. Command int - // Flag is a Terraform sub-command config flag + // Flag is a Terraform sub-command config flag. Flag int ) -// NewConfig takes options and returns a new instance of config +// NewConfig takes options and returns a new instance of config. func NewConfig(opts ...ConfigOpt) *Config { cfg := &Config{ Env: map[string]string{}, @@ -169,112 +169,112 @@ func NewConfig(opts ...ConfigOpt) *Config { return cfg } -// WithUI sets the UI +// WithUI sets the UI. func WithUI(ui *terminal.UI) ConfigOpt { return func(cfg *Config) { cfg.UI = ui } } -// WithBinPath sets the terraform binary path +// WithBinPath sets the terraform binary path. func WithBinPath(path string) ConfigOpt { return func(cfg *Config) { cfg.BinPath = path } } -// WithConfigPath sets the terraform.rc path +// WithConfigPath sets the terraform.rc path. func WithConfigPath(path string) ConfigOpt { return func(cfg *Config) { cfg.ConfigPath = path } } -// WithDirPath sets the terraform module directory path +// WithDirPath sets the terraform module directory path. func WithDirPath(path string) ConfigOpt { return func(cfg *Config) { cfg.DirPath = path } } -// WithEnv set environment variables +// WithEnv set environment variables. func WithEnv(env map[string]string) ConfigOpt { return func(cfg *Config) { cfg.Env = env } } -// WithExecSubCommand set the raw sub-command +// WithExecSubCommand set the raw sub-command. func WithExecSubCommand(cmd string) ConfigOpt { return func(cfg *Config) { cfg.ExecSubCmd = cmd } } -// WithLockTimeout sets the state lock timeout +// WithLockTimeout sets the state lock timeout. func WithLockTimeout(timeout time.Duration) ConfigOpt { return func(cfg *Config) { cfg.Flags.LockTimeout = durationpb.New(timeout) } } -// WithNoBackend disables the configured backend +// WithNoBackend disables the configured backend. func WithNoBackend() ConfigOpt { return func(cfg *Config) { cfg.Flags.NoBackend = true } } -// WithNoLock disables waiting for the state lock +// WithNoLock disables waiting for the state lock. func WithNoLock() ConfigOpt { return func(cfg *Config) { cfg.Flags.NoLock = true } } -// WithNoDownload disables module and provider downloading during init +// WithNoDownload disables module and provider downloading during init. func WithNoDownload() ConfigOpt { return func(cfg *Config) { cfg.Flags.NoDownload = true } } -// WithNoRefresh disables refreshing during plan and apply +// WithNoRefresh disables refreshing during plan and apply. func WithNoRefresh() ConfigOpt { return func(cfg *Config) { cfg.Flags.NoRefresh = true } } -// WithParallelism sets the parallelism +// WithParallelism sets the parallelism. func WithParallelism(p int) ConfigOpt { return func(cfg *Config) { cfg.Flags.Parallelism = uint32(p) } } -// WithRefreshOnly does refresh only mode +// WithRefreshOnly does refresh only mode. func WithRefreshOnly() ConfigOpt { return func(cfg *Config) { cfg.Flags.RefreshOnly = true } } -// WithUpgrade upgrades the terraform providers and modules during init +// WithUpgrade upgrades the terraform providers and modules during init. func WithUpgrade() ConfigOpt { return func(cfg *Config) { cfg.Flags.Upgrade = true } } -// WithNoReconfigure don't reconfigure the backend during +// WithNoReconfigure don't reconfigure the backend during. func WithNoReconfigure() ConfigOpt { return func(cfg *Config) { cfg.Flags.NoReconfigure = true } } -// WithProtoConfig sets configuration from a proto config +// WithProtoConfig sets configuration from a proto config. func WithProtoConfig(pcfg *pb.Terraform_Runner_Config) ConfigOpt { return func(cfg *Config) { cfg.FromProto(pcfg) @@ -289,7 +289,7 @@ func (c *Config) lockTimeoutString() string { return fmt.Sprintf("%dms", c.Flags.GetLockTimeout().AsDuration().Milliseconds()) } -// InitOptions are the init command options +// InitOptions are the init command options. func (c *Config) InitOptions() []tfexec.InitOption { return []tfexec.InitOption{ tfexec.Backend(!c.Flags.GetNoBackend()), @@ -299,7 +299,7 @@ func (c *Config) InitOptions() []tfexec.InitOption { } } -// PlanOptions are the plan command options +// PlanOptions are the plan command options. func (c *Config) PlanOptions() []tfexec.PlanOption { return []tfexec.PlanOption{ tfexec.Refresh(!c.Flags.GetNoRefresh()), @@ -310,7 +310,7 @@ func (c *Config) PlanOptions() []tfexec.PlanOption { } } -// ApplyOptions are the apply command options +// ApplyOptions are the apply command options. func (c *Config) ApplyOptions() []tfexec.ApplyOption { return []tfexec.ApplyOption{ tfexec.Backup(c.Flags.GetBackupStateFilePath()), @@ -322,7 +322,7 @@ func (c *Config) ApplyOptions() []tfexec.ApplyOption { } } -// DestroyOptions are the destroy command options +// DestroyOptions are the destroy command options. func (c *Config) DestroyOptions() []tfexec.DestroyOption { return []tfexec.DestroyOption{ tfexec.Backup(c.Flags.GetBackupStateFilePath()), @@ -334,12 +334,12 @@ func (c *Config) DestroyOptions() []tfexec.DestroyOption { } } -// OutputOptions are the output commands options +// OutputOptions are the output commands options. func (c *Config) OutputOptions() []tfexec.OutputOption { return []tfexec.OutputOption{} } -// ShowOptions are the show command options +// ShowOptions are the show command options. func (c *Config) ShowOptions() []tfexec.ShowOption { return []tfexec.ShowOption{} } diff --git a/internal/operation/worker.go b/internal/operation/worker.go index 0085823f..ac7ede2e 100644 --- a/internal/operation/worker.go +++ b/internal/operation/worker.go @@ -10,18 +10,16 @@ import ( "github.com/hashicorp/go-hclog" ) -// WorkFunc is a function that a worker can run +// WorkFunc is a function that a worker can run. type WorkFunc func( context.Context, chan *pb.Operation_Event, hclog.Logger, ) *pb.Operation_Response -// worker is an operation worker. It listens pulls work requests from the -// the queue and executes them. +// worker is an operation worker. It listens pulls work requests from the queue and executes them. type worker struct { id string - ctx context.Context requests chan *workReq events chan *pb.Operation_Event log hclog.Logger @@ -33,10 +31,9 @@ type workReq struct { f WorkFunc } -// newWorker takes a context, work request channel, and update channel and -// returns a new instance of a worker. +// newWorker takes a context, work request channel, and update channel and returns a new instance +// of a worker. func newWorker( - ctx context.Context, id string, requests chan *workReq, events chan *pb.Operation_Event, @@ -44,7 +41,6 @@ func newWorker( saveState func(*pb.Operation_Response) error, ) *worker { return &worker{ - ctx: ctx, id: id, requests: requests, events: events, @@ -81,25 +77,26 @@ func newWorkReqForOpReq(op *pb.Operation_Request) (*workReq, error) { return req, err } -// Run runs the worker. It continuously polls the input channel for new work -// requests. -func (w *worker) run() { +// Run runs the worker. It continuously polls the input channel for new work requests. +func (w *worker) run(ctx context.Context) { w.log.Debug("running") for { select { - case <-w.ctx.Done(): + case <-ctx.Done(): w.log.Debug("stopped") + return default: } select { - case <-w.ctx.Done(): + case <-ctx.Done(): w.log.Debug("stopped") + return case req := <-w.requests: - w.runRequest(req) + w.runRequest(ctx, req) } } } @@ -109,11 +106,10 @@ func (w *worker) sendEvent(event *pb.Operation_Event, done bool) { w.events <- event } -// runRequest is responsible for executing our WorkFunc. We execute the WorkFunc, -// filter and pass on the events to the events channel, persist the resulting -// response, and sending the done event. -func (w *worker) runRequest(req *workReq) { - workCtx, workCancel := context.WithCancel(w.ctx) +// runRequest is responsible for executing our WorkFunc. We execute the WorkFunc, filter and pass +// on the events to the events channel, persist the resulting response, and sending the done event. +func (w *worker) runRequest(ctx context.Context, req *workReq) { + workCtx, workCancel := context.WithCancel(ctx) eventC := make(chan *pb.Operation_Event) resC := make(chan *pb.Operation_Response, 1) eWg := sync.WaitGroup{} @@ -169,7 +165,7 @@ func (w *worker) runRequest(req *workReq) { select { case <-workCtx.Done(): return - case resC <- req.f(w.ctx, eventC, log.Named(req.req.GetId())): + case resC <- req.f(ctx, eventC, log.Named(req.req.GetId())): return } }() diff --git a/internal/server/flightplan.go b/internal/server/flightplan.go index b68dbd35..9c754a92 100644 --- a/internal/server/flightplan.go +++ b/internal/server/flightplan.go @@ -1,12 +1,15 @@ package server import ( + "context" + "github.com/hashicorp/enos/internal/diagnostics" "github.com/hashicorp/enos/internal/flightplan" "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) func decodeFlightPlan( + ctx context.Context, pfp *pb.FlightPlan, mode flightplan.DecodeMode, f *pb.Scenario_Filter, @@ -23,21 +26,17 @@ func decodeFlightPlan( flightplan.WithDecoderDecodeMode(mode), } - if f != nil { - filter, err := flightplan.NewScenarioFilter( - flightplan.WithScenarioFilterDecode(f), - ) - if err != nil { - res.Diagnostics = append(res.GetDiagnostics(), diagnostics.FromErr(err)...) - return nil, res - } - - opts = append(opts, flightplan.WithDecoderScenarioFilter(filter)) + sf, err := flightplan.NewScenarioFilter(flightplan.WithScenarioFilterDecode(f)) + if err != nil { + res.Diagnostics = append(res.GetDiagnostics(), diagnostics.FromErr(err)...) + } else { + opts = append(opts, flightplan.WithDecoderScenarioFilter(sf)) } dec, err := flightplan.NewDecoder(opts...) if err != nil { res.Diagnostics = diagnostics.FromErr(err) + return nil, res } @@ -50,7 +49,7 @@ func decodeFlightPlan( return nil, res } - fp, hclDiags := dec.Decode() + fp, hclDiags := dec.Decode(ctx) if len(hclDiags) > 0 { res.Diagnostics = append(res.Diagnostics, diagnostics.FromHCL(dec.ParserFiles(), hclDiags)...) } diff --git a/internal/server/log_interceptor.go b/internal/server/log_interceptor.go index 87dab994..02d55aa1 100644 --- a/internal/server/log_interceptor.go +++ b/internal/server/log_interceptor.go @@ -59,7 +59,7 @@ func logUnaryInterceptor(logger hclog.Logger, verbose bool) grpc.UnaryServerInte // // Additionally, logUnaryInterceptor logs request and response metadata. If verbose // is set to true, the request and response attributes are logged too. -func logStreamInterceptor(logger hclog.Logger, verbose bool) grpc.StreamServerInterceptor { +func logStreamInterceptor(logger hclog.Logger) grpc.StreamServerInterceptor { return func( srv interface{}, ss grpc.ServerStream, diff --git a/internal/server/service_v1.go b/internal/server/service_v1.go index cd12c07b..3a690b58 100644 --- a/internal/server/service_v1.go +++ b/internal/server/service_v1.go @@ -7,7 +7,6 @@ import ( "net/url" "os" "os/signal" - "strings" "sync" "syscall" "time" @@ -27,13 +26,13 @@ import ( var _ pb.EnosServiceServer = (*ServiceV1)(nil) -// ServiceV1 is the enos.v1.ServerService +// ServiceV1 is the enos.v1.ServerService. type ServiceV1 struct { log hclog.Logger - grpcListenAddr net.Addr - grpcListener net.Listener - grpcServer *grpc.Server + configuredURL *url.URL + grpcListener net.Listener + grpcServer *grpc.Server operator operation.Operator } @@ -43,49 +42,40 @@ type ServiceConfig struct { ListenAddr net.Addr } -// Opt is a functional option +// Opt is a functional option. type Opt func(*ServiceV1) error -// WithGRPCListenURL configures the gRPC listener address from a given URL +// WithGRPCListenURL configures the gRPC listener address from a given URL. func WithGRPCListenURL(url *url.URL) Opt { return func(s *ServiceV1) error { - var err error - s.grpcListenAddr, err = ListenAddr(url) - return err + if url == nil { + return fmt.Errorf("cannot configure listener URL that is nil") + } + s.configuredURL = url + + return nil } } -// WithLogger configures the logger +// WithLogger configures the logger. func WithLogger(log hclog.Logger) Opt { return func(s *ServiceV1) error { s.log = log + return nil } } -// WithOperator configures the servers operation operator +// WithOperator configures the servers operation operator. func WithOperator(op operation.Operator) Opt { return func(s *ServiceV1) error { s.operator = op - return nil - } -} -// ListenAddr returns a server listen address from a URL -func ListenAddr(url *url.URL) (net.Addr, error) { - switch url.Scheme { - case "unix", "unixpacket": - return net.ResolveUnixAddr(url.Scheme, url.Host) - default: - addr := url.Host - if idx := strings.IndexByte(addr, ':'); idx < 0 { - addr += ":3205" - } - return net.ResolveTCPAddr("tcp", addr) + return nil } } -// New takes options and returns an instance of ServiceV1 +// New takes options and returns an instance of ServiceV1. func New(opts ...Opt) (*ServiceV1, error) { svc := &ServiceV1{ log: hclog.NewNullLogger(), @@ -106,7 +96,7 @@ func New(opts ...Opt) (*ServiceV1, error) { logUnaryInterceptor(grpcLogger, false), ), grpc.ChainStreamInterceptor( - logStreamInterceptor(grpcLogger, false), + logStreamInterceptor(grpcLogger), ), grpc.KeepaliveEnforcementPolicy( keepalive.EnforcementPolicy{ @@ -123,13 +113,12 @@ func New(opts ...Opt) (*ServiceV1, error) { // Start takes a context and starts the server. It returns any immediate errors and a service config. // Fatal errors encountered will automatically stop the server. func (s *ServiceV1) Start(ctx context.Context) (*ServiceConfig, error) { - if s.grpcListenAddr == nil { + if s.configuredURL == nil { return nil, fmt.Errorf("unable to start gRPC service: you must provider a listen address") } s.log.Info("starting gRPC server", - "network", s.grpcListenAddr.Network(), - "addr", s.grpcListenAddr.String(), + "listen_grpc", s.configuredURL.String(), ) // Only interrupt and kill are guaranteed on all OSes. We'll pipe through unix signals we care @@ -152,7 +141,7 @@ func (s *ServiceV1) Start(ctx context.Context) (*ServiceConfig, error) { errC := make(chan error, 1) wg.Add(1) go func() { - errC <- s.serve(ctx) + errC <- s.serve() wg.Done() }() @@ -163,12 +152,14 @@ func (s *ServiceV1) Start(ctx context.Context) (*ServiceConfig, error) { if err != nil { s.log.Error("server encountered an error", "error", err) } + return case <-ctx.Done(): err := multierror.Append(ctx.Err(), s.Stop()).ErrorOrNil() if err != nil { s.log.Error(err.Error()) } + return } } @@ -180,7 +171,7 @@ func (s *ServiceV1) Start(ctx context.Context) (*ServiceConfig, error) { }, nil } -// startListener starts the gRPC server listener +// startListener starts the gRPC server listener. func (s *ServiceV1) startListener(ctx context.Context) error { // Reflection makes it easy to see what methods are on a server via grpcurl. reflection.Register(s.grpcServer) @@ -189,30 +180,56 @@ func (s *ServiceV1) startListener(ctx context.Context) error { pb.RegisterEnosServiceServer(s.grpcServer, s) s.log.Debug("starting gRPC server listener", - "network", s.grpcListenAddr.Network(), - "addr", s.grpcListenAddr.String(), + "listen_grpc", s.configuredURL.String(), ) + + // Start the listener + var addr net.Addr var err error - s.grpcListener, err = net.Listen(s.grpcListenAddr.Network(), s.grpcListenAddr.String()) + + switch s.configuredURL.Scheme { + case "unix", "unixpacket": + addr, err = net.ResolveUnixAddr(s.configuredURL.Scheme, s.configuredURL.Host) + default: + if p := s.configuredURL.Port(); p == "" { + s.configuredURL.Host = fmt.Sprintf("%s:0", s.configuredURL.Host) + } + + addr, err = net.ResolveTCPAddr("tcp", s.configuredURL.Host) + } + + if err != nil { + s.log.Error("failed to resolve gRPC server listener", + "listen_grpc", s.configuredURL.String(), + "error", err, + ) + + return fmt.Errorf("failed to resolve gRPC server listener: %w", err) + } + + lc := &net.ListenConfig{} + s.grpcListener, err = lc.Listen(ctx, addr.Network(), addr.String()) if err != nil { s.log.Error("failed to start gRPC server listener", - "network", s.grpcListenAddr.Network(), - "addr", s.grpcListenAddr.String(), + "listen_grpc", s.configuredURL.String(), "error", err, ) - return err + + return fmt.Errorf("starting gRPC server listener %w", err) } s.log.Debug("gRPC server listener is listening", - "network", s.grpcListenAddr.Network(), - "configured_addr", s.grpcListenAddr.String(), + "host", s.configuredURL.Host, + "port", s.configuredURL.Host, + "requested_addr", addr, + "network", s.grpcListener.Addr().Network(), "resolved_addr", s.grpcListener.Addr().String(), ) return nil } -// startOperator starts the service operator +// startOperator starts the service operator. func (s *ServiceV1) startOperator(ctx context.Context) error { s.log.Debug("starting service operator") @@ -221,24 +238,27 @@ func (s *ServiceV1) startOperator(ctx context.Context) error { s.log.Error("failed to start service operator", "error", err, ) + return err } s.log.Debug("service operator running") + return nil } // serve services requests. It will block until an error is encountered. -func (s *ServiceV1) serve(ctx context.Context) error { +func (s *ServiceV1) serve() error { s.log.Debug("serving gRPC requests", - "network", s.grpcListenAddr.Network(), + "listen_grpc", s.configuredURL.String(), + "network", s.grpcListener.Addr().Network(), "addr", s.grpcListener.Addr(), ) return s.grpcServer.Serve(s.grpcListener) } -// Stop stops the service +// Stop stops the service. func (s *ServiceV1) Stop() error { var wg sync.WaitGroup var err error @@ -282,6 +302,7 @@ func (s *ServiceV1) Stop() error { // of operation references, and any diagnostics if dispatching isn't possible. // The base operation must include a valid operation type. func (s *ServiceV1) dispatch( + ctx context.Context, f *pb.Scenario_Filter, baseReq *pb.Operation_Request, ) ( @@ -300,6 +321,7 @@ func (s *ServiceV1) dispatch( } fp, decRes := decodeFlightPlan( + ctx, ws.GetFlightplan(), flightplan.DecodeModeFull, f, @@ -336,6 +358,7 @@ func (s *ServiceV1) dispatch( err := proto.Copy(baseReq, req) if err != nil { diags = append(diags, diagnostics.FromErr(err)...) + continue } diff --git a/internal/server/service_v1_check_scenarios.go b/internal/server/service_v1_check_scenarios.go index e3fa09b9..7d55d8c4 100644 --- a/internal/server/service_v1_check_scenarios.go +++ b/internal/server/service_v1_check_scenarios.go @@ -18,11 +18,13 @@ func (s *ServiceV1) CheckScenarios( ) { res := &pb.CheckScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Check_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_destroy_scenarios.go b/internal/server/service_v1_destroy_scenarios.go index dcdcf3b0..0115a54d 100644 --- a/internal/server/service_v1_destroy_scenarios.go +++ b/internal/server/service_v1_destroy_scenarios.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// DestroyScenarios destroys scenarios +// DestroyScenarios destroys scenarios. func (s *ServiceV1) DestroyScenarios( ctx context.Context, req *pb.DestroyScenariosRequest, @@ -16,11 +16,13 @@ func (s *ServiceV1) DestroyScenarios( ) { res := &pb.DestroyScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Destroy_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_exec_scenarios.go b/internal/server/service_v1_exec_scenarios.go index 14b1e4c8..5233fac3 100644 --- a/internal/server/service_v1_exec_scenarios.go +++ b/internal/server/service_v1_exec_scenarios.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ExecScenarios executes a Terraform sub-command in the context of scenarios +// ExecScenarios executes a Terraform sub-command in the context of scenarios. func (s *ServiceV1) ExecScenarios( ctx context.Context, req *pb.ExecScenariosRequest, @@ -16,11 +16,13 @@ func (s *ServiceV1) ExecScenarios( ) { res := &pb.ExecScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Exec_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_format.go b/internal/server/service_v1_format.go index d76de86b..ad8197b0 100644 --- a/internal/server/service_v1_format.go +++ b/internal/server/service_v1_format.go @@ -17,7 +17,7 @@ import ( "github.com/hashicorp/hcl/v2/hclwrite" ) -// Format does formatting on Enos configuration +// Format does formatting on Enos configuration. func (s *ServiceV1) Format( ctx context.Context, req *pb.FormatRequest, @@ -39,6 +39,7 @@ func (s *ServiceV1) Format( if diags.HasErrors() { r.Diagnostics = diagnostics.FromHCL(nil, diags) res.Responses = append(res.Responses, r) + continue } @@ -46,6 +47,7 @@ func (s *ServiceV1) Format( if bytes.Equal(file.GetBody(), formatted) { // If nothing has changed we can move on res.Responses = append(res.Responses, r) + continue } @@ -72,6 +74,7 @@ func (s *ServiceV1) Format( if err != nil { res.Diagnostics = diagnostics.FromErr(err) res.Responses = append(res.Responses, r) + continue } defer f.Close() @@ -80,6 +83,7 @@ func (s *ServiceV1) Format( if err != nil { res.Diagnostics = diagnostics.FromErr(err) res.Responses = append(res.Responses, r) + continue } } diff --git a/internal/server/service_v1_generate_scenarios.go b/internal/server/service_v1_generate_scenarios.go index 7a9cf886..929584a5 100644 --- a/internal/server/service_v1_generate_scenarios.go +++ b/internal/server/service_v1_generate_scenarios.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// GenerateScenarios generates scenario Terraform modules and configuration +// GenerateScenarios generates scenario Terraform modules and configuration. func (s *ServiceV1) GenerateScenarios( ctx context.Context, req *pb.GenerateScenariosRequest, @@ -16,11 +16,13 @@ func (s *ServiceV1) GenerateScenarios( ) { res := &pb.GenerateScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Generate_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_launch_scenarios.go b/internal/server/service_v1_launch_scenarios.go index 3c20bc73..0dcef09b 100644 --- a/internal/server/service_v1_launch_scenarios.go +++ b/internal/server/service_v1_launch_scenarios.go @@ -18,11 +18,13 @@ func (s *ServiceV1) LaunchScenarios( ) { res := &pb.LaunchScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Launch_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_list_scenarios.go b/internal/server/service_v1_list_scenarios.go index e019b769..e93d6a01 100644 --- a/internal/server/service_v1_list_scenarios.go +++ b/internal/server/service_v1_list_scenarios.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ListScenarios returns the version information +// ListScenarios returns the version information. func (s *ServiceV1) ListScenarios( ctx context.Context, req *pb.ListScenariosRequest, @@ -19,6 +19,7 @@ func (s *ServiceV1) ListScenarios( res := &pb.ListScenariosResponse{} fp, decRes := decodeFlightPlan( + ctx, req.GetWorkspace().GetFlightplan(), flightplan.DecodeModeRef, req.GetFilter(), diff --git a/internal/server/service_v1_operation.go b/internal/server/service_v1_operation.go index b5d64d6e..c2985cb8 100644 --- a/internal/server/service_v1_operation.go +++ b/internal/server/service_v1_operation.go @@ -14,6 +14,7 @@ func (s *ServiceV1) Operation( req *pb.OperationRequest, ) (*pb.OperationResponse, error) { res, err := s.operator.Response(req.GetOp()) + return &pb.OperationResponse{ Diagnostics: diagnostics.FromErr(err), Response: res, diff --git a/internal/server/service_v1_output_scenarios.go b/internal/server/service_v1_output_scenarios.go index 29eafdab..97344d6d 100644 --- a/internal/server/service_v1_output_scenarios.go +++ b/internal/server/service_v1_output_scenarios.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// OutputScenarios returns scenario outputs +// OutputScenarios returns scenario outputs. func (s *ServiceV1) OutputScenarios( ctx context.Context, req *pb.OutputScenariosRequest, @@ -16,11 +16,13 @@ func (s *ServiceV1) OutputScenarios( ) { res := &pb.OutputScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Output_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_run_scenarios.go b/internal/server/service_v1_run_scenarios.go index 25a197e3..b66324c8 100644 --- a/internal/server/service_v1_run_scenarios.go +++ b/internal/server/service_v1_run_scenarios.go @@ -18,11 +18,13 @@ func (s *ServiceV1) RunScenarios( ) { res := &pb.RunScenariosResponse{} res.Diagnostics, res.Decode, res.Operations = s.dispatch( + ctx, req.GetFilter(), &pb.Operation_Request{ Workspace: req.GetWorkspace(), Value: &pb.Operation_Request_Run_{}, }, ) + return res, nil } diff --git a/internal/server/service_v1_validate_scenarios_config.go b/internal/server/service_v1_validate_scenarios_config.go index fc177052..750afe97 100644 --- a/internal/server/service_v1_validate_scenarios_config.go +++ b/internal/server/service_v1_validate_scenarios_config.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ValidateScenariosConfiguration validates a flight plan config +// ValidateScenariosConfiguration validates a flight plan config. func (s *ServiceV1) ValidateScenariosConfiguration( ctx context.Context, req *pb.ValidateScenariosConfigurationRequest, @@ -18,6 +18,7 @@ func (s *ServiceV1) ValidateScenariosConfiguration( res := &pb.ValidateScenariosConfigurationResponse{} _, decRes := decodeFlightPlan( + ctx, req.GetWorkspace().GetFlightplan(), flightplan.DecodeModeFull, req.GetFilter(), diff --git a/internal/server/service_v1_version.go b/internal/server/service_v1_version.go index d4a4800c..67067f17 100644 --- a/internal/server/service_v1_version.go +++ b/internal/server/service_v1_version.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/version" ) -// GetVersion returns the version information +// GetVersion returns the version information. func (s *ServiceV1) GetVersion( ctx context.Context, req *pb.GetVersionRequest, diff --git a/internal/state/state.go b/internal/state/state.go index 71fb697f..832f60b6 100644 --- a/internal/state/state.go +++ b/internal/state/state.go @@ -2,7 +2,7 @@ package state import "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" -// State is our server state +// State is our server state. type State interface { // GetOperationResponse returns the most recent committed operation response. GetOperationResponse(*pb.Ref_Operation) (*pb.Operation_Response, error) diff --git a/internal/state/state_inmem.go b/internal/state/state_inmem.go index d86f0418..5bab5de9 100644 --- a/internal/state/state_inmem.go +++ b/internal/state/state_inmem.go @@ -11,7 +11,7 @@ import ( var _ State = (*InMemoryState)(nil) -// InMemoryState is an implementation of our state that lives only in memory +// InMemoryState is an implementation of our state that lives only in memory. type InMemoryState struct { mu sync.RWMutex // scenario ID -> operation id -> operation response @@ -20,7 +20,7 @@ type InMemoryState struct { events map[string]map[string][]*pb.Operation_Event } -// NewInMemoryState returns a new InMemoryState +// NewInMemoryState returns a new InMemoryState. func NewInMemoryState() *InMemoryState { return &InMemoryState{ mu: sync.RWMutex{}, @@ -44,7 +44,7 @@ func (i *InMemoryState) GetOperationResponse( } // GetOperationEvents takes a reference to an operation and returns the -// the entire event history. +// entire event history. func (i *InMemoryState) GetOperationEvents( ref *pb.Ref_Operation, ) ( @@ -168,12 +168,12 @@ func (i *InMemoryState) getOperationResponse( _, ok := i.responses[sid] if !ok { - return nil, nil + return nil, fmt.Errorf("no operations matching scenario ID") } op, ok := i.responses[sid][uid] if !ok { - return nil, nil + return nil, fmt.Errorf("no operations matching scenario and operation IDs") } return op, nil diff --git a/internal/ui/basic/basic.go b/internal/ui/basic/basic.go index 864a2672..cd4b7731 100644 --- a/internal/ui/basic/basic.go +++ b/internal/ui/basic/basic.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// View is our basic terminal CLI view +// View is our basic terminal CLI view. type View struct { settings *pb.UI_Settings ui *terminal.UI @@ -16,10 +16,10 @@ type View struct { stderr io.ReadWriteCloser } -// Opt is a functional option +// Opt is a functional option. type Opt func(*View) -// New takes options and returns a new basic.View +// New takes options and returns a new basic.View. func New(opts ...Opt) (*View, error) { v := &View{} @@ -62,7 +62,6 @@ func New(opts ...Opt) (*View, error) { uiOpts = append(uiOpts, terminal.WithStderr(f)) } - } v.ui = terminal.NewUI(uiOpts...) @@ -70,19 +69,19 @@ func New(opts ...Opt) (*View, error) { return v, nil } -// WithUISettings sets the ui settings +// WithUISettings sets the ui settings. func WithUISettings(ui *pb.UI_Settings) Opt { return func(view *View) { view.settings = ui } } -// Settings returns the views UI settings +// Settings returns the views UI settings. func (v *View) Settings() *pb.UI_Settings { return v.settings } -// Close closes any open file handles +// Close closes any open file handles. func (v *View) Close() error { if v.stderr != nil { err := v.stderr.Close() @@ -143,6 +142,11 @@ func (v *View) opStatusString(status pb.Operation_Status) string { if !tty { res = "queued" } + case pb.Operation_STATUS_UNSPECIFIED, pb.Operation_STATUS_UNKNOWN: + res = "⁉️" + if !tty { + res = "unknown" + } default: res = "⁉️" if !tty { @@ -163,16 +167,19 @@ func (v *View) writeMsg( if status == pb.Operation_STATUS_FAILED { v.ui.Error(msg) + return } if status == pb.Operation_STATUS_COMPLETED_WARNING && v.settings.Level >= pb.UI_Settings_LEVEL_WARN { v.ui.Warn(msg) + return } if v.settings.Level >= pb.UI_Settings_LEVEL_INFO { v.ui.Info(msg) + return } diff --git a/internal/ui/basic/diags.go b/internal/ui/basic/diags.go index 62de60ba..09faf2ed 100644 --- a/internal/ui/basic/diags.go +++ b/internal/ui/basic/diags.go @@ -8,13 +8,14 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowDiagnostics writes a diagnostic to stderr +// ShowDiagnostics writes a diagnostic to stderr. func (v *View) ShowDiagnostics(diags []*pb.Diagnostic) error { v.WriteDiagnostics(diags) + return nil } -// WriteDiagnostics writes diagnostics in a basic human friendly way +// WriteDiagnostics writes diagnostics in a basic human friendly way. func (v *View) WriteDiagnostics(diags []*pb.Diagnostic) { if len(diags) < 1 { return @@ -22,7 +23,7 @@ func (v *View) WriteDiagnostics(diags []*pb.Diagnostic) { v.ui.Error(v.diagsToString(diags)) } -// diagsToString returns the diagsnostics as a string +// diagsToString returns the diagsnostics as a string. func (v *View) diagsToString(diags []*pb.Diagnostic) string { if len(diags) < 1 { return "" diff --git a/internal/ui/basic/op_events.go b/internal/ui/basic/op_events.go index ea8ef793..79f3bf72 100644 --- a/internal/ui/basic/op_events.go +++ b/internal/ui/basic/op_events.go @@ -218,6 +218,7 @@ func (v *View) writeEventOutput(e *pb.Operation_Event, w *strings.Builder) { s, err := format.TerraformOutput(meta, 2) if err != nil { diags = append(diags, diagnostics.FromErr(err)...) + continue } diff --git a/internal/ui/basic/op_responses.go b/internal/ui/basic/op_responses.go index 192f9e37..ac84b876 100644 --- a/internal/ui/basic/op_responses.go +++ b/internal/ui/basic/op_responses.go @@ -21,6 +21,7 @@ func (v *View) writeDecodeResponse(out *pb.DecodeResponse) { } v.ui.Error(msg) v.WriteDiagnostics(out.GetDiagnostics()) + return } @@ -58,6 +59,7 @@ func (v *View) writeGenerateResponse(out *pb.Operation_Response_Generate) { v.ui.Error(fmt.Sprintf(" Module rc path: %s", out.GetTerraformModule().GetRcPath())) v.ui.Error(msg) v.WriteDiagnostics(out.GetDiagnostics()) + return } diff --git a/internal/ui/basic/show_decode.go b/internal/ui/basic/show_decode.go index 96ad7cbe..ff696baf 100644 --- a/internal/ui/basic/show_decode.go +++ b/internal/ui/basic/show_decode.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowDecode shows the human friendly view of decoding +// ShowDecode shows the human friendly view of decoding. func (v *View) ShowDecode(res *pb.DecodeResponse, incremental bool) error { if res == nil { return nil diff --git a/internal/ui/basic/show_error.go b/internal/ui/basic/show_error.go index 46070c87..13b64aa9 100644 --- a/internal/ui/basic/show_error.go +++ b/internal/ui/basic/show_error.go @@ -1,10 +1,11 @@ package basic -// ShowError writes the error message to stdout +// ShowError writes the error message to stdout. func (v *View) ShowError(err error) error { if err == nil { return nil } v.ui.Error(err.Error()) + return nil } diff --git a/internal/ui/basic/show_format.go b/internal/ui/basic/show_format.go index bf15d168..712b19bc 100644 --- a/internal/ui/basic/show_format.go +++ b/internal/ui/basic/show_format.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowFormat displays the response of a format request +// ShowFormat displays the response of a format request. func (v *View) ShowFormat(cfg *pb.FormatRequest_Config, res *pb.FormatResponse) error { for _, r := range res.GetResponses() { if cfg.GetList() && r.GetChanged() && r.GetPath() != "STDIN" { diff --git a/internal/ui/basic/show_op_event.go b/internal/ui/basic/show_op_event.go index 2cf1d7b5..03e70643 100644 --- a/internal/ui/basic/show_op_event.go +++ b/internal/ui/basic/show_op_event.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowOperationEvent shows the human friendly view on an operation event +// ShowOperationEvent shows the human friendly view on an operation event. func (v *View) ShowOperationEvent(event *pb.Operation_Event) { if event == nil { return diff --git a/internal/ui/basic/show_op_response.go b/internal/ui/basic/show_op_response.go index 99582422..14cabb1e 100644 --- a/internal/ui/basic/show_op_response.go +++ b/internal/ui/basic/show_op_response.go @@ -3,6 +3,7 @@ package basic import ( "fmt" "math/rand" + "time" "github.com/hashicorp/enos/internal/diagnostics" "github.com/hashicorp/enos/internal/flightplan" @@ -12,7 +13,7 @@ import ( var failedIcons = []string{"🙈", "🙉", "🙊"} -// ShowOperationResponses shows an operation responses +// ShowOperationResponses shows an operation responses. func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { // Determine if anything failed so we can create the correct display header diags := diagnostics.Concat(res.GetDecode().GetDiagnostics(), res.GetDiagnostics()) @@ -21,6 +22,7 @@ func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { for _, r := range res.GetResponses() { if diagnostics.OpResFailed(v.Settings().GetFailOnWarnings(), r) { failed = true + break } } @@ -29,7 +31,9 @@ func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { header := "\nEnos operations" if failed { if v.settings.IsTty { - header = fmt.Sprintf("%s failed! %s\n", header, failedIcons[rand.Intn(len(failedIcons))]) + //nolint:gosec // G404 it's okay to use weak random numbers for random failed icons + r := rand.New(rand.NewSource(time.Now().UnixNano())) + header = fmt.Sprintf("%s failed! %s\n", header, failedIcons[r.Intn(len(failedIcons))]) } else { header += " failed!\n" } @@ -80,7 +84,7 @@ func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { return status.OperationResponses(v.Settings().GetFailOnWarnings(), res) } -// ShowOperationResponse shows an operation response +// ShowOperationResponse shows an operation response. func (v *View) ShowOperationResponse(res *pb.Operation_Response) error { if res == nil { return nil @@ -95,7 +99,7 @@ func (v *View) ShowOperationResponse(res *pb.Operation_Response) error { }) } -// showOperationResponse shows an operation response +// showOperationResponse shows an operation response. func (v *View) showOperationResponse(res *pb.Operation_Response, fullOnComplete bool) error { if res == nil { return nil @@ -168,7 +172,7 @@ func (v *View) showOperationResponse(res *pb.Operation_Response, fullOnComplete // shouldShowFullResponse determines whether or not we should display the entire // operation response. We show the full response if something went wrong, if // a full response has been requested, or if there is operation information for -// for sub-operations that have output like exec or output. +// sub-operations that have output like exec or output. func shouldShowFullResponse(res *pb.Operation_Response, fullOnComplete bool) bool { if fullOnComplete { return true diff --git a/internal/ui/basic/show_scenario_list.go b/internal/ui/basic/show_scenario_list.go index d55f4d33..5ff33499 100644 --- a/internal/ui/basic/show_scenario_list.go +++ b/internal/ui/basic/show_scenario_list.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowScenarioList shows the a list of scenarios +// ShowScenarioList shows the a list of scenarios. func (v *View) ShowScenarioList(res *pb.ListScenariosResponse) error { header := []string{"name"} rows := [][]string{{""}} // add a padding row @@ -18,6 +18,7 @@ func (v *View) ShowScenarioList(res *pb.ListScenariosResponse) error { for vi := range scenario.Variants.Elements() { if vi == 0 { header = append(header, "variants") + continue } // create a blank "header" for every variant diff --git a/internal/ui/basic/show_scenario_validate_config.go b/internal/ui/basic/show_scenario_validate_config.go index d93764c5..5917128d 100644 --- a/internal/ui/basic/show_scenario_validate_config.go +++ b/internal/ui/basic/show_scenario_validate_config.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowScenariosValidateConfig shows the flight plan validation response +// ShowScenariosValidateConfig shows the flight plan validation response. func (v *View) ShowScenariosValidateConfig(res *pb.ValidateScenariosConfigurationResponse) error { diags := diagnostics.Concat(res.GetDecode().GetDiagnostics(), res.GetDiagnostics()) v.WriteDiagnostics(diags) diff --git a/internal/ui/basic/show_version.go b/internal/ui/basic/show_version.go index 8d018d45..56463f1b 100644 --- a/internal/ui/basic/show_version.go +++ b/internal/ui/basic/show_version.go @@ -7,7 +7,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ShowVersion shows the version information +// ShowVersion shows the version information. func (v *View) ShowVersion(all bool, res *pb.GetVersionResponse) error { if !all { v.ui.Output(res.Version) diff --git a/internal/ui/basic/tf_responses.go b/internal/ui/basic/tf_responses.go index 948371c3..7fab56d4 100644 --- a/internal/ui/basic/tf_responses.go +++ b/internal/ui/basic/tf_responses.go @@ -36,6 +36,7 @@ func (v *View) writeValidateResponse(validate *pb.Terraform_Command_Validate_Res v.ui.Error(fmt.Sprintf(" Validation warnings: %d", validate.GetWarningCount())) v.ui.Debug(fmt.Sprintf(" Validation format: %s", validate.GetFormatVersion())) v.WriteDiagnostics(validate.GetDiagnostics()) + return } @@ -77,6 +78,7 @@ func (v *View) writeExecResponse(exec *pb.Terraform_Command_Exec_Response) { v.ui.Error(stderr) } v.WriteDiagnostics(exec.GetDiagnostics()) + return } @@ -138,6 +140,7 @@ func (v *View) writeShowResponse(show *pb.Terraform_Command_Show_Response) { } v.WriteDiagnostics(show.GetDiagnostics()) + return } @@ -177,6 +180,7 @@ func (v *View) writePlainTextResponse(cmd string, stderr string, res status.ResW v.ui.Error(stderr) } v.WriteDiagnostics(res.GetDiagnostics()) + return } diff --git a/internal/ui/machine/machine.go b/internal/ui/machine/machine.go index 495fa38c..6cdd6cbd 100644 --- a/internal/ui/machine/machine.go +++ b/internal/ui/machine/machine.go @@ -15,7 +15,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// View is our basic terminal CLI view +// View is our basic terminal CLI view. type View struct { settings *pb.UI_Settings stderr io.ReadWriteCloser @@ -31,7 +31,7 @@ type diagsJSON struct { Diags []json.RawMessage `json:"diagnostics"` } -// Opt is a functional option +// Opt is a functional option. type Opt func(*View) // NewErrUnsupportedEncodingFormat returns a new unsupported encoding format @@ -42,10 +42,11 @@ func NewErrUnsupportedEncodingFormat(format pb.UI_Settings_Format) error { if !ok { msg = fmt.Sprintf("%s: %s", msg, friendlyName) } + return fmt.Errorf(msg) } -// New takes options and returns a new basic.View +// New takes options and returns a new basic.View. func New(opts ...Opt) (*View, error) { v := &View{} @@ -87,19 +88,19 @@ func New(opts ...Opt) (*View, error) { return v, nil } -// WithUISettings configures the view with the UI settings +// WithUISettings configures the view with the UI settings. func WithUISettings(settings *pb.UI_Settings) Opt { return func(view *View) { view.settings = settings } } -// Settings returns the views UI settings +// Settings returns the views UI settings. func (v *View) Settings() *pb.UI_Settings { return v.settings } -// Close closes any open file handles +// Close closes any open file handles. func (v *View) Close() error { if v.stderr != nil { err := v.stderr.Close() @@ -120,49 +121,53 @@ func (v *View) Close() error { return nil } -// ShowFormat shows the output of a format request +// ShowFormat shows the output of a format request. func (v *View) ShowFormat(cfg *pb.FormatRequest_Config, res *pb.FormatResponse) error { if err := v.write(res); err != nil { return err } + return status.Format(cfg, res) } -// ShowError writes the given error to stdout in the formatted version +// ShowError writes the given error to stdout in the formatted version. func (v *View) ShowError(err error) error { return v.writeError(err) } -// ShowDiagnostics writes the given diagnostic to stdout in the formatted version +// ShowDiagnostics writes the given diagnostic to stdout in the formatted version. func (v *View) ShowDiagnostics(diags []*pb.Diagnostic) error { return v.writeDiagnostics(diags) } -// ShowVersion shows the version information +// ShowVersion shows the version information. func (v *View) ShowVersion(all bool, res *pb.GetVersionResponse) error { if err := v.write(res); err != nil { return err } + return status.GetVersion(res) } -// ShowScenariosValidateConfig shows the validation response +// ShowScenariosValidateConfig shows the validation response. func (v *View) ShowScenariosValidateConfig(res *pb.ValidateScenariosConfigurationResponse) error { if err := v.write(res); err != nil { return err } + return status.ScenariosValidateConfig(v.settings.GetFailOnWarnings(), res) } -// ShowScenarioList shows the a list of scenarios +// ShowScenarioList shows the a list of scenarios. func (v *View) ShowScenarioList(res *pb.ListScenariosResponse) error { if err := v.write(res); err != nil { return err } + return status.ListScenarios(v.settings.GetFailOnWarnings(), res) } -// ShowDecode shows the decode response unless it's a incremental update +// ShowDecode shows the decode response unless it's a incremental update. func (v *View) ShowDecode(res *pb.DecodeResponse, incremental bool) error { if incremental { // machine output doesn't show incremental update so we early return @@ -176,7 +181,7 @@ func (v *View) ShowDecode(res *pb.DecodeResponse, incremental bool) error { return status.Decode(v.Settings().GetFailOnWarnings(), res) } -// ShowOutput shows output response +// ShowOutput shows output response. func (v *View) ShowOutput(out *pb.OperationResponses) error { if err := v.write(out); err != nil { return err @@ -185,11 +190,11 @@ func (v *View) ShowOutput(out *pb.OperationResponses) error { return status.OperationResponses(v.Settings().GetFailOnWarnings(), out) } -// ShowOperationEvent does nothing as the machine output doesn't stream events +// ShowOperationEvent does nothing as the machine output doesn't stream events. func (v *View) ShowOperationEvent(*pb.Operation_Event) { } -// ShowOperationResponse shows an operation response +// ShowOperationResponse shows an operation response. func (v *View) ShowOperationResponse(res *pb.Operation_Response) error { if err := v.write(res); err != nil { return err @@ -205,7 +210,7 @@ func (v *View) ShowOperationResponse(res *pb.Operation_Response) error { ) } -// ShowOperationResponses shows the results of multiple operations +// ShowOperationResponses shows the results of multiple operations. func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { if err := v.write(res); err != nil { return err @@ -214,7 +219,7 @@ func (v *View) ShowOperationResponses(res *pb.OperationResponses) error { return status.OperationResponses(v.settings.GetFailOnWarnings(), res) } -// writeError does our best to write the given error to our stderr +// writeError does our best to write the given error to our stderr. func (v *View) writeError(err error) error { tryJSON := func(err error) error { msg := &errJSON{ @@ -249,17 +254,26 @@ func (v *View) writeError(err error) error { if err != nil { return tryPlainText(err) } + + return nil + case pb.UI_Settings_FORMAT_UNSPECIFIED, pb.UI_Settings_FORMAT_BASIC_TEXT: + err := tryJSON(fmt.Errorf("%w: %s", err, NewErrUnsupportedEncodingFormat(v.settings.GetFormat()).Error())) + if err != nil { + return tryPlainText(err) + } + return nil default: err := tryJSON(fmt.Errorf("%w: %s", err, NewErrUnsupportedEncodingFormat(v.settings.GetFormat()).Error())) if err != nil { return tryPlainText(err) } + return nil } } -// writeDiagnostics does our best to write the diagnostics to our stderr +// writeDiagnostics does our best to write the diagnostics to our stderr. func (v *View) writeDiagnostics(diags []*pb.Diagnostic) error { tryJSON := func(diags []*pb.Diagnostic) error { msg := &diagsJSON{ @@ -270,6 +284,7 @@ func (v *View) writeDiagnostics(diags []*pb.Diagnostic) error { bytes, err := protojson.Marshal(diag) if err != nil { msg.Diags = append(msg.Diags, []byte(err.Error())) + continue } msg.Diags = append(msg.Diags, bytes) @@ -278,6 +293,7 @@ func (v *View) writeDiagnostics(diags []*pb.Diagnostic) error { bytes, err := json.Marshal(msg) if err != nil { _, _ = v.ui.Stderr.Write([]byte(err.Error())) + return err } @@ -301,6 +317,17 @@ func (v *View) writeDiagnostics(diags []*pb.Diagnostic) error { if err != nil { return tryPlainText(err) } + + return nil + case pb.UI_Settings_FORMAT_UNSPECIFIED, pb.UI_Settings_FORMAT_BASIC_TEXT: + diags = append(diags, diagnostics.FromErr( + NewErrUnsupportedEncodingFormat(v.settings.GetFormat()), + )...) + err := tryJSON(diags) + if err != nil { + return tryPlainText(err) + } + return nil default: diags = append(diags, diagnostics.FromErr( @@ -310,11 +337,12 @@ func (v *View) writeDiagnostics(diags []*pb.Diagnostic) error { if err != nil { return tryPlainText(err) } + return nil } } -// write takes a proto messages and writes it to the desired output +// write takes a proto messages and writes it to the desired output. func (v *View) write(msg proto.Message) error { var err error var bytes []byte @@ -325,10 +353,13 @@ func (v *View) write(msg proto.Message) error { if err != nil { return err } + case pb.UI_Settings_FORMAT_UNSPECIFIED, pb.UI_Settings_FORMAT_BASIC_TEXT: + return NewErrUnsupportedEncodingFormat(v.settings.GetFormat()) default: return NewErrUnsupportedEncodingFormat(v.settings.GetFormat()) } _, err = v.ui.Stdout.Write(bytes) + return err } diff --git a/internal/ui/status/diagnostics.go b/internal/ui/status/diagnostics.go index c26a4a7d..3a52dd87 100644 --- a/internal/ui/status/diagnostics.go +++ b/internal/ui/status/diagnostics.go @@ -5,12 +5,12 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// ResWithDiags is an interface of a response that has diagnostics +// ResWithDiags is an interface of a response that has diagnostics. type ResWithDiags interface { GetDiagnostics() []*pb.Diagnostic } -// HasErrorDiags returns whether or not the response has error diagnostics +// HasErrorDiags returns whether or not the response has error diagnostics. func HasErrorDiags(res ...ResWithDiags) bool { if len(res) < 1 { return false @@ -19,7 +19,7 @@ func HasErrorDiags(res ...ResWithDiags) bool { return diagnostics.HasErrors(combinedResWithDiags(res)) } -// HasWarningDiags returns whether or not the response has warning diagnostics +// HasWarningDiags returns whether or not the response has warning diagnostics. func HasWarningDiags(res ...ResWithDiags) bool { if res == nil { return false diff --git a/internal/ui/status/error.go b/internal/ui/status/error.go index 4728e510..a9a0786a 100644 --- a/internal/ui/status/error.go +++ b/internal/ui/status/error.go @@ -8,7 +8,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// Error takes a message and optional errors to wrap and returns a new error +// Error takes a message and optional errors to wrap and returns a new error. func Error(msg string, errs ...error) error { err := fmt.Errorf(msg) for _, err2 := range errs { @@ -20,7 +20,7 @@ func Error(msg string, errs ...error) error { return err } -// ErrExit is an error that contains requested special exit behavior +// ErrExit is an error that contains requested special exit behavior. type ErrExit struct { Err error ExitCode int @@ -35,19 +35,20 @@ func (e *ErrExit) Error() string { return Error(e.Msg, e.Err).Error() } -// ErrDiagnostic is an error that can carry diagnostics information +// ErrDiagnostic is an error that can carry diagnostics information. type ErrDiagnostic struct { Diags []*pb.Diagnostic DiagStringOpts []diagnostics.StringOpt Err error } -// Error returns a joined message from all diagnostics errors +// Error returns a joined message from all diagnostics errors. func (e *ErrDiagnostic) Error() string { if e.Diags == nil { if e.Err != nil { return e.Err.Error() } + return "" } @@ -59,7 +60,7 @@ func (e *ErrDiagnostic) Error() string { return msg.String() } -// Unwrap returns the wrapped error +// Unwrap returns the wrapped error. func (e *ErrDiagnostic) Unwrap() error { return e.Err } diff --git a/internal/ui/status/format.go b/internal/ui/status/format.go index 9a950394..4e2fb2e5 100644 --- a/internal/ui/status/format.go +++ b/internal/ui/status/format.go @@ -6,7 +6,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// Format returns the format status +// Format returns the format status. func Format(cfg *pb.FormatRequest_Config, res *pb.FormatResponse) error { checkFailed := false var err error diff --git a/internal/ui/status/scenarios.go b/internal/ui/status/scenarios.go index 6f1f5a4b..ef0c8c3d 100644 --- a/internal/ui/status/scenarios.go +++ b/internal/ui/status/scenarios.go @@ -5,7 +5,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// Decode returns the status for a fligth plan decode +// Decode returns the status for a fligth plan decode. func Decode(failOnWarn bool, res *pb.DecodeResponse) error { if HasFailed(failOnWarn, res) { return Error("failed to decode") @@ -14,7 +14,7 @@ func Decode(failOnWarn bool, res *pb.DecodeResponse) error { return nil } -// OperationResponses returns the status multiple operations +// OperationResponses returns the status multiple operations. func OperationResponses(failOnWarn bool, res *pb.OperationResponses) error { var err error @@ -32,7 +32,7 @@ func OperationResponses(failOnWarn bool, res *pb.OperationResponses) error { return nil } -// OperationResponse returns the status for an operation +// OperationResponse returns the status for an operation. func OperationResponse(failOnWarn bool, res *pb.Operation_Response) error { if diagnostics.OpResFailed(failOnWarn, res) { // Return a status code here because the operation response UI should @@ -44,7 +44,7 @@ func OperationResponse(failOnWarn bool, res *pb.Operation_Response) error { return nil } -// ListScenarios returns the status response for a scenario list +// ListScenarios returns the status response for a scenario list. func ListScenarios(failOnWarn bool, res *pb.ListScenariosResponse) error { if HasFailed(failOnWarn, res, res.GetDecode()) { return Error("failed to list scenarios") diff --git a/internal/ui/status/scenarios_test.go b/internal/ui/status/scenarios_test.go index 4a069195..e054bf3b 100644 --- a/internal/ui/status/scenarios_test.go +++ b/internal/ui/status/scenarios_test.go @@ -134,7 +134,10 @@ func TestOperationResponsesHandlesFailures(t *testing.T) { shouldFail: true, }, } { + test := test t.Run(desc, func(t *testing.T) { + t.Parallel() + err := OperationResponses(test.failOnWarn, test.res) if test.shouldFail { require.Error(t, err) diff --git a/internal/ui/status/scenarios_validate_config.go b/internal/ui/status/scenarios_validate_config.go index e27c7931..0ef64eac 100644 --- a/internal/ui/status/scenarios_validate_config.go +++ b/internal/ui/status/scenarios_validate_config.go @@ -2,7 +2,7 @@ package status import "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" -// ScenariosValidateConfig returns the status response for a flight plan validation +// ScenariosValidateConfig returns the status response for a flight plan validation. func ScenariosValidateConfig(failOnWarn bool, res *pb.ValidateScenariosConfigurationResponse) error { if HasFailed(failOnWarn, res, res.GetDecode()) { return Error("scenario configuration is not valid") diff --git a/internal/ui/status/version.go b/internal/ui/status/version.go index 92631fb2..641eee32 100644 --- a/internal/ui/status/version.go +++ b/internal/ui/status/version.go @@ -4,7 +4,7 @@ import ( "github.com/hashicorp/enos/proto/hashicorp/enos/v1/pb" ) -// GetVersion returns the get version response +// GetVersion returns the get version response. func GetVersion(res *pb.GetVersionResponse) error { if HasErrorDiags(res) { return Error("unable to get version") diff --git a/internal/ui/terminal/ui.go b/internal/ui/terminal/ui.go index a6c6d0bd..2e7f8b34 100644 --- a/internal/ui/terminal/ui.go +++ b/internal/ui/terminal/ui.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/hcl/v2" ) -// RenderTable does a basic render of table data to the desired writer +// RenderTable does a basic render of table data to the desired writer. func (u *UI) RenderTable(header []string, rows [][]string) { table := tablewriter.NewWriter(u.Stdout) @@ -35,7 +35,7 @@ func (u *UI) RenderTable(header []string, rows [][]string) { var _ cli.Ui = (*UI)(nil) -// UI is a CLI UI +// UI is a CLI UI. type UI struct { Stderr io.Writer Stdout io.Writer @@ -56,13 +56,13 @@ type UI struct { ui cli.Ui } -// Level is the output level +// Level is the output level. type Level int -// Opt is a UI option +// Opt is a UI option. type Opt func(*UI) -// NewUI takes zero or more options and returns a new UI +// NewUI takes zero or more options and returns a new UI. func NewUI(opts ...Opt) *UI { ui := &UI{ Level: pb.UI_Settings_LEVEL_INFO, @@ -94,132 +94,132 @@ func NewUI(opts ...Opt) *UI { return ui } -// WithStderr sets stderr +// WithStderr sets stderr. func WithStderr(stderr io.Writer) Opt { return func(ui *UI) { ui.Stderr = stderr } } -// WithStdout sets stdout +// WithStdout sets stdout. func WithStdout(stdout io.Writer) Opt { return func(ui *UI) { ui.Stdout = stdout } } -// WithStdin sets stdin +// WithStdin sets stdin. func WithStdin(stdin io.Reader) Opt { return func(ui *UI) { ui.Stdin = stdin } } -// WithAskPrefix sets the ask prefix +// WithAskPrefix sets the ask prefix. func WithAskPrefix(p string) Opt { return func(ui *UI) { ui.AskPrefix = p } } -// WithAskSecretPrefix sets the ask prefix +// WithAskSecretPrefix sets the ask prefix. func WithAskSecretPrefix(p string) Opt { return func(ui *UI) { ui.AskSecretPrefix = p } } -// WithOutputPrefix sets the output prefix +// WithOutputPrefix sets the output prefix. func WithOutputPrefix(p string) Opt { return func(ui *UI) { ui.OutputPrefix = p } } -// WithInfoPrefix sets the info prefix +// WithInfoPrefix sets the info prefix. func WithInfoPrefix(p string) Opt { return func(ui *UI) { ui.InfoPrefix = p } } -// WithErrorPrefix sets the error prefix +// WithErrorPrefix sets the error prefix. func WithErrorPrefix(p string) Opt { return func(ui *UI) { ui.ErrorPrefix = p } } -// WithWarnPrefix sets the warn prefix +// WithWarnPrefix sets the warn prefix. func WithWarnPrefix(p string) Opt { return func(ui *UI) { ui.WarnPrefix = p } } -// WithLevel sets logging level +// WithLevel sets logging level. func WithLevel(l pb.UI_Settings_Level) Opt { return func(ui *UI) { ui.Level = l } } -// WithColor sets whether or not to use color +// WithColor sets whether or not to use color. func WithColor(use bool) Opt { return func(ui *UI) { ui.UseColor = use } } -// WithWidth sets the line wrapping +// WithWidth sets the line wrapping. func WithWidth(wrap uint) Opt { return func(ui *UI) { ui.Width = wrap } } -// Ask prompts the user for some data +// Ask prompts the user for some data. func (u *UI) Ask(q string) (string, error) { return u.ui.Ask(q) } -// AskSecret prompts the user for some data +// AskSecret prompts the user for some data. func (u *UI) AskSecret(q string) (string, error) { return u.ui.AskSecret(q) } -// Output outputs a message to stdout +// Output outputs a message to stdout. func (u *UI) Output(m string) { u.ui.Output(m) } -// Info outputs a message at info level +// Info outputs a message at info level. func (u *UI) Info(m string) { if u.Level >= pb.UI_Settings_LEVEL_INFO { u.ui.Info(m) } } -// Error outputs a message at error level +// Error outputs a message at error level. func (u *UI) Error(m string) { u.ui.Error(m) } -// Warn outputs a message at warn level +// Warn outputs a message at warn level. func (u *UI) Warn(m string) { if u.Level >= pb.UI_Settings_LEVEL_WARN { u.ui.Warn(m) } } -// Debug outputs a message at warn level +// Debug outputs a message at warn level. func (u *UI) Debug(m string) { if u.Level >= pb.UI_Settings_LEVEL_DEBUG { u.ui.Info(m) } } -// Diagnostics outputs diagnostics to stderr +// Diagnostics outputs diagnostics to stderr. func (u *UI) Diagnostics(files map[string]*hcl.File, diags hcl.Diagnostics) error { return hcl.NewDiagnosticTextWriter( u.Stderr, diff --git a/internal/ui/ui.go b/internal/ui/ui.go index 6f6cce39..99413aed 100644 --- a/internal/ui/ui.go +++ b/internal/ui/ui.go @@ -15,8 +15,10 @@ var ( ) // View is a UI view. ShowX() methods are responsible for taking a command output -// reponse, displaying it appropriately, and exiting with an error if _any_ +// response, displaying it appropriately, and exiting with an error if _any_ // error diagnostics are present in the response. +// +//nolint:interfacebloat // we have reason for a complex UI interface type View interface { io.Closer Settings() *pb.UI_Settings @@ -33,19 +35,22 @@ type View interface { ShowOperationResponses(*pb.OperationResponses) error } -// New takes a UI configuration settings and returns a new view +// New takes a UI configuration settings and returns a new view. func New(s *pb.UI_Settings) (View, error) { switch s.Format { case pb.UI_Settings_FORMAT_JSON: return machine.New(machine.WithUISettings(s)) case pb.UI_Settings_FORMAT_BASIC_TEXT: return basic.New(basic.WithUISettings(s)) + case pb.UI_Settings_FORMAT_UNSPECIFIED: + return basic.New(basic.WithUISettings(s)) default: msg := "unsupported UI format" name, ok := pb.UI_Settings_Format_name[int32(s.Format)] if ok { msg = fmt.Sprintf("%s is not a supported UI format", name) } + return nil, fmt.Errorf(msg) } } diff --git a/proto/hashicorp/enos/v1/pb/enos.pb.go b/proto/hashicorp/enos/v1/pb/enos.pb.go index 1d6a17e8..64ced3fc 100644 --- a/proto/hashicorp/enos/v1/pb/enos.pb.go +++ b/proto/hashicorp/enos/v1/pb/enos.pb.go @@ -3139,6 +3139,7 @@ type Operation_Request struct { Workspace *Workspace `protobuf:"bytes,2,opt,name=workspace,proto3" json:"workspace,omitempty"` Id string `protobuf:"bytes,3,opt,name=id,proto3" json:"id,omitempty"` // Types that are assignable to Value: + // // *Operation_Request_Generate_ // *Operation_Request_Check_ // *Operation_Request_Launch_ @@ -3320,6 +3321,7 @@ type Operation_Response struct { // the ui package. // // Types that are assignable to Value: + // // *Operation_Response_Generate_ // *Operation_Response_Check_ // *Operation_Response_Launch_ @@ -3504,6 +3506,7 @@ type Operation_Event struct { // diagnostics and ui packages to handle the new event types. // // Types that are assignable to Value: + // // *Operation_Event_Decode // *Operation_Event_Generate // *Operation_Event_Init diff --git a/proto/hashicorp/enos/v1/pb/enos_grpc.pb.go b/proto/hashicorp/enos/v1/pb/enos_grpc.pb.go index 4be6ea59..ccaba057 100644 --- a/proto/hashicorp/enos/v1/pb/enos_grpc.pb.go +++ b/proto/hashicorp/enos/v1/pb/enos_grpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go-grpc. DO NOT EDIT. // versions: -// - protoc-gen-go-grpc v1.2.0 +// - protoc-gen-go-grpc v1.3.0 // - protoc (unknown) // source: enos.proto @@ -18,6 +18,22 @@ import ( // Requires gRPC-Go v1.32.0 or later. const _ = grpc.SupportPackageIsVersion7 +const ( + EnosService_GetVersion_FullMethodName = "/hashicorp.enos.v1.EnosService/GetVersion" + EnosService_ValidateScenariosConfiguration_FullMethodName = "/hashicorp.enos.v1.EnosService/ValidateScenariosConfiguration" + EnosService_ListScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/ListScenarios" + EnosService_CheckScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/CheckScenarios" + EnosService_GenerateScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/GenerateScenarios" + EnosService_LaunchScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/LaunchScenarios" + EnosService_DestroyScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/DestroyScenarios" + EnosService_RunScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/RunScenarios" + EnosService_ExecScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/ExecScenarios" + EnosService_OutputScenarios_FullMethodName = "/hashicorp.enos.v1.EnosService/OutputScenarios" + EnosService_Format_FullMethodName = "/hashicorp.enos.v1.EnosService/Format" + EnosService_OperationEventStream_FullMethodName = "/hashicorp.enos.v1.EnosService/OperationEventStream" + EnosService_Operation_FullMethodName = "/hashicorp.enos.v1.EnosService/Operation" +) + // EnosServiceClient is the client API for EnosService service. // // For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream. @@ -47,7 +63,7 @@ func NewEnosServiceClient(cc grpc.ClientConnInterface) EnosServiceClient { func (c *enosServiceClient) GetVersion(ctx context.Context, in *GetVersionRequest, opts ...grpc.CallOption) (*GetVersionResponse, error) { out := new(GetVersionResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/GetVersion", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_GetVersion_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -56,7 +72,7 @@ func (c *enosServiceClient) GetVersion(ctx context.Context, in *GetVersionReques func (c *enosServiceClient) ValidateScenariosConfiguration(ctx context.Context, in *ValidateScenariosConfigurationRequest, opts ...grpc.CallOption) (*ValidateScenariosConfigurationResponse, error) { out := new(ValidateScenariosConfigurationResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/ValidateScenariosConfiguration", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_ValidateScenariosConfiguration_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -65,7 +81,7 @@ func (c *enosServiceClient) ValidateScenariosConfiguration(ctx context.Context, func (c *enosServiceClient) ListScenarios(ctx context.Context, in *ListScenariosRequest, opts ...grpc.CallOption) (*ListScenariosResponse, error) { out := new(ListScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/ListScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_ListScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -74,7 +90,7 @@ func (c *enosServiceClient) ListScenarios(ctx context.Context, in *ListScenarios func (c *enosServiceClient) CheckScenarios(ctx context.Context, in *CheckScenariosRequest, opts ...grpc.CallOption) (*CheckScenariosResponse, error) { out := new(CheckScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/CheckScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_CheckScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -83,7 +99,7 @@ func (c *enosServiceClient) CheckScenarios(ctx context.Context, in *CheckScenari func (c *enosServiceClient) GenerateScenarios(ctx context.Context, in *GenerateScenariosRequest, opts ...grpc.CallOption) (*GenerateScenariosResponse, error) { out := new(GenerateScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/GenerateScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_GenerateScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -92,7 +108,7 @@ func (c *enosServiceClient) GenerateScenarios(ctx context.Context, in *GenerateS func (c *enosServiceClient) LaunchScenarios(ctx context.Context, in *LaunchScenariosRequest, opts ...grpc.CallOption) (*LaunchScenariosResponse, error) { out := new(LaunchScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/LaunchScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_LaunchScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -101,7 +117,7 @@ func (c *enosServiceClient) LaunchScenarios(ctx context.Context, in *LaunchScena func (c *enosServiceClient) DestroyScenarios(ctx context.Context, in *DestroyScenariosRequest, opts ...grpc.CallOption) (*DestroyScenariosResponse, error) { out := new(DestroyScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/DestroyScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_DestroyScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -110,7 +126,7 @@ func (c *enosServiceClient) DestroyScenarios(ctx context.Context, in *DestroySce func (c *enosServiceClient) RunScenarios(ctx context.Context, in *RunScenariosRequest, opts ...grpc.CallOption) (*RunScenariosResponse, error) { out := new(RunScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/RunScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_RunScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -119,7 +135,7 @@ func (c *enosServiceClient) RunScenarios(ctx context.Context, in *RunScenariosRe func (c *enosServiceClient) ExecScenarios(ctx context.Context, in *ExecScenariosRequest, opts ...grpc.CallOption) (*ExecScenariosResponse, error) { out := new(ExecScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/ExecScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_ExecScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -128,7 +144,7 @@ func (c *enosServiceClient) ExecScenarios(ctx context.Context, in *ExecScenarios func (c *enosServiceClient) OutputScenarios(ctx context.Context, in *OutputScenariosRequest, opts ...grpc.CallOption) (*OutputScenariosResponse, error) { out := new(OutputScenariosResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/OutputScenarios", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_OutputScenarios_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -137,7 +153,7 @@ func (c *enosServiceClient) OutputScenarios(ctx context.Context, in *OutputScena func (c *enosServiceClient) Format(ctx context.Context, in *FormatRequest, opts ...grpc.CallOption) (*FormatResponse, error) { out := new(FormatResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/Format", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_Format_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -145,7 +161,7 @@ func (c *enosServiceClient) Format(ctx context.Context, in *FormatRequest, opts } func (c *enosServiceClient) OperationEventStream(ctx context.Context, in *OperationEventStreamRequest, opts ...grpc.CallOption) (EnosService_OperationEventStreamClient, error) { - stream, err := c.cc.NewStream(ctx, &EnosService_ServiceDesc.Streams[0], "/hashicorp.enos.v1.EnosService/OperationEventStream", opts...) + stream, err := c.cc.NewStream(ctx, &EnosService_ServiceDesc.Streams[0], EnosService_OperationEventStream_FullMethodName, opts...) if err != nil { return nil, err } @@ -178,7 +194,7 @@ func (x *enosServiceOperationEventStreamClient) Recv() (*OperationEventStreamRes func (c *enosServiceClient) Operation(ctx context.Context, in *OperationRequest, opts ...grpc.CallOption) (*OperationResponse, error) { out := new(OperationResponse) - err := c.cc.Invoke(ctx, "/hashicorp.enos.v1.EnosService/Operation", in, out, opts...) + err := c.cc.Invoke(ctx, EnosService_Operation_FullMethodName, in, out, opts...) if err != nil { return nil, err } @@ -269,7 +285,7 @@ func _EnosService_GetVersion_Handler(srv interface{}, ctx context.Context, dec f } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/GetVersion", + FullMethod: EnosService_GetVersion_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).GetVersion(ctx, req.(*GetVersionRequest)) @@ -287,7 +303,7 @@ func _EnosService_ValidateScenariosConfiguration_Handler(srv interface{}, ctx co } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/ValidateScenariosConfiguration", + FullMethod: EnosService_ValidateScenariosConfiguration_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).ValidateScenariosConfiguration(ctx, req.(*ValidateScenariosConfigurationRequest)) @@ -305,7 +321,7 @@ func _EnosService_ListScenarios_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/ListScenarios", + FullMethod: EnosService_ListScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).ListScenarios(ctx, req.(*ListScenariosRequest)) @@ -323,7 +339,7 @@ func _EnosService_CheckScenarios_Handler(srv interface{}, ctx context.Context, d } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/CheckScenarios", + FullMethod: EnosService_CheckScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).CheckScenarios(ctx, req.(*CheckScenariosRequest)) @@ -341,7 +357,7 @@ func _EnosService_GenerateScenarios_Handler(srv interface{}, ctx context.Context } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/GenerateScenarios", + FullMethod: EnosService_GenerateScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).GenerateScenarios(ctx, req.(*GenerateScenariosRequest)) @@ -359,7 +375,7 @@ func _EnosService_LaunchScenarios_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/LaunchScenarios", + FullMethod: EnosService_LaunchScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).LaunchScenarios(ctx, req.(*LaunchScenariosRequest)) @@ -377,7 +393,7 @@ func _EnosService_DestroyScenarios_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/DestroyScenarios", + FullMethod: EnosService_DestroyScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).DestroyScenarios(ctx, req.(*DestroyScenariosRequest)) @@ -395,7 +411,7 @@ func _EnosService_RunScenarios_Handler(srv interface{}, ctx context.Context, dec } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/RunScenarios", + FullMethod: EnosService_RunScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).RunScenarios(ctx, req.(*RunScenariosRequest)) @@ -413,7 +429,7 @@ func _EnosService_ExecScenarios_Handler(srv interface{}, ctx context.Context, de } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/ExecScenarios", + FullMethod: EnosService_ExecScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).ExecScenarios(ctx, req.(*ExecScenariosRequest)) @@ -431,7 +447,7 @@ func _EnosService_OutputScenarios_Handler(srv interface{}, ctx context.Context, } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/OutputScenarios", + FullMethod: EnosService_OutputScenarios_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).OutputScenarios(ctx, req.(*OutputScenariosRequest)) @@ -449,7 +465,7 @@ func _EnosService_Format_Handler(srv interface{}, ctx context.Context, dec func( } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/Format", + FullMethod: EnosService_Format_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).Format(ctx, req.(*FormatRequest)) @@ -488,7 +504,7 @@ func _EnosService_Operation_Handler(srv interface{}, ctx context.Context, dec fu } info := &grpc.UnaryServerInfo{ Server: srv, - FullMethod: "/hashicorp.enos.v1.EnosService/Operation", + FullMethod: EnosService_Operation_FullMethodName, } handler := func(ctx context.Context, req interface{}) (interface{}, error) { return srv.(EnosServiceServer).Operation(ctx, req.(*OperationRequest)) diff --git a/tools/homebrew/main.go b/tools/homebrew/main.go index 7b3b830e..c1e49aeb 100644 --- a/tools/homebrew/main.go +++ b/tools/homebrew/main.go @@ -81,7 +81,7 @@ func newCreateFormulaCommand() *cobra.Command { return createFormula } -// Get the the version, the version tag, and the SHASUMS of each asset from the SHASUMS file +// Get the version, the version tag, and the SHASUMS of each asset from the SHASUMS file. func readMetadata(path string) (*metadata, error) { metadata := &metadata{} input, err := os.Open(path) @@ -139,22 +139,23 @@ func readMetadata(path string) (*metadata, error) { return metadata, nil } -// Execute the template with the metadata values to `dest` +// Execute the template with the metadata values to `dest`. func renderHomebrewFormulaTemplate(dest io.Writer, metadataPath string) error { metadata, err := readMetadata(metadataPath) if err != nil { - fmt.Printf("reading metadata: %s\n", err.Error()) + return fmt.Errorf("reading metadata: %s", err.Error()) } // Execute the template using metadata values from the SHASUMS file err = t.Execute(dest, metadata) if err != nil { - fmt.Printf("executing template: %s\n", err.Error()) + return fmt.Errorf("executing template: %s", err.Error()) } + return nil } -// Write the executed template to an output file +// Write the executed template to an output file. func createFormula(cmd *cobra.Command, args []string) error { buf := bytes.Buffer{} diff --git a/tools/homebrew/main_test.go b/tools/homebrew/main_test.go index 3d68b18c..113bd301 100644 --- a/tools/homebrew/main_test.go +++ b/tools/homebrew/main_test.go @@ -10,8 +10,10 @@ import ( "github.com/stretchr/testify/require" ) -// Ensure that the SHASUMS file is parsed accurately +// Ensure that the SHASUMS file is parsed accurately. func Test_decodeMetadata(t *testing.T) { + t.Parallel() + p, err := filepath.Abs("./support/enos_0.0.1_SHA256SUMS") require.NoError(t, err) @@ -29,8 +31,10 @@ func Test_decodeMetadata(t *testing.T) { require.Equal(t, expected, sums) } -// Ensure that the template renders correctly +// Ensure that the template renders correctly. func Test_renderHomebrewFormulaTemplate(t *testing.T) { + t.Parallel() + p, err := filepath.Abs("./support/enos_0.0.1_SHA256SUMS") require.NoError(t, err)