From 21dfff1990314a68c2bddf1a5d4145eb0102727c Mon Sep 17 00:00:00 2001 From: Radek Simko Date: Thu, 21 Oct 2021 16:14:19 +0100 Subject: [PATCH] earlydecoder: Decode backend block (#78) --- backend/backend.go | 21 +++++++ earlydecoder/backend.go | 29 +++++++++ earlydecoder/decoder.go | 22 +++++++ earlydecoder/decoder_test.go | 119 ++++++++++++++++++++++++++++++++++- earlydecoder/load_module.go | 21 +++++++ earlydecoder/schema.go | 4 ++ module/meta.go | 7 +++ 7 files changed, 220 insertions(+), 3 deletions(-) create mode 100644 backend/backend.go create mode 100644 earlydecoder/backend.go diff --git a/backend/backend.go b/backend/backend.go new file mode 100644 index 00000000..282cbfa1 --- /dev/null +++ b/backend/backend.go @@ -0,0 +1,21 @@ +package backend + +type BackendData interface { + Copy() BackendData +} + +type UnknownBackendData struct{} + +func (*UnknownBackendData) Copy() BackendData { + return &UnknownBackendData{} +} + +type Remote struct { + Hostname string +} + +func (r *Remote) Copy() BackendData { + return &Remote{ + Hostname: r.Hostname, + } +} diff --git a/earlydecoder/backend.go b/earlydecoder/backend.go new file mode 100644 index 00000000..c98d5bdc --- /dev/null +++ b/earlydecoder/backend.go @@ -0,0 +1,29 @@ +package earlydecoder + +import ( + "github.com/hashicorp/hcl/v2" + "github.com/hashicorp/terraform-schema/backend" + "github.com/zclconf/go-cty/cty" +) + +func decodeBackendsBlock(block *hcl.Block) (backend.BackendData, hcl.Diagnostics) { + bType := block.Labels[0] + attrs, diags := block.Body.JustAttributes() + + switch bType { + case "remote": + if attr, ok := attrs["hostname"]; ok { + val, vDiags := attr.Expr.Value(nil) + diags = append(diags, vDiags...) + if val.IsWhollyKnown() && val.Type() == cty.String { + return &backend.Remote{ + Hostname: val.AsString(), + }, nil + } + } + + return &backend.Remote{}, nil + } + + return &backend.UnknownBackendData{}, diags +} diff --git a/earlydecoder/decoder.go b/earlydecoder/decoder.go index b38a1a99..7cf95dbd 100644 --- a/earlydecoder/decoder.go +++ b/earlydecoder/decoder.go @@ -32,6 +32,27 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag coreRequirements = append(coreRequirements, c...) } + var backend *module.Backend + if len(mod.Backends) == 1 { + for bType, data := range mod.Backends { + backend = &module.Backend{ + Type: bType, + Data: data, + } + } + } else if len(mod.Backends) > 1 { + backendTypes := make([]string, len(mod.Backends)) + for bType := range mod.Backends { + backendTypes = append(backendTypes, bType) + } + + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Unable to parse backend configuration", + Detail: fmt.Sprintf("Multiple backend definitions: %q", backendTypes), + }) + } + var ( providerRequirements = make(map[tfaddr.Provider]version.Constraints, 0) refs = make(map[module.ProviderRef]tfaddr.Provider, 0) @@ -139,6 +160,7 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag return &module.Meta{ Path: path, + Backend: backend, ProviderReferences: refs, ProviderRequirements: providerRequirements, CoreRequirements: coreRequirements, diff --git a/earlydecoder/decoder_test.go b/earlydecoder/decoder_test.go index f60ff126..57b5a0ad 100644 --- a/earlydecoder/decoder_test.go +++ b/earlydecoder/decoder_test.go @@ -9,6 +9,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/hclsyntax" tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform-schema/backend" "github.com/hashicorp/terraform-schema/module" "github.com/zclconf/go-cty-debug/ctydebug" "github.com/zclconf/go-cty/cty" @@ -536,7 +537,7 @@ resource "google_something" "test" { }, } - executeTestCases(testCases, t, path) + runTestCases(testCases, t, path) } func TestLoadModule_Variables(t *testing.T) { @@ -787,11 +788,123 @@ output "name" { nil, }, } - executeTestCases(testCases, t, path) + runTestCases(testCases, t, path) } -func executeTestCases(testCases []testCase, t *testing.T, path string) { +func TestLoadModule_backend(t *testing.T) { + path := t.TempDir() + + testCases := []testCase{ + { + "no backend", + ` +terraform { + +}`, + &module.Meta{ + Path: path, + Backend: nil, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + }, + nil, + }, + { + "s3 backend", + ` +terraform { + backend "s3" { + blah = "test" + } +}`, + &module.Meta{ + Path: path, + Backend: &module.Backend{ + Type: "s3", + Data: &backend.UnknownBackendData{}, + }, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + }, + nil, + }, + { + "empty remote backend", + ` +terraform { + backend "remote" {} +}`, + &module.Meta{ + Path: path, + Backend: &module.Backend{ + Type: "remote", + Data: &backend.Remote{}, + }, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + }, + nil, + }, + { + "remote backend with hostname", + ` +terraform { + backend "remote" { + hostname = "app.terraform.io" + } +}`, + &module.Meta{ + Path: path, + Backend: &module.Backend{ + Type: "remote", + Data: &backend.Remote{Hostname: "app.terraform.io"}, + }, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + }, + nil, + }, + { + "remote backend with hostname and more attributes", + ` +terraform { + backend "remote" { + hostname = "app.terraform.io" + organization = "test" + + workspaces { + name = "test" + } + } +}`, + &module.Meta{ + Path: path, + Backend: &module.Backend{ + Type: "remote", + Data: &backend.Remote{Hostname: "app.terraform.io"}, + }, + ProviderReferences: map[module.ProviderRef]tfaddr.Provider{}, + ProviderRequirements: map[tfaddr.Provider]version.Constraints{}, + Variables: map[string]module.Variable{}, + Outputs: map[string]module.Output{}, + }, + nil, + }, + } + + runTestCases(testCases, t, path) +} + +func runTestCases(testCases []testCase, t *testing.T, path string) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d-%s", i, tc.name), func(t *testing.T) { f, diags := hclsyntax.ParseConfig([]byte(tc.cfg), "test.tf", hcl.InitialPos) diff --git a/earlydecoder/load_module.go b/earlydecoder/load_module.go index 4ada533d..01b94c69 100644 --- a/earlydecoder/load_module.go +++ b/earlydecoder/load_module.go @@ -6,6 +6,7 @@ import ( "github.com/hashicorp/hcl/v2" "github.com/hashicorp/hcl/v2/gohcl" "github.com/hashicorp/hcl/v2/hclsyntax" + "github.com/hashicorp/terraform-schema/backend" "github.com/hashicorp/terraform-schema/internal/typeexpr" "github.com/hashicorp/terraform-schema/module" "github.com/zclconf/go-cty/cty" @@ -15,6 +16,7 @@ import ( // decodedModule is the type representing a decoded Terraform module. type decodedModule struct { RequiredCore []string + Backends map[string]backend.BackendData ProviderRequirements map[string]*providerRequirement ProviderConfigs map[string]*providerConfig Resources map[string]*resource @@ -26,6 +28,7 @@ type decodedModule struct { func newDecodedModule() *decodedModule { return &decodedModule{ RequiredCore: make([]string, 0), + Backends: make(map[string]backend.BackendData), ProviderRequirements: make(map[string]*providerRequirement), ProviderConfigs: make(map[string]*providerConfig), Resources: make(map[string]*resource), @@ -68,6 +71,24 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { for _, innerBlock := range content.Blocks { switch innerBlock.Type { + case "backend": + bType := innerBlock.Labels[0] + + data, bDiags := decodeBackendsBlock(innerBlock) + diags = append(diags, bDiags...) + + if _, exists := mod.Backends[bType]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple backend definitions", + Detail: fmt.Sprintf("Found multiple backend definitions for %q. Only one is allowed.", bType), + Subject: &innerBlock.DefRange, + }) + continue + } + + mod.Backends[bType] = data + case "required_providers": reqs, reqsDiags := decodeRequiredProvidersBlock(innerBlock) diags = append(diags, reqsDiags...) diff --git a/earlydecoder/schema.go b/earlydecoder/schema.go index ad5734b8..2960222a 100644 --- a/earlydecoder/schema.go +++ b/earlydecoder/schema.go @@ -42,6 +42,10 @@ var terraformBlockSchema = &hcl.BodySchema{ { Type: "required_providers", }, + { + Type: "backend", + LabelNames: []string{"type"}, + }, }, } diff --git a/module/meta.go b/module/meta.go index e8421cf6..8762b7b9 100644 --- a/module/meta.go +++ b/module/meta.go @@ -3,11 +3,13 @@ package module import ( "github.com/hashicorp/go-version" tfaddr "github.com/hashicorp/terraform-registry-address" + "github.com/hashicorp/terraform-schema/backend" ) type Meta struct { Path string + Backend *Backend ProviderReferences map[ProviderRef]tfaddr.Provider ProviderRequirements map[tfaddr.Provider]version.Constraints CoreRequirements version.Constraints @@ -15,6 +17,11 @@ type Meta struct { Outputs map[string]Output } +type Backend struct { + Type string + Data backend.BackendData +} + type ProviderRef struct { LocalName string