From 22dcc5186b054cf32030cb2979f346f576e262a7 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Fri, 23 Feb 2024 09:41:01 +0100 Subject: [PATCH 1/2] fix: handle nil expressions in earlydecoder when parsing outputs with incomplete provider defined functions --- earlydecoder/decoder_test.go | 36 ++++++++++++++++++++++++++++++++++++ earlydecoder/load_module.go | 4 +++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/earlydecoder/decoder_test.go b/earlydecoder/decoder_test.go index 3866a22e..58ea1b49 100644 --- a/earlydecoder/decoder_test.go +++ b/earlydecoder/decoder_test.go @@ -567,6 +567,42 @@ resource "google_something" "test" { runTestCases(testCases, t, path) } +func TestLoadModule_nil_expr(t *testing.T) { + path := t.TempDir() + + cfg := ` + output "foo" { + value = provider:: + }` + + // We're ignoring diagnostics here, since our config contains invalid HCL + f, _ := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) + + files := map[string]*hcl.File{ + "test.tf": f, + } + + meta, diags := LoadModule(path, files) + + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + + expectedMeta := &module.Meta{ + Path: path, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{"foo": {}}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.DeclaredModuleCall{}, + } + + if diff := cmp.Diff(expectedMeta, meta, customComparer...); diff != "" { + t.Fatalf("module meta doesn't match: %s", diff) + } +} + func TestLoadModule_Variables(t *testing.T) { path := t.TempDir() diff --git a/earlydecoder/load_module.go b/earlydecoder/load_module.go index 96174e3c..5e9e3a5d 100644 --- a/earlydecoder/load_module.go +++ b/earlydecoder/load_module.go @@ -277,7 +277,9 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { diags = append(diags, valDiags...) } value := cty.NilVal - if attr, defined := content.Attributes["value"]; defined { + // skip if attr.Expr is nil, which can happen when a module is opened that has incomplete + // provider defined functions in outputs (might happen when a wip project is opened) + if attr, defined := content.Attributes["value"]; defined && attr.Expr != nil { // TODO: Provide context w/ funcs and variables val, diags := attr.Expr.Value(nil) if !diags.HasErrors() { From e096c50ec8c0e3643669b00b95246b7d91ebc551 Mon Sep 17 00:00:00 2001 From: Ansgar Mertens Date: Thu, 29 Feb 2024 11:00:32 +0100 Subject: [PATCH 2/2] fix: catch and test all possible nil expressions for partial provider defined function expressions --- earlydecoder/backend.go | 4 +- earlydecoder/decoder_test.go | 170 ++++++++++++++++++++++---- earlydecoder/load_module.go | 26 ++-- earlydecoder/provider_requirements.go | 4 + 4 files changed, 164 insertions(+), 40 deletions(-) diff --git a/earlydecoder/backend.go b/earlydecoder/backend.go index 998edea9..6100691b 100644 --- a/earlydecoder/backend.go +++ b/earlydecoder/backend.go @@ -15,7 +15,7 @@ func decodeBackendsBlock(block *hcl.Block) (backend.BackendData, hcl.Diagnostics switch bType { case "remote": - if attr, ok := attrs["hostname"]; ok { + if attr, ok := attrs["hostname"]; ok && attr.Expr != nil { val, vDiags := attr.Expr.Value(nil) diags = append(diags, vDiags...) if val.IsWhollyKnown() && val.Type() == cty.String { @@ -38,7 +38,7 @@ func decodeCloudBlock(block *hcl.Block) (*backend.Cloud, hcl.Diagnostics) { // https://developer.hashicorp.com/terraform/language/settings/terraform-cloud#usage-example // Required for Terraform Enterprise // Defaults to app.terraform.io for Terraform Cloud - if attr, ok := attrs["hostname"]; ok { + if attr, ok := attrs["hostname"]; ok && attr.Expr != nil { val, vDiags := attr.Expr.Value(nil) if val.IsWhollyKnown() && val.Type() == cty.String { return &backend.Cloud{ diff --git a/earlydecoder/decoder_test.go b/earlydecoder/decoder_test.go index 58ea1b49..66088d5d 100644 --- a/earlydecoder/decoder_test.go +++ b/earlydecoder/decoder_test.go @@ -570,36 +570,156 @@ resource "google_something" "test" { func TestLoadModule_nil_expr(t *testing.T) { path := t.TempDir() - cfg := ` - output "foo" { - value = provider:: - }` - - // We're ignoring diagnostics here, since our config contains invalid HCL - f, _ := hclsyntax.ParseConfig([]byte(cfg), "test.tf", hcl.InitialPos) - - files := map[string]*hcl.File{ - "test.tf": f, + testCases := []struct { + name string + cfg string + }{ + { + "remote backend hostname", + `terraform { + backend "remote" { + hostname = provider:: + } +}`, + }, + { + "cloud block hostname", + `terraform { + cloud { + hostname = provider:: + } +}`, + }, + { + "required providers", + `terraform { + required_providers { + aws = provider:: + } +}`, + }, + { + "required providers nested version", + `terraform { + required_providers { + aws = { + version = provider:: + } + } +}`, + }, + { + "required providers nested configuration_aliases", + `terraform { + required_providers { + aws = { + source = "hashicorp/aws" + configuration_aliases = [ provider:: ] + } + } +}`, + }, + { + "terraform required_version", + `terraform { + required_version = provider:: +}`, + }, + { + "provider block version", + `provider "aws" { + version = provider:: +}`, + }, + { + "provider block alias", + `provider "aws" { + alias = provider:: +}`, + }, + { + "variable description", + `variable "foo" { + description = provider:: +}`, + }, + { + "variable sensitive", + `variable "foo" { + sensitive = provider:: +}`, + }, + { + "variable default", + `variable "foo" { + default = provider:: +}`, + }, + { + "variable type", + `variable "foo" { + type = provider:: +}`, + }, + { + "output description", + `output "foo" { + description = provider:: +}`, + }, + { + "output sensitive", + `output "foo" { + sensitive = provider:: +}`, + }, + { + "output value", + `output "foo" { + value = provider:: +}`, + }, + { + "module source", + `module "foo" { + source = provider:: +}`, + }, + { + "module version", + `module "foo" { + version = provider:: +}`, + }, + { + "resource provider alias", + `resource "aws_instance" "foo" { + provider = provider:: +}`, + }, + { + "data provider alias", + `data "aws_instance" "foo" { + provider = provider:: +}`, + }, } - meta, diags := LoadModule(path, files) + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + // We're ignoring diagnostics here, since our config contains invalid HCL + f, _ := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) - if len(diags) > 0 { - t.Fatalf("unexpected diagnostics: %s", diags) - } + files := map[string]*hcl.File{ + "test.tf": f, + } - expectedMeta := &module.Meta{ - Path: path, - ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, - ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, - Variables: map[string]module.Variable{}, - Outputs: map[string]module.Output{"foo": {}}, - Filenames: []string{"test.tf"}, - ModuleCalls: map[string]module.DeclaredModuleCall{}, - } + _, diags := LoadModule(path, files) - if diff := cmp.Diff(expectedMeta, meta, customComparer...); diff != "" { - t.Fatalf("module meta doesn't match: %s", diff) + if len(diags) > 0 { + t.Fatalf("unexpected diagnostics: %s", diags) + } + }) } } diff --git a/earlydecoder/load_module.go b/earlydecoder/load_module.go index 5e9e3a5d..b865e596 100644 --- a/earlydecoder/load_module.go +++ b/earlydecoder/load_module.go @@ -68,7 +68,7 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { content, _, contentDiags := block.Body.PartialContent(terraformBlockSchema) diags = append(diags, contentDiags...) - if attr, defined := content.Attributes["required_version"]; defined { + if attr, defined := content.Attributes["required_version"]; defined && attr.Expr != nil { var version string valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version) diags = append(diags, valDiags...) @@ -136,7 +136,7 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { if _, exists := mod.ProviderRequirements[name]; !exists { mod.ProviderRequirements[name] = &providerRequirement{} } - if attr, defined := content.Attributes["version"]; defined { + if attr, defined := content.Attributes["version"]; defined && attr.Expr != nil { var version string valDiags := gohcl.DecodeExpression(attr.Expr, nil, &version) diags = append(diags, valDiags...) @@ -147,7 +147,7 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { providerKey := name var alias string - if attr, defined := content.Attributes["alias"]; defined { + if attr, defined := content.Attributes["alias"]; defined && attr.Expr != nil { valDiags := gohcl.DecodeExpression(attr.Expr, nil, &alias) diags = append(diags, valDiags...) if !valDiags.HasErrors() && alias != "" { @@ -171,7 +171,7 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { mod.DataSources[ds.MapKey()] = ds - if attr, defined := content.Attributes["provider"]; defined { + if attr, defined := content.Attributes["provider"]; defined && attr.Expr != nil { ref, aDiags := decodeProviderAttribute(attr) diags = append(diags, aDiags...) ds.Provider = ref @@ -194,7 +194,7 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { mod.Resources[r.MapKey()] = r - if attr, defined := content.Attributes["provider"]; defined { + if attr, defined := content.Attributes["provider"]; defined && attr.Expr != nil { ref, aDiags := decodeProviderAttribute(attr) diags = append(diags, aDiags...) r.Provider = ref @@ -216,22 +216,22 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { description := "" isSensitive := false var valDiags hcl.Diagnostics - if attr, defined := content.Attributes["description"]; defined { + if attr, defined := content.Attributes["description"]; defined && attr.Expr != nil { valDiags = gohcl.DecodeExpression(attr.Expr, nil, &description) diags = append(diags, valDiags...) } varType := cty.DynamicPseudoType var defaults *typeexpr.Defaults - if attr, defined := content.Attributes["type"]; defined { + if attr, defined := content.Attributes["type"]; defined && attr.Expr != nil { varType, defaults, valDiags = typeexpr.TypeConstraintWithDefaults(attr.Expr) diags = append(diags, valDiags...) } - if attr, defined := content.Attributes["sensitive"]; defined { + if attr, defined := content.Attributes["sensitive"]; defined && attr.Expr != nil { valDiags = gohcl.DecodeExpression(attr.Expr, nil, &isSensitive) diags = append(diags, valDiags...) } defaultValue := cty.NilVal - if attr, defined := content.Attributes["default"]; defined { + if attr, defined := content.Attributes["default"]; defined && attr.Expr != nil { val, diags := attr.Expr.Value(nil) if !diags.HasErrors() { if varType != cty.NilType { @@ -268,11 +268,11 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { description := "" isSensitive := false var valDiags hcl.Diagnostics - if attr, defined := content.Attributes["description"]; defined { + if attr, defined := content.Attributes["description"]; defined && attr.Expr != nil { valDiags = gohcl.DecodeExpression(attr.Expr, nil, &description) diags = append(diags, valDiags...) } - if attr, defined := content.Attributes["sensitive"]; defined { + if attr, defined := content.Attributes["sensitive"]; defined && attr.Expr != nil { valDiags = gohcl.DecodeExpression(attr.Expr, nil, &isSensitive) diags = append(diags, valDiags...) } @@ -302,11 +302,11 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { var versionCons version.Constraints var valDiags hcl.Diagnostics - if attr, defined := content.Attributes["source"]; defined { + if attr, defined := content.Attributes["source"]; defined && attr.Expr != nil { valDiags = gohcl.DecodeExpression(attr.Expr, nil, &source) diags = append(diags, valDiags...) } - if attr, defined := content.Attributes["version"]; defined { + if attr, defined := content.Attributes["version"]; defined && attr.Expr != nil { var versionStr string valDiags = gohcl.DecodeExpression(attr.Expr, nil, &versionStr) diags = append(diags, valDiags...) diff --git a/earlydecoder/provider_requirements.go b/earlydecoder/provider_requirements.go index 4a151ce7..608337fc 100644 --- a/earlydecoder/provider_requirements.go +++ b/earlydecoder/provider_requirements.go @@ -22,6 +22,10 @@ func decodeRequiredProvidersBlock(block *hcl.Block) (map[string]*providerRequire attrs, diags := block.Body.JustAttributes() reqs := make(map[string]*providerRequirement) for name, attr := range attrs { + if attr.Expr == nil { + continue + } + // Look for a legacy version in the attribute first if expr, err := attr.Expr.Value(nil); err == nil && expr.Type().IsPrimitiveType() { var version string