diff --git a/CHANGELOG.md b/CHANGELOG.md index 201877d..bd2c6c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +# 0.1.5 + +- Remove dependency on terraform in go.mod + +# 0.1.4 + +- Fix empty YAML crash (#21) + +# 0.1.3 + +- Ignore empty documents + # 0.1.2 - Add heredoc syntax for multiline strings (#14) diff --git a/Makefile b/Makefile index 5cd3b96..ae0d045 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,6 @@ .PHONY: build docker docker-push release install test clean -VERSION := 0.1.4 +VERSION := 0.1.5 DOCKER_IMAGE_NAME := jrhouston/tfk8s build: @@ -32,7 +32,7 @@ install: go install -ldflags "-X main.toolVersion=${VERSION}" test: - go test -v + go test -v ./... clean: rm -rf release/* diff --git a/contrib/hashicorp/terraform/format.go b/contrib/hashicorp/terraform/format.go new file mode 100644 index 0000000..145e0cc --- /dev/null +++ b/contrib/hashicorp/terraform/format.go @@ -0,0 +1,175 @@ +// NOTE this file was lifted verbatim from internal/repl in the terraform project +// because the FormatValue function became internal in v1.0.0 + +package terraform + +import ( + "fmt" + "strconv" + "strings" + + "github.com/zclconf/go-cty/cty" +) + +// FormatValue formats a value in a way that resembles Terraform language syntax +// and uses the type conversion functions where necessary to indicate exactly +// what type it is given, so that equality test failures can be quickly +// understood. +func FormatValue(v cty.Value, indent int) string { + if !v.IsKnown() { + return "(known after apply)" + } + if v.IsMarked() { + return "(sensitive)" + } + if v.IsNull() { + ty := v.Type() + switch { + case ty == cty.DynamicPseudoType: + return "null" + case ty == cty.String: + return "tostring(null)" + case ty == cty.Number: + return "tonumber(null)" + case ty == cty.Bool: + return "tobool(null)" + case ty.IsListType(): + return fmt.Sprintf("tolist(null) /* of %s */", ty.ElementType().FriendlyName()) + case ty.IsSetType(): + return fmt.Sprintf("toset(null) /* of %s */", ty.ElementType().FriendlyName()) + case ty.IsMapType(): + return fmt.Sprintf("tomap(null) /* of %s */", ty.ElementType().FriendlyName()) + default: + return fmt.Sprintf("null /* %s */", ty.FriendlyName()) + } + } + + ty := v.Type() + switch { + case ty.IsPrimitiveType(): + switch ty { + case cty.String: + if formatted, isMultiline := formatMultilineString(v, indent); isMultiline { + return formatted + } + return strconv.Quote(v.AsString()) + case cty.Number: + bf := v.AsBigFloat() + return bf.Text('f', -1) + case cty.Bool: + if v.True() { + return "true" + } else { + return "false" + } + } + case ty.IsObjectType(): + return formatMappingValue(v, indent) + case ty.IsTupleType(): + return formatSequenceValue(v, indent) + case ty.IsListType(): + return fmt.Sprintf("tolist(%s)", formatSequenceValue(v, indent)) + case ty.IsSetType(): + return fmt.Sprintf("toset(%s)", formatSequenceValue(v, indent)) + case ty.IsMapType(): + return fmt.Sprintf("tomap(%s)", formatMappingValue(v, indent)) + } + + // Should never get here because there are no other types + return fmt.Sprintf("%#v", v) +} + +func formatMultilineString(v cty.Value, indent int) (string, bool) { + str := v.AsString() + lines := strings.Split(str, "\n") + if len(lines) < 2 { + return "", false + } + + // If the value is indented, we use the indented form of heredoc for readability. + operator := "<<" + if indent > 0 { + operator = "<<-" + } + + // Default delimiter is "End Of Text" by convention + delimiter := "EOT" + +OUTER: + for { + // Check if any of the lines are in conflict with the delimiter. The + // parser allows leading and trailing whitespace, so we must remove it + // before comparison. + for _, line := range lines { + // If the delimiter matches a line, extend it and start again + if strings.TrimSpace(line) == delimiter { + delimiter = delimiter + "_" + continue OUTER + } + } + + // None of the lines match the delimiter, so we're ready + break + } + + // Write the heredoc, with indentation as appropriate. + var buf strings.Builder + + buf.WriteString(operator) + buf.WriteString(delimiter) + for _, line := range lines { + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(line) + } + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(delimiter) + + return buf.String(), true +} + +func formatMappingValue(v cty.Value, indent int) string { + var buf strings.Builder + count := 0 + buf.WriteByte('{') + indent += 2 + for it := v.ElementIterator(); it.Next(); { + count++ + k, v := it.Element() + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(FormatValue(k, indent)) + buf.WriteString(" = ") + buf.WriteString(FormatValue(v, indent)) + } + indent -= 2 + if count > 0 { + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + } + buf.WriteByte('}') + return buf.String() +} + +func formatSequenceValue(v cty.Value, indent int) string { + var buf strings.Builder + count := 0 + buf.WriteByte('[') + indent += 2 + for it := v.ElementIterator(); it.Next(); { + count++ + _, v := it.Element() + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + buf.WriteString(FormatValue(v, indent)) + buf.WriteByte(',') + } + indent -= 2 + if count > 0 { + buf.WriteByte('\n') + buf.WriteString(strings.Repeat(" ", indent)) + } + buf.WriteByte(']') + return buf.String() +} diff --git a/contrib/hashicorp/terraform/format_test.go b/contrib/hashicorp/terraform/format_test.go new file mode 100644 index 0000000..5ffc340 --- /dev/null +++ b/contrib/hashicorp/terraform/format_test.go @@ -0,0 +1,189 @@ +// NOTE this file was lifted verbatim from internal/repl in the terraform project +// because the FormatValue function became internal in v1.0.0 + +package terraform + +import ( + "fmt" + "testing" + + "github.com/zclconf/go-cty/cty" +) + +func TestFormatValue(t *testing.T) { + tests := []struct { + Val cty.Value + Want string + }{ + { + cty.NullVal(cty.DynamicPseudoType), + `null`, + }, + { + cty.NullVal(cty.String), + `tostring(null)`, + }, + { + cty.NullVal(cty.Number), + `tonumber(null)`, + }, + { + cty.NullVal(cty.Bool), + `tobool(null)`, + }, + { + cty.NullVal(cty.List(cty.String)), + `tolist(null) /* of string */`, + }, + { + cty.NullVal(cty.Set(cty.Number)), + `toset(null) /* of number */`, + }, + { + cty.NullVal(cty.Map(cty.Bool)), + `tomap(null) /* of bool */`, + }, + { + cty.NullVal(cty.Object(map[string]cty.Type{"a": cty.Bool})), + `null /* object */`, // Ideally this would display the full object type, including its attributes + }, + { + cty.UnknownVal(cty.DynamicPseudoType), + `(known after apply)`, + }, + { + cty.StringVal(""), + `""`, + }, + { + cty.StringVal("hello"), + `"hello"`, + }, + { + cty.StringVal("hello\nworld"), + `<