diff --git a/backend/cloud.go b/backend/cloud.go new file mode 100644 index 00000000..926fbb29 --- /dev/null +++ b/backend/cloud.go @@ -0,0 +1,40 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package backend + +type CloudData interface { + Copy() CloudData + Equals(CloudData) bool +} + +type UnknownCloudData struct{} + +func (*UnknownCloudData) Copy() CloudData { + return &UnknownCloudData{} +} + +func (*UnknownCloudData) Equals(d CloudData) bool { + _, ok := d.(*UnknownCloudData) + return ok +} + +type Cloud struct { + Organization string + Hostname string +} + +func (r *Cloud) Copy() CloudData { + return &Cloud{ + Organization: r.Organization, + } +} + +func (r *Cloud) Equals(d CloudData) bool { + data, ok := d.(*Cloud) + if !ok { + return false + } + + return data.Organization == r.Organization +} diff --git a/earlydecoder/backend.go b/earlydecoder/backend.go index 960c68e5..1b558bc3 100644 --- a/earlydecoder/backend.go +++ b/earlydecoder/backend.go @@ -30,3 +30,24 @@ func decodeBackendsBlock(block *hcl.Block) (backend.BackendData, hcl.Diagnostics return &backend.UnknownBackendData{}, diags } + +func decodeCloudBlock(block *hcl.Block) (backend.CloudData, hcl.Diagnostics) { + attrs, diags := block.Body.JustAttributes() + + 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.Cloud{ + Hostname: val.AsString(), + }, nil + } + } + + // https://developer.hashicorp.com/terraform/language/settings/terraform-cloud#usage-example + // Required for Terraform Enterprise; + // Defaults to app.terraform.io for Terraform Cloud + return &backend.Cloud{ + Hostname: "app.terraform.io", + }, nil +} diff --git a/earlydecoder/decoder.go b/earlydecoder/decoder.go index a9191b66..5f2fb83f 100644 --- a/earlydecoder/decoder.go +++ b/earlydecoder/decoder.go @@ -41,6 +41,14 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag coreRequirements = append(coreRequirements, c...) } + var tfCloud *module.CloudBackend + if mod.CloudBackend != nil { + tfCloud = &module.CloudBackend{ + Type: "cloud", + Data: mod.CloudBackend, + } + } + var backend *module.Backend if len(mod.Backends) == 1 { for bType, data := range mod.Backends { @@ -208,6 +216,7 @@ func LoadModule(path string, files map[string]*hcl.File) (*module.Meta, hcl.Diag return &module.Meta{ Path: path, Backend: backend, + CloudBackend: tfCloud, ProviderReferences: refs, ProviderRequirements: providerRequirements, CoreRequirements: coreRequirements, diff --git a/earlydecoder/decoder_test.go b/earlydecoder/decoder_test.go index 258f27e0..67f9aad2 100644 --- a/earlydecoder/decoder_test.go +++ b/earlydecoder/decoder_test.go @@ -997,6 +997,88 @@ terraform { runTestCases(testCases, t, path) } +func TestLoadModule_cloud(t *testing.T) { + path := t.TempDir() + + testCases := []testCase{ + { + "cloud backend", + ` +terraform { + cloud { + hostname = "app.terraform.io" + organization = "example_corp" + + workspaces { + tags = ["app"] + } + } +}`, + &module.Meta{ + Path: path, + // Backend: &module.Backend{ + // Type: "cloud", + // Data: &backend.Remote{ + // Hostname: "app.terraform.io", + // }, + // }, + Backend: nil, + CloudBackend: &module.CloudBackend{ + Type: "cloud", + Data: &backend.Cloud{ + 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{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.DeclaredModuleCall{}, + }, + nil, + }, + { + "cloud backend empy hostname", + ` +terraform { + cloud { + organization = "example_corp" + + workspaces { + tags = ["app"] + } + } +}`, + &module.Meta{ + Path: path, + // Backend: &module.Backend{ + // Type: "cloud", + // Data: &backend.Remote{ + // Hostname: "app.terraform.io", + // }, + // }, + Backend: nil, + CloudBackend: &module.CloudBackend{ + Type: "cloud", + Data: &backend.Cloud{ + 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{}, + Filenames: []string{"test.tf"}, + ModuleCalls: map[string]module.DeclaredModuleCall{}, + }, + nil, + }, + } + + runTestCases(testCases, t, path) +} + func TestLoadModule_Modules(t *testing.T) { path := t.TempDir() diff --git a/earlydecoder/load_module.go b/earlydecoder/load_module.go index 13829e73..98ed567c 100644 --- a/earlydecoder/load_module.go +++ b/earlydecoder/load_module.go @@ -22,6 +22,7 @@ import ( type decodedModule struct { RequiredCore []string Backends map[string]backend.BackendData + CloudBackend backend.CloudData ProviderRequirements map[string]*providerRequirement ProviderConfigs map[string]*providerConfig Resources map[string]*resource @@ -78,6 +79,23 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { for _, innerBlock := range content.Blocks { switch innerBlock.Type { + case "cloud": + bType := innerBlock.Type + + data, bDiags := decodeCloudBlock(innerBlock) + diags = append(diags, bDiags...) + + if _, exists := mod.Backends[bType]; exists { + diags = append(diags, &hcl.Diagnostic{ + Severity: hcl.DiagError, + Summary: "Multiple cloud definitions", + Detail: fmt.Sprintf("Found multiple cloud definitions for %q. Only one is allowed.", bType), + Subject: &innerBlock.DefRange, + }) + continue + } + + mod.CloudBackend = data case "backend": bType := innerBlock.Labels[0] @@ -95,7 +113,6 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics { } 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 35cd6d90..4865c9a5 100644 --- a/earlydecoder/schema.go +++ b/earlydecoder/schema.go @@ -49,6 +49,9 @@ var terraformBlockSchema = &hcl.BodySchema{ { Type: "required_providers", }, + { + Type: "cloud", + }, { Type: "backend", LabelNames: []string{"type"}, diff --git a/module/meta.go b/module/meta.go index 24e826f9..3a6fbdf6 100644 --- a/module/meta.go +++ b/module/meta.go @@ -14,6 +14,7 @@ type Meta struct { Filenames []string Backend *Backend + CloudBackend *CloudBackend ProviderReferences map[ProviderRef]tfaddr.Provider ProviderRequirements ProviderRequirements CoreRequirements version.Constraints @@ -42,6 +43,27 @@ func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool { return true } +type CloudBackend struct { + Type string + Data backend.CloudData +} + +func (be *CloudBackend) Equals(b *CloudBackend) bool { + if be == nil && b == nil { + return true + } + + if be == nil || b == nil { + return false + } + + if be.Type != b.Type { + return false + } + + return be.Data.Equals(b.Data) +} + type Backend struct { Type string Data backend.BackendData