mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00:00
feat: enable Terraform template-wide variables by default (#8334)
This commit is contained in:
@@ -3,15 +3,10 @@ package terraform
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/hcl/v2"
|
||||
"github.com/hashicorp/hcl/v2/gohcl"
|
||||
"github.com/hashicorp/hcl/v2/hclparse"
|
||||
"github.com/hashicorp/terraform-config-inspect/tfconfig"
|
||||
"github.com/mitchellh/go-wordwrap"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -20,25 +15,6 @@ import (
|
||||
"github.com/coder/coder/provisionersdk/proto"
|
||||
)
|
||||
|
||||
const featureUseManagedVariables = "feature_use_managed_variables"
|
||||
|
||||
var terraformWithFeaturesSchema = &hcl.BodySchema{
|
||||
Blocks: []hcl.BlockHeaderSchema{
|
||||
{
|
||||
Type: "provider",
|
||||
LabelNames: []string{"type"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var providerFeaturesConfigSchema = &hcl.BodySchema{
|
||||
Attributes: []hcl.AttributeSchema{
|
||||
{
|
||||
Name: featureUseManagedVariables,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// Parse extracts Terraform variables from source-code.
|
||||
func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisioner_ParseStream) error {
|
||||
_, span := s.startTrace(stream.Context(), tracing.FuncName())
|
||||
@@ -50,11 +26,6 @@ func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisione
|
||||
return xerrors.Errorf("load module: %s", formatDiagnostics(request.Directory, diags))
|
||||
}
|
||||
|
||||
flags, flagsDiags := loadEnabledFeatures(request.Directory)
|
||||
if flagsDiags.HasErrors() {
|
||||
return xerrors.Errorf("load coder provider features: %s", formatDiagnostics(request.Directory, diags))
|
||||
}
|
||||
|
||||
// Sort variables by (filename, line) to make the ordering consistent
|
||||
variables := make([]*tfconfig.Variable, 0, len(module.Variables))
|
||||
for _, v := range module.Variables {
|
||||
@@ -66,17 +37,12 @@ func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisione
|
||||
|
||||
var templateVariables []*proto.TemplateVariable
|
||||
|
||||
useManagedVariables := flags != nil && flags[featureUseManagedVariables]
|
||||
if useManagedVariables {
|
||||
for _, v := range variables {
|
||||
mv, err := convertTerraformVariableToManagedVariable(v)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't convert the Terraform variable to a managed one: %w", err)
|
||||
}
|
||||
templateVariables = append(templateVariables, mv)
|
||||
for _, v := range variables {
|
||||
mv, err := convertTerraformVariable(v)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("can't convert the Terraform variable to a managed one: %w", err)
|
||||
}
|
||||
} else if len(variables) > 0 {
|
||||
return xerrors.Errorf("legacy parameters are not supported anymore, use %q flag to enable managed Terraform variables", featureUseManagedVariables)
|
||||
templateVariables = append(templateVariables, mv)
|
||||
}
|
||||
return stream.Send(&proto.Parse_Response{
|
||||
Type: &proto.Parse_Response_Complete{
|
||||
@@ -87,81 +53,8 @@ func (s *server) Parse(request *proto.Parse_Request, stream proto.DRPCProvisione
|
||||
})
|
||||
}
|
||||
|
||||
func loadEnabledFeatures(moduleDir string) (map[string]bool, hcl.Diagnostics) {
|
||||
flags := map[string]bool{}
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
entries, err := os.ReadDir(moduleDir)
|
||||
if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: "Failed to read module directory",
|
||||
Detail: fmt.Sprintf("Module directory %s does not exist or cannot be read.", moduleDir),
|
||||
})
|
||||
return flags, diags
|
||||
}
|
||||
|
||||
var found bool
|
||||
for _, entry := range entries {
|
||||
if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") {
|
||||
continue
|
||||
}
|
||||
|
||||
flags, found, diags = parseFeatures(path.Join(moduleDir, entry.Name()))
|
||||
if found {
|
||||
break
|
||||
}
|
||||
}
|
||||
return flags, diags
|
||||
}
|
||||
|
||||
func parseFeatures(hclFilepath string) (map[string]bool, bool, hcl.Diagnostics) {
|
||||
flags := map[string]bool{}
|
||||
var diags hcl.Diagnostics
|
||||
|
||||
_, err := os.Stat(hclFilepath)
|
||||
if os.IsNotExist(err) {
|
||||
return flags, false, diags
|
||||
} else if err != nil {
|
||||
diags = append(diags, &hcl.Diagnostic{
|
||||
Severity: hcl.DiagError,
|
||||
Summary: fmt.Sprintf("Failed to open %q file", hclFilepath),
|
||||
})
|
||||
return flags, false, diags
|
||||
}
|
||||
|
||||
parser := hclparse.NewParser()
|
||||
var parsedHCL *hcl.File
|
||||
if strings.HasSuffix(hclFilepath, ".tf.json") {
|
||||
parsedHCL, diags = parser.ParseJSONFile(hclFilepath)
|
||||
} else {
|
||||
parsedHCL, diags = parser.ParseHCLFile(hclFilepath)
|
||||
}
|
||||
if diags.HasErrors() {
|
||||
return flags, false, diags
|
||||
}
|
||||
|
||||
var found bool
|
||||
content, _ := parsedHCL.Body.Content(terraformWithFeaturesSchema)
|
||||
for _, block := range content.Blocks {
|
||||
if block.Type == "provider" && block.Labels[0] == "coder" {
|
||||
content, _, partialDiags := block.Body.PartialContent(providerFeaturesConfigSchema)
|
||||
diags = append(diags, partialDiags...)
|
||||
if attr, defined := content.Attributes[featureUseManagedVariables]; defined {
|
||||
found = true
|
||||
|
||||
var useManagedVariables bool
|
||||
partialDiags := gohcl.DecodeExpression(attr.Expr, nil, &useManagedVariables)
|
||||
diags = append(diags, partialDiags...)
|
||||
flags[featureUseManagedVariables] = useManagedVariables
|
||||
}
|
||||
}
|
||||
}
|
||||
return flags, found, diags
|
||||
}
|
||||
|
||||
// Converts a Terraform variable to a managed variable.
|
||||
func convertTerraformVariableToManagedVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
|
||||
// Converts a Terraform variable to a template-wide variable, processed by Coder.
|
||||
func convertTerraformVariable(variable *tfconfig.Variable) (*proto.TemplateVariable, error) {
|
||||
var defaultData string
|
||||
if variable.Default != nil {
|
||||
var valid bool
|
||||
|
||||
@@ -31,9 +31,7 @@ func TestParse(t *testing.T) {
|
||||
Files: map[string]string{
|
||||
"main.tf": `variable "A" {
|
||||
description = "Testing!"
|
||||
}
|
||||
|
||||
provider "coder" { feature_use_managed_variables = "true" }`,
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
Type: &proto.Parse_Response_Complete{
|
||||
@@ -54,9 +52,7 @@ func TestParse(t *testing.T) {
|
||||
Files: map[string]string{
|
||||
"main.tf": `variable "A" {
|
||||
default = "wow"
|
||||
}
|
||||
|
||||
provider "coder" { feature_use_managed_variables = "true" }`,
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
Type: &proto.Parse_Response_Complete{
|
||||
@@ -78,9 +74,7 @@ func TestParse(t *testing.T) {
|
||||
validation {
|
||||
condition = var.A == "value"
|
||||
}
|
||||
}
|
||||
|
||||
provider "coder" { feature_use_managed_variables = "true" }`,
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
Type: &proto.Parse_Response_Complete{
|
||||
@@ -106,9 +100,7 @@ func TestParse(t *testing.T) {
|
||||
Name: "multiple-variables",
|
||||
Files: map[string]string{
|
||||
"main1.tf": `variable "foo" { }
|
||||
variable "bar" { }
|
||||
|
||||
provider "coder" { feature_use_managed_variables = "true" }`,
|
||||
variable "bar" { }`,
|
||||
"main2.tf": `variable "baz" { }
|
||||
variable "quux" { }`,
|
||||
},
|
||||
@@ -138,17 +130,13 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable-managed-variables-with-default-bool",
|
||||
Name: "template-variables-with-default-bool",
|
||||
Files: map[string]string{
|
||||
"main.tf": `variable "A" {
|
||||
description = "Testing!"
|
||||
type = bool
|
||||
default = true
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
@@ -169,17 +157,13 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable-managed-variables-with-default-string",
|
||||
Name: "template-variables-with-default-string",
|
||||
Files: map[string]string{
|
||||
"main.tf": `variable "A" {
|
||||
description = "Testing!"
|
||||
type = string
|
||||
default = "abc"
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
@@ -200,17 +184,13 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable-managed-variables-with-default-empty-string",
|
||||
Name: "template-variables-with-default-empty-string",
|
||||
Files: map[string]string{
|
||||
"main.tf": `variable "A" {
|
||||
description = "Testing!"
|
||||
type = string
|
||||
default = ""
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
@@ -231,16 +211,12 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "enable-managed-variables-without-default",
|
||||
Name: "template-variables-without-default",
|
||||
Files: map[string]string{
|
||||
"main2.tf": `variable "A" {
|
||||
description = "Testing!"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}`,
|
||||
},
|
||||
Response: &proto.Parse_Response{
|
||||
|
||||
Reference in New Issue
Block a user