diff --git a/provisioner/terraform/parameters.go b/provisioner/terraform/parameters.go index 92892d7b1746c..4c92b25ddfbe5 100644 --- a/provisioner/terraform/parameters.go +++ b/provisioner/terraform/parameters.go @@ -27,29 +27,44 @@ func rawRichParameterNames(workdir string) ([]string, error) { var coderParameterNames []string for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".tf") { + if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") { continue } - hclFilepath := path.Join(workdir, entry.Name()) - parser := hclparse.NewParser() - parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + var ( + parsedTF *hcl.File + diags hcl.Diagnostics + tfFilepath = path.Join(workdir, entry.Name()) + parser = hclparse.NewParser() + ) + + // Support .tf.json files. + // Warning: since JSON parsing in Go automatically sorts maps + // alphabetically, we can't preserve the original order of parameters + // like in HCL. + if strings.HasSuffix(entry.Name(), ".tf.json") { + parsedTF, diags = parser.ParseJSONFile(tfFilepath) + } else { + parsedTF, diags = parser.ParseHCLFile(tfFilepath) + } + if diags.HasErrors() { return nil, hcl.Diagnostics{ { Severity: hcl.DiagError, Summary: "Failed to parse HCL file", - Detail: fmt.Sprintf("parser.ParseHCLFile can't parse %q file", hclFilepath), + Detail: fmt.Sprintf("parser.ParseHCLFile can't parse %q file", tfFilepath), }, } } - content, _, _ := parsedHCL.Body.PartialContent(terraformWithCoderParametersSchema) + content, _, _ := parsedTF.Body.PartialContent(terraformWithCoderParametersSchema) for _, block := range content.Blocks { if block.Type == "data" && block.Labels[0] == "coder_parameter" && len(block.Labels) == 2 { coderParameterNames = append(coderParameterNames, block.Labels[1]) } } } + return coderParameterNames, nil } diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go index 2801cf638c193..237954e4447db 100644 --- a/provisioner/terraform/parse.go +++ b/provisioner/terraform/parse.go @@ -103,7 +103,7 @@ func loadEnabledFeatures(moduleDir string) (map[string]bool, hcl.Diagnostics) { var found bool for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".tf") { + if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") { continue } @@ -131,7 +131,12 @@ func parseFeatures(hclFilepath string) (map[string]bool, bool, hcl.Diagnostics) } parser := hclparse.NewParser() - parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + 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 } diff --git a/provisioner/terraform/provision_test.go b/provisioner/terraform/provision_test.go index f410a476e37f2..b274b547406e3 100644 --- a/provisioner/terraform/provision_test.go +++ b/provisioner/terraform/provision_test.go @@ -254,6 +254,31 @@ func TestProvision(t *testing.T) { }, Apply: true, }, + { + Name: "single-resource-json", + Files: map[string]string{ + "main.tf.json": `{ + "resource": { + "null_resource": { + "A": [ + {} + ] + } + } + }`, + }, + Response: &proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Resources: []*proto.Resource{{ + Name: "A", + Type: "null_resource", + }}, + }, + }, + }, + Apply: true, + }, { Name: "bad-syntax-1", Files: map[string]string{ @@ -349,6 +374,88 @@ func TestProvision(t *testing.T) { }, }, }, + { + Name: "rich-parameter-with-value-json", + Files: map[string]string{ + "main.tf.json": `{ + "data": { + "coder_parameter": { + "example": [ + { + "default": "foobar", + "name": "Example", + "type": "string" + } + ], + "sample": [ + { + "default": "foobaz", + "name": "Sample", + "type": "string" + } + ] + } + }, + "resource": { + "null_resource": { + "example": [ + { + "triggers": { + "misc": "${data.coder_parameter.example.value}" + } + } + ] + } + }, + "terraform": [ + { + "required_providers": [ + { + "coder": { + "source": "coder/coder", + "version": "0.6.20" + } + } + ] + } + ] + }`, + }, + Request: &proto.Provision_Plan{ + RichParameterValues: []*proto.RichParameterValue{ + { + Name: "Example", + Value: "foobaz", + }, + { + Name: "Sample", + Value: "foofoo", + }, + }, + }, + Response: &proto.Provision_Response{ + Type: &proto.Provision_Response_Complete{ + Complete: &proto.Provision_Complete{ + Parameters: []*proto.RichParameter{ + { + Name: "Example", + Type: "string", + DefaultValue: "foobar", + }, + { + Name: "Sample", + Type: "string", + DefaultValue: "foobaz", + }, + }, + Resources: []*proto.Resource{{ + Name: "example", + Type: "null_resource", + }}, + }, + }, + }, + }, { Name: "git-auth", Files: map[string]string{ diff --git a/provisionersdk/archive.go b/provisionersdk/archive.go index ec496b6f31592..a0aa4d52b19dc 100644 --- a/provisionersdk/archive.go +++ b/provisionersdk/archive.go @@ -15,15 +15,17 @@ const ( TemplateArchiveLimit = 1 << 20 ) -func dirHasExt(dir string, ext string) (bool, error) { +func dirHasExt(dir string, exts ...string) (bool, error) { dirEnts, err := os.ReadDir(dir) if err != nil { return false, err } for _, fi := range dirEnts { - if strings.HasSuffix(fi.Name(), ext) { - return true, nil + for _, ext := range exts { + if strings.HasSuffix(fi.Name(), ext) { + return true, nil + } } } @@ -35,8 +37,8 @@ func Tar(w io.Writer, directory string, limit int64) error { tarWriter := tar.NewWriter(w) totalSize := int64(0) - const tfExt = ".tf" - hasTf, err := dirHasExt(directory, tfExt) + tfExts := []string{".tf", ".tf.json"} + hasTf, err := dirHasExt(directory, tfExts...) if err != nil { return err } @@ -50,7 +52,7 @@ func Tar(w io.Writer, directory string, limit int64) error { // useless. return xerrors.Errorf( "%s is not a valid template since it has no %s files", - absPath, tfExt, + absPath, tfExts, ) } diff --git a/provisionersdk/archive_test.go b/provisionersdk/archive_test.go index 66fae25dd9832..1bb5ea793fa9a 100644 --- a/provisionersdk/archive_test.go +++ b/provisionersdk/archive_test.go @@ -33,6 +33,15 @@ func TestTar(t *testing.T) { err = provisionersdk.Tar(io.Discard, dir, 1024) require.NoError(t, err) }) + t.Run("ValidJSON", func(t *testing.T) { + t.Parallel() + dir := t.TempDir() + file, err := os.CreateTemp(dir, "*.tf.json") + require.NoError(t, err) + _ = file.Close() + err = provisionersdk.Tar(io.Discard, dir, 1024) + require.NoError(t, err) + }) t.Run("HiddenFiles", func(t *testing.T) { t.Parallel() dir := t.TempDir()