Skip to content

Commit 49962b1

Browse files
feat(segment): detect terraform/tofu version from tenv
resolves #7168
1 parent 29bd48b commit 49962b1

4 files changed

Lines changed: 178 additions & 8 deletions

File tree

src/segments/terraform.go

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"errors"
66
"path/filepath"
77
"slices"
8+
"strings"
89

910
"github.com/hashicorp/hcl/v2/gohcl"
1011
"github.com/hashicorp/hcl/v2/hclparse"
@@ -47,6 +48,13 @@ func (tf *Terraform) Enabled() bool {
4748
return true
4849
}
4950

51+
// tenv (https://github.com/tofuutils/tenv) pins the version through an
52+
// environment variable or a version file, which takes precedence over the
53+
// version declared in the terraform files or the state file.
54+
if tf.setVersionFromTenv() {
55+
return true
56+
}
57+
5058
if err := tf.setVersionFromTfFiles(); err == nil {
5159
return true
5260
}
@@ -55,6 +63,44 @@ func (tf *Terraform) Enabled() bool {
5563
return true
5664
}
5765

66+
// tenvSources returns the environment variable and version file tenv uses to
67+
// pin the version, based on whether the segment targets terraform or tofu.
68+
func (tf *Terraform) tenvSources() (envVar, versionFile string) {
69+
cmd := tf.options.String(Command, "terraform")
70+
if strings.Contains(cmd, "tofu") {
71+
return "TOFUENV_TOFU_VERSION", ".opentofu-version"
72+
}
73+
74+
return "TFENV_TERRAFORM_VERSION", ".terraform-version"
75+
}
76+
77+
func (tf *Terraform) setVersionFromTenv() bool {
78+
envVar, versionFile := tf.tenvSources()
79+
80+
// the environment variable takes precedence over the version file
81+
if version := strings.TrimSpace(tf.env.Getenv(envVar)); version != "" {
82+
tf.Version = &version
83+
return true
84+
}
85+
86+
if !tf.env.HasFiles(versionFile) {
87+
return false
88+
}
89+
90+
version := strings.TrimSpace(tf.env.FileContent(versionFile))
91+
// a version file holds a single version reference, guard against trailing content
92+
if line, _, found := strings.Cut(version, "\n"); found {
93+
version = strings.TrimSpace(line)
94+
}
95+
96+
if version == "" {
97+
return false
98+
}
99+
100+
tf.Version = &version
101+
return true
102+
}
103+
58104
func (tf *Terraform) inContext(fetchVersion bool) bool {
59105
terraformFolder := filepath.Join(tf.env.Pwd(), ".terraform")
60106

@@ -71,7 +117,8 @@ func (tf *Terraform) inContext(fetchVersion bool) bool {
71117
return false
72118
}
73119

74-
versionFiles := []string{"versions.tf", "main.tf", "terraform.tfstate"}
120+
_, tenvVersionFile := tf.tenvSources()
121+
versionFiles := []string{"versions.tf", "main.tf", "terraform.tfstate", tenvVersionFile}
75122
return slices.ContainsFunc(versionFiles, tf.env.HasFiles)
76123
}
77124

src/segments/terraform_test.go

Lines changed: 106 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@ func TestTerraform(t *testing.T) {
1414
cases := []struct {
1515
Case string
1616
Template string
17+
Command string
1718
WorkspaceName string
19+
TenvEnv string
20+
TenvFile string
21+
TenvFileContent string
1822
ExpectedString string
1923
HasTfCommand bool
2024
HasTfFolder bool
@@ -79,21 +83,120 @@ func TestTerraform(t *testing.T) {
7983
HasTfFiles: true,
8084
HasTfCommand: true,
8185
},
86+
{
87+
Case: "terraform version from .terraform-version file",
88+
ExpectedString: "1.7.0",
89+
ExpectedEnabled: true,
90+
WorkspaceName: "default",
91+
Template: "{{ .Version }}",
92+
HasTfFolder: true,
93+
HasTfCommand: true,
94+
FetchVersion: true,
95+
TenvFile: ".terraform-version",
96+
TenvFileContent: "1.7.0\n",
97+
},
98+
{
99+
Case: "terraform version from TFENV_TERRAFORM_VERSION env",
100+
ExpectedString: "1.8.0",
101+
ExpectedEnabled: true,
102+
WorkspaceName: "default",
103+
Template: "{{ .Version }}",
104+
HasTfFolder: true,
105+
HasTfCommand: true,
106+
FetchVersion: true,
107+
TenvEnv: "1.8.0",
108+
},
109+
{
110+
Case: "env takes precedence over .terraform-version file",
111+
ExpectedString: "1.8.0",
112+
ExpectedEnabled: true,
113+
WorkspaceName: "default",
114+
Template: "{{ .Version }}",
115+
HasTfFolder: true,
116+
HasTfCommand: true,
117+
FetchVersion: true,
118+
TenvEnv: "1.8.0",
119+
TenvFile: ".terraform-version",
120+
TenvFileContent: "1.7.0",
121+
},
122+
{
123+
Case: "tenv file takes precedence over terraform files",
124+
ExpectedString: "1.7.0",
125+
ExpectedEnabled: true,
126+
WorkspaceName: "default",
127+
Template: "{{ .Version }}",
128+
HasTfVersionFiles: true,
129+
HasTfCommand: true,
130+
FetchVersion: true,
131+
TenvFile: ".terraform-version",
132+
TenvFileContent: "1.7.0",
133+
},
134+
{
135+
Case: "enabled by .terraform-version alone",
136+
ExpectedString: "1.7.0",
137+
ExpectedEnabled: true,
138+
WorkspaceName: "default",
139+
Template: "{{ .Version }}",
140+
HasTfCommand: true,
141+
FetchVersion: true,
142+
TenvFile: ".terraform-version",
143+
TenvFileContent: "1.7.0",
144+
},
145+
{
146+
Case: "tofu version from .opentofu-version file",
147+
ExpectedString: "1.8.5",
148+
ExpectedEnabled: true,
149+
WorkspaceName: "default",
150+
Template: "{{ .Version }}",
151+
Command: "tofu",
152+
HasTfFolder: true,
153+
HasTfCommand: true,
154+
FetchVersion: true,
155+
TenvFile: ".opentofu-version",
156+
TenvFileContent: "1.8.5",
157+
},
158+
{
159+
Case: "tofu version from TOFUENV_TOFU_VERSION env",
160+
ExpectedString: "1.9.0",
161+
ExpectedEnabled: true,
162+
WorkspaceName: "default",
163+
Template: "{{ .Version }}",
164+
Command: "tofu",
165+
HasTfFolder: true,
166+
HasTfCommand: true,
167+
FetchVersion: true,
168+
TenvEnv: "1.9.0",
169+
},
82170
}
83171

84172
for _, tc := range cases {
85173
env := new(mock.Environment)
86174

87-
env.On("HasCommand", "terraform").Return(tc.HasTfCommand)
175+
cmd := tc.Command
176+
if cmd == "" {
177+
cmd = "terraform"
178+
}
179+
180+
tenvEnvVar := "TFENV_TERRAFORM_VERSION"
181+
tenvFile := ".terraform-version"
182+
if cmd == "tofu" {
183+
tenvEnvVar = "TOFUENV_TOFU_VERSION"
184+
tenvFile = ".opentofu-version"
185+
}
186+
187+
env.On("HasCommand", cmd).Return(tc.HasTfCommand)
88188
env.On("HasFolder", ".terraform").Return(tc.HasTfFolder)
89189
env.On("HasFiles", ".tf").Return(tc.HasTfFiles)
90190
env.On("HasFiles", ".tfplan").Return(tc.HasTfFiles)
91191
env.On("HasFiles", ".tfstate").Return(tc.HasTfFiles)
92192
env.On("Pwd").Return("")
93-
env.On("RunCommand", "terraform", []string{"workspace", "show"}).Return(tc.WorkspaceName, nil)
193+
env.On("RunCommand", cmd, []string{"workspace", "show"}).Return(tc.WorkspaceName, nil)
94194
env.On("HasFiles", "versions.tf").Return(tc.HasTfVersionFiles)
95195
env.On("HasFiles", "main.tf").Return(tc.HasTfVersionFiles)
96196
env.On("HasFiles", "terraform.tfstate").Return(tc.HasTfStateFile)
197+
env.On("Getenv", tenvEnvVar).Return(tc.TenvEnv)
198+
env.On("HasFiles", tenvFile).Return(tc.TenvFile == tenvFile)
199+
env.On("FileContent", tenvFile).Return(tc.TenvFileContent)
97200
if tc.HasTfVersionFiles {
98201
content, _ := os.ReadFile("../test/versions.tf")
99202
env.On("FileContent", "versions.tf").Return(string(content))
@@ -105,6 +208,7 @@ func TestTerraform(t *testing.T) {
105208

106209
props := options.Map{
107210
options.FetchVersion: tc.FetchVersion,
211+
Command: cmd,
108212
}
109213

110214
tf := &Terraform{}

themes/schema.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4725,7 +4725,7 @@
47254725
"fetch_version": {
47264726
"type": "boolean",
47274727
"title": "Fetch Version",
4728-
"description": "Fetch the version number",
4728+
"description": "Fetch the version number from tenv (TFENV_TERRAFORM_VERSION / TOFUENV_TOFU_VERSION or .terraform-version / .opentofu-version), versions.tf, main.tf or terraform.tfstate",
47294729
"default": false
47304730
}
47314731
},

website/docs/segments/cli/terraform.mdx

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,27 @@ import Config from "@site/src/components/Config.js";
2525

2626
## Options
2727

28-
| Name | Type | Default | Description |
29-
| --------------- | :-------: | :---------: | ---------------------------------------------------------------------------------- |
30-
| `fetch_version` | `boolean` | `false` | fetch the version information from `versions.tf`, `main.tf` or `terraform.tfstate` |
31-
| `command` | `string` | `terraform` | the command(s) to run, allows support for `tofu` |
28+
| Name | Type | Default | Description |
29+
| --------------- | :-------: | :---------: | ------------------------------------------------------------------------------------------------------------------ |
30+
| `fetch_version` | `boolean` | `false` | fetch the version information, see [Version detection][version-detection] for the sources and their priority |
31+
| `command` | `string` | `terraform` | the command(s) to run, allows support for `tofu` |
32+
33+
### Version detection
34+
35+
When `fetch_version` is enabled, the version is resolved from the first source
36+
that yields a value, in this order:
37+
38+
1. the [tenv][tenv] environment variable — `TFENV_TERRAFORM_VERSION` (or
39+
`TOFUENV_TOFU_VERSION` when `command` is `tofu`)
40+
2. the [tenv][tenv] version file — `.terraform-version` (or `.opentofu-version`
41+
when `command` is `tofu`)
42+
3. the `required_version` declared in `versions.tf` or `main.tf`
43+
4. the `terraform_version` stored in `terraform.tfstate`
44+
45+
The tenv sources let the segment show the pinned version even when you don't use
46+
`main.tf` / `versions.tf`. The environment variable and version file are only
47+
used to read the version; a project file (a `.terraform` folder or one of the
48+
files above) is still required for the segment to activate.
3249

3350
## Template ([info][templates])
3451

@@ -49,3 +66,5 @@ import Config from "@site/src/components/Config.js";
4966

5067
[templates]: /docs/configuration/templates
5168
[terraform]: https://developer.hashicorp.com/terraform
69+
[tenv]: https://github.com/tofuutils/tenv
70+
[version-detection]: #version-detection

0 commit comments

Comments
 (0)