From b3d9981eecfbdd85c52ee94863443c015ee2ea0b Mon Sep 17 00:00:00 2001 From: Jan De Dobbeleer Date: Wed, 9 Feb 2022 14:31:51 +0100 Subject: [PATCH] feat(terraform): add version information resolves #1455 --- docs/docs/segment-terraform.md | 8 +- src/go.mod | 10 ++- src/go.sum | 10 +++ src/segments/terraform.go | 74 ++++++++++++++++- src/segments/terraform_test.go | 142 +++++++++++++++++++++------------ src/test/terraform.tfstate | 6 ++ src/test/versions.tf | 3 + 7 files changed, 198 insertions(+), 55 deletions(-) create mode 100644 src/test/terraform.tfstate create mode 100644 src/test/versions.tf diff --git a/docs/docs/segment-terraform.md b/docs/docs/segment-terraform.md index 5b4d96d2..0db3e0d2 100644 --- a/docs/docs/segment-terraform.md +++ b/docs/docs/segment-terraform.md @@ -27,12 +27,17 @@ This requires a terraform binary in your PATH and will only show in directories } ``` +## Properties + +- fetch_version: `boolean` - fetch the version information from `versions.tf`, `main.tf` or `terraform.tfstate` - +defaults to `false` + ## Template ([info][templates]) :::note default template ``` template -{{ .WorkspaceName }} +{{ .WorkspaceName }}{{ if .Version }} {{ .Version }}{{ end }} ``` ::: @@ -40,5 +45,6 @@ This requires a terraform binary in your PATH and will only show in directories ### Properties - `.WorkspaceName`: `string` - is the current workspace name +- `.Version`: `string` - terraform version (set `fetch_version` to `true`) [templates]: /docs/config-templates diff --git a/src/go.mod b/src/go.mod index 95ca76e0..eaceb75c 100644 --- a/src/go.mod +++ b/src/go.mod @@ -33,6 +33,7 @@ require ( ) require ( + github.com/hashicorp/hcl/v2 v2.11.1 golang.org/x/mod v0.5.1 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b ) @@ -62,6 +63,13 @@ require ( gopkg.in/yaml.v2 v2.4.0 // indirect ) -require github.com/shopspring/decimal v1.3.1 // indirect +require ( + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/google/go-cmp v0.5.6 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/shopspring/decimal v1.3.1 // indirect + github.com/zclconf/go-cty v1.8.0 // indirect +) replace github.com/distatus/battery v0.10.0 => github.com/JanDeDobbeleer/battery v0.10.0-2 diff --git a/src/go.sum b/src/go.sum index ab681de7..c9235f1c 100644 --- a/src/go.sum +++ b/src/go.sum @@ -12,6 +12,7 @@ github.com/Masterminds/sprig/v3 v3.2.2 h1:17jRggJu518dr3QaafizSXOjKYp94wKfABxUmy github.com/Masterminds/sprig/v3 v3.2.2/go.mod h1:UoaO7Yp8KlPnJIYWTFkMaqPUYKTfGFPhxNuwnnxkKlk= github.com/StackExchange/wmi v1.2.1 h1:VIkavFPXSjcnS+O8yTq7NI32k0R5Aj+v39y29VYDOSA= github.com/StackExchange/wmi v1.2.1/go.mod h1:rcmrprowKIVzvc+NUiLncP2uuArMWLCbu9SBzvHz7e8= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38 h1:smF2tmSOzy2Mm+0dGI2AIUHY+w0BUc+4tn40djz7+6U= github.com/alecthomas/assert v0.0.0-20170929043011-405dbfeb8e38/go.mod h1:r7bzyVFMNntcxPZXK3/+KdruV1H5KSlyVY0gc+NgInI= @@ -20,7 +21,9 @@ github.com/alecthomas/colour v0.1.0/go.mod h1:QO9JBoKquHd+jz9nshCh40fOfO+JzsoXy8 github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48= github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8= github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM= +github.com/apparentlymart/go-textseg v1.0.0 h1:rRmlIsPEEhUTIKQb7T++Nz/A5Q6C9IuX2wFoYVvnCs0= github.com/apparentlymart/go-textseg v1.0.0/go.mod h1:z96Txxhf3xSFMPmb5X/1W05FF/Nj9VFpLOpjS5yuumk= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -32,6 +35,7 @@ github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzP github.com/go-ole/go-ole v1.2.5/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g= github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= @@ -56,8 +60,11 @@ github.com/gookit/goutil v0.4.0 h1:sj2pefgJEQdvHo6KoMMNjm2LwblcJ4HylFHa5eXnCS8= github.com/gookit/goutil v0.4.0/go.mod h1:qlGVh0PI+WnWSjYnIocfz/7tkeogxL6+EDNP1mRe+7o= github.com/gookit/ini/v2 v2.0.11 h1:Wl651xN2AaJbFrb8daBwWo8kS+sQHL3TddFi0/PRNXs= github.com/gookit/ini/v2 v2.0.11/go.mod h1:rIY8Uup5WDdPsrEE7VrF7fcMdGCCcPGA22Bk5R7roJQ= +github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/hcl/v2 v2.10.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/hashicorp/hcl/v2 v2.11.1 h1:yTyWcXcm9XB0TEkyU/JCRU6rYy4K+mgLtzn2wlrJbcc= +github.com/hashicorp/hcl/v2 v2.11.1/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= github.com/huandu/xstrings v1.3.1/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/huandu/xstrings v1.3.2 h1:L18LIDzqlW6xN2rEkpdV8+oL/IXWJ1APd+vsdYy4Wdw= github.com/huandu/xstrings v1.3.2/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= @@ -74,6 +81,7 @@ github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LEHPfb2qLo0BaaOLcbitczOKLWTsrBG9LczfCD4k= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= @@ -86,6 +94,7 @@ github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa1 github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= github.com/mitchellh/mapstructure v1.4.1/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.4.3 h1:OVowDSCllw/YjdLkam3/sm7wEtOy59d8ndGgCcyj8cs= @@ -137,6 +146,7 @@ github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778 h1:QldyIu/L63oPpyvQmHg github.com/xo/terminfo v0.0.0-20210125001918-ca9a967f8778/go.mod h1:2MuV+tbUrU1zIOPMxZ5EncGwgmMJsa+9ucAQZXxsObs= github.com/yosuke-furukawa/json5 v0.1.1/go.mod h1:sw49aWDqNdRJ6DYUtIQiaA3xyj2IL9tjeNYmX2ixwcU= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= +github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= diff --git a/src/segments/terraform.go b/src/segments/terraform.go index 37d1a4e1..366a94ca 100644 --- a/src/segments/terraform.go +++ b/src/segments/terraform.go @@ -1,8 +1,14 @@ package segments import ( + "encoding/json" + "errors" "oh-my-posh/environment" "oh-my-posh/properties" + "path/filepath" + + "github.com/hashicorp/hcl/v2/gohcl" + "github.com/hashicorp/hcl/v2/hclparse" ) type Terraform struct { @@ -10,10 +16,11 @@ type Terraform struct { env environment.Environment WorkspaceName string + TerraformBlock } func (tf *Terraform) Template() string { - return " {{ .WorkspaceName }} " + return " {{ .WorkspaceName }}{{ if .Version }} {{ .Version }}{{ end }} " } func (tf *Terraform) Init(props properties.Properties, env environment.Environment) { @@ -21,11 +28,74 @@ func (tf *Terraform) Init(props properties.Properties, env environment.Environme tf.env = env } +type TerraFormConfig struct { + Terraform *TerraformBlock `hcl:"terraform,block"` +} + +type TerraformBlock struct { + Version *string `hcl:"required_version" json:"terraform_version"` +} + func (tf *Terraform) Enabled() bool { cmd := "terraform" - if !tf.env.HasCommand(cmd) || !tf.env.HasFolder(tf.env.Pwd()+"/.terraform") { + terraformFolder := filepath.Join(tf.env.Pwd(), ".terraform") + fetchVersion := tf.props.GetBool(properties.FetchVersion, false) + if fetchVersion { + // known version files + files := []string{"versions.tf", "main.tf", "terraform.tfstate"} + var hasFiles bool + for _, file := range files { + if tf.env.HasFiles(file) { + hasFiles = true + break + } + } + fetchVersion = hasFiles + } + + inContext := tf.env.HasFolder(terraformFolder) || fetchVersion + if !tf.env.HasCommand(cmd) || !inContext { return false } tf.WorkspaceName, _ = tf.env.RunCommand(cmd, "workspace", "show") + if !fetchVersion { + return true + } + if err := tf.setVersionFromTfFiles(); err == nil { + return true + } + tf.setVersionFromTfStateFile() return true } + +func (tf *Terraform) setVersionFromTfFiles() error { + files := []string{"versions.tf", "main.tf"} + for _, file := range files { + if !tf.env.HasFiles(file) { + continue + } + parser := hclparse.NewParser() + content := tf.env.FileContent(file) + hclFile, diags := parser.ParseHCL([]byte(content), file) + if diags != nil { + continue + } + var config TerraFormConfig + diags = gohcl.DecodeBody(hclFile.Body, nil, &config) + if diags != nil { + continue + } + tf.TerraformBlock = *config.Terraform + return nil + } + return errors.New("no valid terraform files found") +} + +func (tf *Terraform) setVersionFromTfStateFile() { + file := "terraform.tfstate" + if !tf.env.HasFiles(file) { + return + } + content := tf.env.FileContent(file) + _ = json.Unmarshal([]byte(content), &tf.TerraformBlock) +} diff --git a/src/segments/terraform_test.go b/src/segments/terraform_test.go index 8094a7fe..0169f913 100644 --- a/src/segments/terraform_test.go +++ b/src/segments/terraform_test.go @@ -1,6 +1,7 @@ package segments import ( + "io/ioutil" "oh-my-posh/mock" "oh-my-posh/properties" "testing" @@ -8,59 +9,98 @@ import ( "github.com/stretchr/testify/assert" ) -type terraformArgs struct { - hasTfCommand bool - hasTfFolder bool - workspaceName string -} - -func bootStrapTerraformTest(args *terraformArgs) *Terraform { - env := new(mock.MockedEnvironment) - env.On("HasCommand", "terraform").Return(args.hasTfCommand) - env.On("HasFolder", "/.terraform").Return(args.hasTfFolder) - env.On("Pwd").Return("") - env.On("RunCommand", "terraform", []string{"workspace", "show"}).Return(args.workspaceName, nil) - k := &Terraform{ - env: env, - props: properties.Map{}, +func TestTerraform(t *testing.T) { + cases := []struct { + Case string + Template string + HasTfCommand bool + HasTfFolder bool + HasTfFiles bool + HasTfStateFile bool + FetchVersion bool + WorkspaceName string + ExpectedString string + ExpectedEnabled bool + }{ + { + Case: "default workspace", + ExpectedString: "default", + ExpectedEnabled: true, + WorkspaceName: "default", + HasTfFolder: true, + HasTfCommand: true, + }, + { + Case: "no command", + ExpectedString: "", + WorkspaceName: "default", + HasTfFolder: true, + }, + { + Case: "no directory, no files", + ExpectedString: "", + WorkspaceName: "default", + HasTfCommand: true, + }, + { + Case: "no files", + ExpectedString: "", + WorkspaceName: "default", + HasTfCommand: true, + FetchVersion: true, + }, + { + Case: "files", + ExpectedString: ">= 1.0.10", + ExpectedEnabled: true, + WorkspaceName: "default", + Template: "{{ .Version }}", + HasTfFiles: true, + HasTfCommand: true, + FetchVersion: true, + }, + { + Case: "files", + ExpectedString: "0.12.24", + ExpectedEnabled: true, + WorkspaceName: "default", + Template: "{{ .Version }}", + HasTfStateFile: true, + HasTfCommand: true, + FetchVersion: true, + }, } - return k -} -func TestTerraformWriterDisabled(t *testing.T) { - args := &terraformArgs{ - hasTfCommand: false, - hasTfFolder: false, - } - terraform := bootStrapTerraformTest(args) - assert.False(t, terraform.Enabled()) -} + for _, tc := range cases { + env := new(mock.MockedEnvironment) -func TestTerraformMissingDir(t *testing.T) { - args := &terraformArgs{ - hasTfCommand: true, - hasTfFolder: false, + env.On("HasCommand", "terraform").Return(tc.HasTfCommand) + env.On("HasFolder", ".terraform").Return(tc.HasTfFolder) + env.On("Pwd").Return("") + env.On("RunCommand", "terraform", []string{"workspace", "show"}).Return(tc.WorkspaceName, nil) + env.On("HasFiles", "versions.tf").Return(tc.HasTfFiles) + env.On("HasFiles", "main.tf").Return(tc.HasTfFiles) + env.On("HasFiles", "terraform.tfstate").Return(tc.HasTfStateFile) + if tc.HasTfFiles { + content, _ := ioutil.ReadFile("../test/versions.tf") + env.On("FileContent", "versions.tf").Return(string(content)) + } + if tc.HasTfStateFile { + content, _ := ioutil.ReadFile("../test/terraform.tfstate") + env.On("FileContent", "terraform.tfstate").Return(string(content)) + } + tf := &Terraform{ + env: env, + props: properties.Map{ + properties.FetchVersion: tc.FetchVersion, + }, + } + template := tc.Template + if len(template) == 0 { + template = tf.Template() + } + assert.Equal(t, tc.ExpectedEnabled, tf.Enabled(), tc.Case) + var got = renderTemplate(env, template, tf) + assert.Equal(t, tc.ExpectedString, got, tc.Case) } - terraform := bootStrapTerraformTest(args) - assert.False(t, terraform.Enabled()) -} - -func TestTerraformMissingBinary(t *testing.T) { - args := &terraformArgs{ - hasTfCommand: false, - hasTfFolder: true, - } - terraform := bootStrapTerraformTest(args) - assert.False(t, terraform.Enabled()) -} - -func TestTerraformEnabled(t *testing.T) { - expected := "default" - args := &terraformArgs{ - hasTfCommand: true, - hasTfFolder: true, - workspaceName: expected, - } - terraform := bootStrapTerraformTest(args) - assert.True(t, terraform.Enabled()) } diff --git a/src/test/terraform.tfstate b/src/test/terraform.tfstate new file mode 100644 index 00000000..901937ac --- /dev/null +++ b/src/test/terraform.tfstate @@ -0,0 +1,6 @@ +{ + "version": 4, + "terraform_version": "0.12.24", + "serial": 22, + "lineage": "6cc278b4-0426-a833-9920-356a6635038c" +} diff --git a/src/test/versions.tf b/src/test/versions.tf new file mode 100644 index 00000000..febe30c6 --- /dev/null +++ b/src/test/versions.tf @@ -0,0 +1,3 @@ +terraform { + required_version = ">= 1.0.10" +}