From 90d7e1b354c44fea34766b544df6230a928dfdc5 Mon Sep 17 00:00:00 2001 From: Technofab Date: Sun, 4 Jun 2023 13:56:55 +0200 Subject: [PATCH 1/3] feat(provisionersdk,provisioner): add support for .tf.json templates --- provisioner/terraform/parameters.go | 8 ++++++-- provisioner/terraform/parse.go | 8 ++++++-- provisionersdk/archive.go | 14 ++++++++------ provisionersdk/archive_test.go | 9 +++++++++ 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/provisioner/terraform/parameters.go b/provisioner/terraform/parameters.go index 92892d7b1746c..2957769fef355 100644 --- a/provisioner/terraform/parameters.go +++ b/provisioner/terraform/parameters.go @@ -27,13 +27,17 @@ 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) + if strings.HasSuffix(entry.Name(), ".tf.json") { + parsedHCL, diags := parser.ParseJSONFile(hclFilepath) + } else { + parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + } if diags.HasErrors() { return nil, hcl.Diagnostics{ { diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go index 2801cf638c193..805fd364bda08 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,11 @@ func parseFeatures(hclFilepath string) (map[string]bool, bool, hcl.Diagnostics) } parser := hclparse.NewParser() - parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + 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/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() From 9d2b35ada21efd719d81fc301e8778dca1aea325 Mon Sep 17 00:00:00 2001 From: Technofab Date: Sun, 4 Jun 2023 17:30:36 +0200 Subject: [PATCH 2/3] fix(provisioner): parser variable scope --- provisioner/terraform/parameters.go | 6 ++++-- provisioner/terraform/parse.go | 5 +++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/provisioner/terraform/parameters.go b/provisioner/terraform/parameters.go index 2957769fef355..0ecb430932129 100644 --- a/provisioner/terraform/parameters.go +++ b/provisioner/terraform/parameters.go @@ -31,12 +31,14 @@ func rawRichParameterNames(workdir string) ([]string, error) { continue } + var parsedHCL *hcl.File + var diags hcl.Diagnostics hclFilepath := path.Join(workdir, entry.Name()) parser := hclparse.NewParser() if strings.HasSuffix(entry.Name(), ".tf.json") { - parsedHCL, diags := parser.ParseJSONFile(hclFilepath) + parsedHCL, diags = parser.ParseJSONFile(hclFilepath) } else { - parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + parsedHCL, diags = parser.ParseHCLFile(hclFilepath) } if diags.HasErrors() { return nil, hcl.Diagnostics{ diff --git a/provisioner/terraform/parse.go b/provisioner/terraform/parse.go index 805fd364bda08..edf00a4d70ff7 100644 --- a/provisioner/terraform/parse.go +++ b/provisioner/terraform/parse.go @@ -131,10 +131,11 @@ func parseFeatures(hclFilepath string) (map[string]bool, bool, hcl.Diagnostics) } parser := hclparse.NewParser() + var parsedHCL *hcl.File if strings.HasSuffix(hclFilepath, ".tf.json") { - parsedHCL, diags := parser.ParseJSONFile(hclFilepath) + parsedHCL, diags = parser.ParseJSONFile(hclFilepath) } else { - parsedHCL, diags := parser.ParseHCLFile(hclFilepath) + parsedHCL, diags = parser.ParseHCLFile(hclFilepath) } if diags.HasErrors() { return flags, false, diags From 348f785300e3b8bbc0f2656a07c33e4cafeae295 Mon Sep 17 00:00:00 2001 From: Colin Adler Date: Wed, 7 Jun 2023 20:02:36 +0000 Subject: [PATCH 3/3] add tests and comments --- provisioner/terraform/parameters.go | 27 ++++-- provisioner/terraform/parse.go | 2 +- provisioner/terraform/provision_test.go | 107 ++++++++++++++++++++++++ 3 files changed, 126 insertions(+), 10 deletions(-) diff --git a/provisioner/terraform/parameters.go b/provisioner/terraform/parameters.go index 0ecb430932129..4c92b25ddfbe5 100644 --- a/provisioner/terraform/parameters.go +++ b/provisioner/terraform/parameters.go @@ -27,35 +27,44 @@ func rawRichParameterNames(workdir string) ([]string, error) { var coderParameterNames []string for _, entry := range entries { - if !strings.HasSuffix(entry.Name(), ".tf") || !strings.HasSuffix(entry.Name(), ".tf.json") { + if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") { continue } - var parsedHCL *hcl.File - var diags hcl.Diagnostics - hclFilepath := path.Join(workdir, entry.Name()) - parser := hclparse.NewParser() + 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") { - parsedHCL, diags = parser.ParseJSONFile(hclFilepath) + parsedTF, diags = parser.ParseJSONFile(tfFilepath) } else { - parsedHCL, diags = parser.ParseHCLFile(hclFilepath) + 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 edf00a4d70ff7..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") || !strings.HasSuffix(entry.Name(), ".tf.json") { + if !strings.HasSuffix(entry.Name(), ".tf") && !strings.HasSuffix(entry.Name(), ".tf.json") { continue } 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{