mirror of https://github.com/coder/coder.git
feat: enable Terraform template-wide variables by default (#8334)
This commit is contained in:
parent
435c67ab75
commit
64687631aa
|
@ -248,12 +248,14 @@ data "coder_parameter" "cpu" {
|
|||
|
||||
As a template improvement, the template author can consider making some of the new `coder_parameter` resources `mutable`.
|
||||
|
||||
## Managed Terraform variables
|
||||
## Terraform template-wide variables
|
||||
|
||||
> ⚠️ Flag `feature_use_managed_variables` is available until v0.25.0 (Jul 2023) release. After this release, template-wide Terraform variables will be enabled by default.
|
||||
|
||||
As parameters are intended to be used only for workspace customization purposes, Terraform variables can be freely managed by the template author to build templates. Workspace users are not able to modify
|
||||
template variables.
|
||||
|
||||
The template author can enable managed Terraform variables mode by specifying the following flag:
|
||||
The template author can enable Terraform template-wide variables mode by specifying the following flag:
|
||||
|
||||
```hcl
|
||||
provider "coder" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "ecs-cluster" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "step1_do_project_id" {
|
||||
|
|
|
@ -38,7 +38,6 @@ variable "use_kubeconfig" {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = "true"
|
||||
}
|
||||
|
||||
variable "namespace" {
|
||||
|
|
|
@ -16,7 +16,6 @@ provider "fly" {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
resource "fly_app" "workspace" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "project_id" {
|
||||
|
|
|
@ -12,7 +12,6 @@ terraform {
|
|||
}
|
||||
|
||||
provider "coder" {
|
||||
feature_use_managed_variables = true
|
||||
}
|
||||
|
||||
variable "use_kubeconfig" {
|
||||
|
|
|
@ -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{
|
||||
|
|
Loading…
Reference in New Issue