Skip to content

Commit

Permalink
Add TFC usage detection
Browse files Browse the repository at this point in the history
This builds on the existing methods to detect remote backend usage and adds detection of cloud blocks in the terraform block.
  • Loading branch information
jpogran committed Mar 7, 2023
1 parent 36401eb commit 8bda612
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 1 deletion.
40 changes: 40 additions & 0 deletions backend/cloud.go
Original file line number Diff line number Diff line change
@@ -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
}
23 changes: 23 additions & 0 deletions earlydecoder/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,26 @@ 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()

// 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 {
val, vDiags := attr.Expr.Value(nil)
diags = append(diags, vDiags...)
if val.IsWhollyKnown() && val.Type() == cty.String {
return &backend.Cloud{
Hostname: val.AsString(),
}, nil
}
}

// since it defaults to app.terraform.io, it is safe to return that
// if hostname is empty
return &backend.Cloud{
Hostname: "app.terraform.io",
}, nil
}
8 changes: 8 additions & 0 deletions earlydecoder/decoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ 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{
Data: mod.CloudBackend,
}
}

var backend *module.Backend
if len(mod.Backends) == 1 {
for bType, data := range mod.Backends {
Expand Down Expand Up @@ -208,6 +215,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,
Expand Down
80 changes: 80 additions & 0 deletions earlydecoder/decoder_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -997,6 +997,86 @@ 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{
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{
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()

Expand Down
6 changes: 5 additions & 1 deletion earlydecoder/load_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -78,6 +79,10 @@ func loadModuleFromFile(file *hcl.File, mod *decodedModule) hcl.Diagnostics {

for _, innerBlock := range content.Blocks {
switch innerBlock.Type {
case "cloud":
data, bDiags := decodeCloudBlock(innerBlock)
diags = append(diags, bDiags...)
mod.CloudBackend = data
case "backend":
bType := innerBlock.Labels[0]

Expand All @@ -95,7 +100,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...)
Expand Down
3 changes: 3 additions & 0 deletions earlydecoder/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ var terraformBlockSchema = &hcl.BodySchema{
{
Type: "required_providers",
},
{
Type: "cloud",
},
{
Type: "backend",
LabelNames: []string{"type"},
Expand Down
17 changes: 17 additions & 0 deletions module/meta.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type Meta struct {
Filenames []string

Backend *Backend
CloudBackend *CloudBackend
ProviderReferences map[ProviderRef]tfaddr.Provider
ProviderRequirements ProviderRequirements
CoreRequirements version.Constraints
Expand Down Expand Up @@ -42,6 +43,22 @@ func (pr ProviderRequirements) Equals(reqs ProviderRequirements) bool {
return true
}

type CloudBackend struct {
Data backend.CloudData
}

func (be *CloudBackend) Equals(b *CloudBackend) bool {
if be == nil && b == nil {
return true
}

if be == nil || b == nil {
return false
}

return be.Data.Equals(b.Data)
}

type Backend struct {
Type string
Data backend.BackendData
Expand Down

0 comments on commit 8bda612

Please sign in to comment.