From 54256f8f9781cace77f1a5a567932b7907e50218 Mon Sep 17 00:00:00 2001 From: Ryan Pham Date: Mon, 22 Apr 2024 14:52:25 +0900 Subject: [PATCH 1/3] [hcledit/read] fallback to raw values if unparsable --- hcledit_test.go | 20 ++++++++++++++++++++ internal/handler/read.go | 7 +++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/hcledit_test.go b/hcledit_test.go index 1c60431..90c9a78 100644 --- a/hcledit_test.go +++ b/hcledit_test.go @@ -330,6 +330,26 @@ attribute = [true, false, true] "attribute": []bool{true, false, true}, }, }, + + "fallback to absolute variable name": { + input: ` +attribute = local.var +`, + query: "attribute", + want: map[string]interface{}{ + "attribute": "local.var", + }, + }, + + "fallback to uninterpolated string": { + input: ` +attribute = "some-${local.var}" +`, + query: "attribute", + want: map[string]interface{}{ + "attribute": `"some-${local.var}"`, + }, + }, } for name, tc := range cases { diff --git a/internal/handler/read.go b/internal/handler/read.go index 530b3d0..ae1d697 100644 --- a/internal/handler/read.go +++ b/internal/handler/read.go @@ -48,9 +48,12 @@ func parse(buf []byte, name string) (cty.Value, error) { } body := file.Body.(*hclsyntax.Body) - v, diags := body.Attributes[name].Expr.Value(nil) + expr := body.Attributes[name].Expr + v, diags := expr.Value(nil) if diags.HasErrors() { - return cty.Value{}, diags + // Could not parse the value with a nil EvalContext, so this is likely an + // interpolated string. Instead, attempt to parse the raw string value. + return cty.StringVal(string(expr.Range().SliceBytes(buf))), nil } return v, nil } From 19d2193175339f85a5d4a937c86ce32f71e081f5 Mon Sep 17 00:00:00 2001 From: Ryan Pham Date: Mon, 22 Apr 2024 18:32:40 +0900 Subject: [PATCH 2/3] [hcledit/read] add option to give clients ability to enable fallback behavior --- hcledit.go | 2 +- hcledit_test.go | 25 +++++++++++++++++++++---- internal/handler/read.go | 18 ++++++++++++------ option.go | 13 ++++++++++--- 4 files changed, 44 insertions(+), 14 deletions(-) diff --git a/hcledit.go b/hcledit.go index fbaf7ea..bdeb3c1 100644 --- a/hcledit.go +++ b/hcledit.go @@ -89,7 +89,7 @@ func (h *HCLEditor) Read(queryStr string, opts ...Option) (map[string]interface{ } results := make(map[string]cty.Value) - hdlr, err := handler.NewReadHandler(results) + hdlr, err := handler.NewReadHandler(results, opt.readFallbackToRawString) if err != nil { return nil, err } diff --git a/hcledit_test.go b/hcledit_test.go index 90c9a78..679bc3a 100644 --- a/hcledit_test.go +++ b/hcledit_test.go @@ -167,15 +167,17 @@ object1 = { func TestRead(t *testing.T) { cases := map[string]struct { - input string - query string - want map[string]interface{} + input string + query string + options []hcledit.Option + want map[string]interface{} }{ "Attribute": { input: ` attribute = "R" `, query: "attribute", + options: make([]hcledit.Option, 0), want: map[string]interface{}{ "attribute": "R", }, @@ -188,6 +190,7 @@ block "label1" "label2" { attribute = "str" } `, + options: make([]hcledit.Option, 0), query: "block", want: map[string]interface{}{}, }, @@ -198,6 +201,7 @@ block "label1" "label2" { attribute = "R" } `, + options: make([]hcledit.Option, 0), query: "block.label1.label2.attribute", want: map[string]interface{}{ "block.label1.label2.attribute": "R", @@ -212,6 +216,7 @@ block1 "label1" "label2" { } } `, + options: make([]hcledit.Option, 0), query: "block1.label1.label2.block2.label3.label4.attribute", want: map[string]interface{}{ "block1.label1.label2.block2.label3.label4.attribute": "R", @@ -229,6 +234,7 @@ block "label" "label2" { } `, + options: make([]hcledit.Option, 0), query: "block.label.*.attribute", want: map[string]interface{}{ "block.label.label1.attribute": "R", @@ -242,6 +248,7 @@ object = { attribute = "R" } `, + options: make([]hcledit.Option, 0), query: "object.attribute", want: map[string]interface{}{ "object.attribute": "R", @@ -255,6 +262,7 @@ object1 = { } } `, + options: make([]hcledit.Option, 0), query: "object1.object2.attribute", want: map[string]interface{}{ "object1.object2.attribute": "R", @@ -265,6 +273,7 @@ object1 = { input: ` attribute = 1 `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": 1, @@ -275,6 +284,7 @@ attribute = 1 input: ` attribute = "str" `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": "str", @@ -285,6 +295,7 @@ attribute = "str" input: ` attribute = true `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": true, @@ -295,6 +306,7 @@ attribute = true input: ` attribute = false `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": false, @@ -305,6 +317,7 @@ attribute = false input: ` attribute = ["str1", "str2", "str3"] `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": []string{"str1", "str2", "str3"}, @@ -315,6 +328,7 @@ attribute = ["str1", "str2", "str3"] input: ` attribute = [1, 2, 3] `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": []int{1, 2, 3}, @@ -325,6 +339,7 @@ attribute = [1, 2, 3] input: ` attribute = [true, false, true] `, + options: make([]hcledit.Option, 0), query: "attribute", want: map[string]interface{}{ "attribute": []bool{true, false, true}, @@ -335,6 +350,7 @@ attribute = [true, false, true] input: ` attribute = local.var `, + options: []hcledit.Option{hcledit.WithReadFallbackToRawString()}, query: "attribute", want: map[string]interface{}{ "attribute": "local.var", @@ -345,6 +361,7 @@ attribute = local.var input: ` attribute = "some-${local.var}" `, + options: []hcledit.Option{hcledit.WithReadFallbackToRawString()}, query: "attribute", want: map[string]interface{}{ "attribute": `"some-${local.var}"`, @@ -360,7 +377,7 @@ attribute = "some-${local.var}" t.Fatal(err) } - got, err := editor.Read(tc.query) + got, err := editor.Read(tc.query, tc.options...) if err != nil { t.Fatal(err) } diff --git a/internal/handler/read.go b/internal/handler/read.go index ae1d697..df3bfc6 100644 --- a/internal/handler/read.go +++ b/internal/handler/read.go @@ -12,18 +12,20 @@ import ( ) type readHandler struct { - results map[string]cty.Value + results map[string]cty.Value + fallbackToRawString bool } -func NewReadHandler(results map[string]cty.Value) (Handler, error) { +func NewReadHandler(results map[string]cty.Value, fallbackToRawString bool) (Handler, error) { return &readHandler{ - results: results, + results: results, + fallbackToRawString: fallbackToRawString, }, nil } func (h *readHandler) HandleBody(body *hclwrite.Body, name string, keyTrail []string) error { buf := body.GetAttribute(name).BuildTokens(nil).Bytes() - value, err := parse(buf, name) + value, err := parse(buf, name, h.fallbackToRawString) if err != nil { return err } @@ -33,7 +35,7 @@ func (h *readHandler) HandleBody(body *hclwrite.Body, name string, keyTrail []st func (h *readHandler) HandleObject(object *ast.Object, name string, keyTrail []string) error { buf := object.GetObjectAttribute(name).BuildTokens().Bytes() - value, err := parse(buf, name) + value, err := parse(buf, name, h.fallbackToRawString) if err != nil { return err } @@ -41,7 +43,7 @@ func (h *readHandler) HandleObject(object *ast.Object, name string, keyTrail []s return nil } -func parse(buf []byte, name string) (cty.Value, error) { +func parse(buf []byte, name string, fallback bool) (cty.Value, error) { file, diags := hclsyntax.ParseConfig(buf, "", hcl.Pos{Line: 1, Column: 1}) if diags.HasErrors() { return cty.Value{}, diags @@ -51,6 +53,10 @@ func parse(buf []byte, name string) (cty.Value, error) { expr := body.Attributes[name].Expr v, diags := expr.Value(nil) if diags.HasErrors() { + if !fallback { + return cty.Value{}, diags + } + // Could not parse the value with a nil EvalContext, so this is likely an // interpolated string. Instead, attempt to parse the raw string value. return cty.StringVal(string(expr.Range().SliceBytes(buf))), nil diff --git a/option.go b/option.go index 52660a1..dba6b2d 100644 --- a/option.go +++ b/option.go @@ -1,9 +1,10 @@ package hcledit type option struct { - comment string - afterKey string - beforeNewline bool + comment string + afterKey string + beforeNewline bool + readFallbackToRawString bool } // Option configures specific behavior for specific HCLEditor operations. @@ -30,3 +31,9 @@ func WithNewLine() Option { opt.beforeNewline = true } } + +func WithReadFallbackToRawString() Option { + return func(opt *option) { + opt.readFallbackToRawString = true + } +} From 76a798e4521f6457e32a952e669ceba5973ee1e8 Mon Sep 17 00:00:00 2001 From: Ryan Pham Date: Tue, 23 Apr 2024 08:45:14 +0900 Subject: [PATCH 3/3] [hcledit/read] add flag to CLI command to control this behavior --- cmd/hcledit/internal/command/fixture/file.tf | 3 +++ cmd/hcledit/internal/command/read.go | 8 +++++++- cmd/hcledit/internal/command/read_test.go | 16 ++++++++++++++++ 3 files changed, 26 insertions(+), 1 deletion(-) diff --git a/cmd/hcledit/internal/command/fixture/file.tf b/cmd/hcledit/internal/command/fixture/file.tf index 2d98c5c..b1b39ec 100644 --- a/cmd/hcledit/internal/command/fixture/file.tf +++ b/cmd/hcledit/internal/command/fixture/file.tf @@ -15,6 +15,9 @@ module "my-module" { "f", ] + unevaluateable_reference = var.name + unevaluateable_interpolation = "this-${local.reference}" + map_variable = { bool_variable = true int_variable = 1 diff --git a/cmd/hcledit/internal/command/read.go b/cmd/hcledit/internal/command/read.go index 037ef3a..9d78000 100644 --- a/cmd/hcledit/internal/command/read.go +++ b/cmd/hcledit/internal/command/read.go @@ -15,6 +15,7 @@ import ( type ReadOptions struct { OutputFormat string + Fallback bool } func NewCmdRead() *cobra.Command { @@ -36,6 +37,7 @@ func NewCmdRead() *cobra.Command { } cmd.Flags().StringVarP(&opts.OutputFormat, "output-format", "o", "go-template='{{.Value}}'", "format to print the value as") + cmd.Flags().BoolVar(&opts.Fallback, "fallback", false, "falls back to reading the raw value if it cannot be evaluated") return cmd } @@ -48,7 +50,11 @@ func runRead(opts *ReadOptions, args []string) (string, error) { return "", fmt.Errorf("failed to read file: %s", err) } - results, err := editor.Read(query) + readOpts:= []hcledit.Option{} + if opts.Fallback { + readOpts = append(readOpts, hcledit.WithReadFallbackToRawString()) + } + results, err := editor.Read(query, readOpts...) if err != nil { return "", fmt.Errorf("failed to read file: %s", err) } diff --git a/cmd/hcledit/internal/command/read_test.go b/cmd/hcledit/internal/command/read_test.go index 66789ec..3a20720 100644 --- a/cmd/hcledit/internal/command/read_test.go +++ b/cmd/hcledit/internal/command/read_test.go @@ -95,6 +95,22 @@ func TestRunRead(t *testing.T) { want: "", opts: defaultOpts, }, + "unevaluateable reference fallback": { + query: "module.my-module.unevaluateable_reference", + want: "var.name", + opts: &ReadOptions{ + OutputFormat: "go-template='{{.Value}}'", + Fallback: true, + }, + }, + "unevaluateable interpolation fallback": { + query: "module.my-module.unevaluateable_interpolation", + want: `"this-${local.reference}"`, + opts: &ReadOptions{ + OutputFormat: "go-template='{{.Value}}'", + Fallback: true, + }, + }, } for name, tc := range cases {