diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e20c6e5..019e37e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -6,3 +6,4 @@ To run preview: 2. `pnpm install` 3. `cd ..` 4. `go run ./cmd/preview/main.go web --pnpm=site` +5. visit http://localhost:5173/?testcontrols=true diff --git a/preview_test.go b/preview_test.go index a72dfab..afa1531 100644 --- a/preview_test.go +++ b/preview_test.go @@ -5,8 +5,11 @@ import ( "encoding/json" "os" "path/filepath" + "regexp" + "slices" "testing" + "github.com/hashicorp/hcl/v2" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -38,6 +41,7 @@ func Test_Extract(t *testing.T) { expTags map[string]string unknownTags []string params map[string]assertParam + warnings []*regexp.Regexp }{ { name: "bad param values", @@ -402,6 +406,19 @@ func Test_Extract(t *testing.T) { "beta": ap().unknown(), }, }, + { + name: "missing_module", + dir: "missingmodule", + expTags: map[string]string{}, + input: preview.Input{ + ParameterValues: map[string]string{}, + }, + unknownTags: []string{}, + params: map[string]assertParam{}, + warnings: []*regexp.Regexp{ + regexp.MustCompile("Module not loaded"), + }, + }, { skip: "skip until https://github.com/aquasecurity/trivy/pull/8479 is resolved", name: "submodcount", @@ -440,6 +457,16 @@ func Test_Extract(t *testing.T) { } require.False(t, diags.HasErrors()) + if len(tc.warnings) > 0 { + for _, w := range tc.warnings { + idx := slices.IndexFunc(diags, func(diagnostic *hcl.Diagnostic) bool { + return w.MatchString(diagnostic.Error()) + + }) + require.Greater(t, idx, -1, "expected warning %q to be present in diags", w.String()) + } + } + // Assert tags validTags := output.WorkspaceTags.Tags() diff --git a/testdata/missingmodule/main.tf b/testdata/missingmodule/main.tf new file mode 100644 index 0000000..98839f1 --- /dev/null +++ b/testdata/missingmodule/main.tf @@ -0,0 +1,12 @@ +module "does-not-exist" { + source = "registry.coder.com/modules/does-not-exist/coder" +} + +module "does-not-exist-2" { + count = 0 + source = "registry.coder.com/modules/does-not-exist/coder" +} + +module "one" { + source = "./one" +} diff --git a/testdata/missingmodule/one/one.tf b/testdata/missingmodule/one/one.tf new file mode 100644 index 0000000..89e1f85 --- /dev/null +++ b/testdata/missingmodule/one/one.tf @@ -0,0 +1,3 @@ +module "onea" { + source = "./onea" +} diff --git a/testdata/missingmodule/one/onea/onea.tf b/testdata/missingmodule/one/onea/onea.tf new file mode 100644 index 0000000..54718cf --- /dev/null +++ b/testdata/missingmodule/one/onea/onea.tf @@ -0,0 +1,14 @@ +terraform { + required_providers { + coder = { + source = "coder/coder" + version = "2.4.0-pre0" + } + } +} + +data "null_data_source" "values" { + inputs = { + foo = "bar" + } +} diff --git a/testdata/missingmodule/skipe2e b/testdata/missingmodule/skipe2e new file mode 100644 index 0000000..f43c945 --- /dev/null +++ b/testdata/missingmodule/skipe2e @@ -0,0 +1 @@ +Not a real module diff --git a/warnings.go b/warnings.go index 8a55e3b..781f49a 100644 --- a/warnings.go +++ b/warnings.go @@ -11,6 +11,57 @@ import ( func warnings(modules terraform.Modules) hcl.Diagnostics { var diags hcl.Diagnostics diags = diags.Extend(unexpandedCountBlocks(modules)) + diags = diags.Extend(unresolvedModules(modules)) + + return diags +} + +// unresolvedModules does a best effort to try and detect if some modules +// failed to resolve. This is usually because `terraform init` is not run. +func unresolvedModules(modules terraform.Modules) hcl.Diagnostics { + var diags hcl.Diagnostics + modulesUsed := make(map[string]bool) + modulesByID := make(map[string]*terraform.Block) + + // There is no easy way to know if a `module` failed to resolve. The failure is + // only logged in the trivy package. No errors are returned to the caller. So + // instead this code will infer a failed resolution by checking if any blocks + // exist that reference each `module` block. This will work as long as the module + // has some content. If a module is completely empty, then it will be detected as + // "not loaded". + blocks := modules.GetBlocks() + for _, block := range blocks { + if block.InModule() && block.ModuleBlock() != nil { + modulesUsed[block.ModuleBlock().ID()] = true + } + + if block.Type() == "module" { + modulesByID[block.ID()] = block + _, ok := modulesUsed[block.ID()] + if !ok { + modulesUsed[block.ID()] = false + } + } + } + + for id, v := range modulesUsed { + if !v { + block, ok := modulesByID[id] + if ok { + label := block.Type() + for _, l := range block.Labels() { + label += " " + fmt.Sprintf("%q", l) + } + + diags = diags.Append(&hcl.Diagnostic{ + Severity: hcl.DiagWarning, + Summary: "Module not loaded. Did you run `terraform init`?", + Detail: fmt.Sprintf("Module '%s' in file %q cannot be resolved. This module will be ignored.", label, block.HCLBlock().DefRange), + Subject: &(block.HCLBlock().DefRange), + }) + } + } + } return diags }