Skip to content

Commit

Permalink
Add .tofu files support (#194)
Browse files Browse the repository at this point in the history
* add .tofu files support
* update README with .tofu files support

Signed-off-by: Denis Vaumoron <[email protected]>
  • Loading branch information
dvaumoron authored Jun 30, 2024
1 parent 87241b9 commit 57d31d0
Show file tree
Hide file tree
Showing 11 changed files with 132 additions and 106 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1240,7 +1240,7 @@ Recognize same values as `tenv atmos use` command.
<a id="required_version"></a>
<details><summary><b>required_version</b></summary><br>

the `latest-allowed` or `min-required` strategies scan through your IAC files (.tf or .tf.json) and identify a version conforming to the constraint in the relevant files. They fallback to `latest` when no IAC files and no default constraint are found, and can optionally be used with a default constraint as detailed in <a href="#project-binaries">project binaries</a>.
the `latest-allowed` or `min-required` strategies scan through your IAC files (see list in [project binaries](#project-binaries)) and identify a version conforming to the constraint in the relevant files. They fallback to `latest` when no IAC files and no default constraint are found, and can optionally be used with a default constraint as detailed in [project binaries](#project-binaries).

Currently the format for [Terraform required_version](https://developer.hashicorp.com/terraform/language/settings#specifying-a-required-terraform-version) and [OpenTofu required_version](https://opentofu.org/docs/language/settings#specifying-a-required-opentofu-version) are very similar, however this may change over time, always refer to docs for the latest format specification.

Expand Down Expand Up @@ -1274,7 +1274,7 @@ The version resolution order is :
- `${TENV_ROOT}/OpenTofu/version` file (can be written with `tenv tofu use`)
- `latest-allowed`

The `latest-allowed` strategy rely on [required_version](#required_version) from .tf or .tf.json files with a fallback to `latest` when no constraint are found. Moreover it is possible to add a default constraint with TOFUENV_TOFU_DEFAULT_CONSTRAINT environment variable or `${TENV_ROOT}/OpenTofu/constraint` file (can be written with `tenv tofu constraint`). The default constraint is added while using `latest-allowed`, `min-required` or custom constraint. A default constraint with `latest-allowed` or `min-required` will avoid the fallback to `latest` when there is no .tf or .tf.json files.
The `latest-allowed` strategy rely on [required_version](#required_version) from .tofu, .tofu.json, .tf or .tf.json files with a fallback to `latest` when no constraint are found. Moreover it is possible to add a default constraint with TOFUENV_TOFU_DEFAULT_CONSTRAINT environment variable or `${TENV_ROOT}/OpenTofu/constraint` file (can be written with `tenv tofu constraint`). The default constraint is added while using `latest-allowed`, `min-required` or custom constraint. A default constraint with `latest-allowed` or `min-required` will avoid the fallback to `latest` when there is no .tf or .tf.json files.

</details>

Expand Down
24 changes: 12 additions & 12 deletions cmd/tenv/tenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"path/filepath"
"strings"

"github.com/hashicorp/hcl/v2/hclparse"
"github.com/spf13/cobra"

"github.com/tofuutils/tenv/v2/config"
Expand All @@ -31,7 +32,6 @@ import (
"github.com/tofuutils/tenv/v2/versionmanager"
"github.com/tofuutils/tenv/v2/versionmanager/builder"
"github.com/tofuutils/tenv/v2/versionmanager/proxy"
terragruntparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/terragrunt"
)

const (
Expand Down Expand Up @@ -73,16 +73,16 @@ func main() {
cmdconst.AtmosName: builder.BuildAtmosManager,
}

gruntParser := terragruntparser.Make()
manageHiddenCallCmd(&conf, builders, gruntParser) // proxy call use os.Exit when called
hclParser := hclparse.NewParser()
manageHiddenCallCmd(&conf, builders, hclParser) // proxy call use os.Exit when called

if err = initRootCmd(&conf, builders, gruntParser).Execute(); err != nil {
if err = initRootCmd(&conf, builders, hclParser).Execute(); err != nil {
loghelper.StdDisplay(err.Error())
os.Exit(1)
}
}

func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, gruntParser terragruntparser.TerragruntParser) *cobra.Command {
func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser) *cobra.Command {
rootCmd := &cobra.Command{
Use: cmdconst.TenvName,
Long: "tenv help manage several versions of OpenTofu (https://opentofu.org), Terraform (https://www.terraform.io), Terragrunt (https://terragrunt.gruntwork.io), and Atmos (https://atmos.tools/).",
Expand All @@ -102,7 +102,7 @@ func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, g
needToken: true, remoteEnvName: config.TofuRemoteURLEnvName,
pRemote: &conf.Tofu.RemoteURL, pPublicKeyPath: &conf.TofuKeyPath,
}
tofuManager := builders[cmdconst.TofuName](conf, gruntParser)
tofuManager := builders[cmdconst.TofuName](conf, hclParser)
initSubCmds(rootCmd, conf, tofuManager, tofuParams) // add tofu management at root level

tofuCmd := &cobra.Command{
Expand All @@ -127,7 +127,7 @@ func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, g
needToken: false, remoteEnvName: config.TfRemoteURLEnvName,
pRemote: &conf.Tf.RemoteURL, pPublicKeyPath: &conf.TfKeyPath,
}
initSubCmds(tfCmd, conf, builders[cmdconst.TerraformName](conf, gruntParser), tfParams)
initSubCmds(tfCmd, conf, builders[cmdconst.TerraformName](conf, hclParser), tfParams)

rootCmd.AddCommand(tfCmd)

Expand All @@ -141,7 +141,7 @@ func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, g
tgParams := subCmdParams{
needToken: true, remoteEnvName: config.TgRemoteURLEnvName, pRemote: &conf.Tg.RemoteURL,
}
initSubCmds(tgCmd, conf, builders[cmdconst.TerragruntName](conf, gruntParser), tgParams)
initSubCmds(tgCmd, conf, builders[cmdconst.TerragruntName](conf, hclParser), tgParams)

rootCmd.AddCommand(tgCmd)

Expand All @@ -154,23 +154,23 @@ func initRootCmd(conf *config.Config, builders map[string]builder.BuilderFunc, g
atmosParams := subCmdParams{
needToken: true, remoteEnvName: config.AtmosRemoteURLEnvName, pRemote: &conf.Atmos.RemoteURL,
}
initSubCmds(atmosCmd, conf, builders[cmdconst.AtmosName](conf, gruntParser), atmosParams)
initSubCmds(atmosCmd, conf, builders[cmdconst.AtmosName](conf, hclParser), atmosParams)

rootCmd.AddCommand(atmosCmd)

return rootCmd
}

func manageHiddenCallCmd(conf *config.Config, builders map[string]builder.BuilderFunc, gruntParser terragruntparser.TerragruntParser) {
func manageHiddenCallCmd(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser) {
if len(os.Args) < 3 || os.Args[1] != cmdconst.CallSubCmd {
return
}

calledNamed, cmdArgs := os.Args[2], os.Args[3:]
if builder, ok := builders[calledNamed]; ok {
proxy.Exec(conf, builder, gruntParser, calledNamed, cmdArgs)
proxy.Exec(conf, builder, hclParser, calledNamed, cmdArgs)
} else if calledNamed == cmdconst.AgnosticName {
proxy.ExecAgnostic(conf, builders, gruntParser, cmdArgs)
proxy.ExecAgnostic(conf, builders, hclParser, cmdArgs)
}
}

Expand Down
32 changes: 24 additions & 8 deletions versionmanager/builder/builder.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,22 +19,23 @@
package builder

import (
"github.com/hashicorp/hcl/v2/hclparse"
"github.com/tofuutils/tenv/v2/config"
"github.com/tofuutils/tenv/v2/versionmanager"
atmosretriever "github.com/tofuutils/tenv/v2/versionmanager/retriever/atmos"
terraformretriever "github.com/tofuutils/tenv/v2/versionmanager/retriever/terraform"
terragruntretriever "github.com/tofuutils/tenv/v2/versionmanager/retriever/terragrunt"
tofuretriever "github.com/tofuutils/tenv/v2/versionmanager/retriever/tofu"
"github.com/tofuutils/tenv/v2/versionmanager/semantic"
flatparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/flat"
iacparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/iac"
terragruntparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/terragrunt"
tomlparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/toml"
"github.com/tofuutils/tenv/v2/versionmanager/semantic/types"
)

type BuilderFunc = func(*config.Config, terragruntparser.TerragruntParser) versionmanager.VersionManager
type BuilderFunc = func(*config.Config, *hclparse.Parser) versionmanager.VersionManager

func BuildAtmosManager(conf *config.Config, gruntParser terragruntparser.TerragruntParser) versionmanager.VersionManager {
func BuildAtmosManager(conf *config.Config, hclParser *hclparse.Parser) versionmanager.VersionManager {
atmosRetriever := atmosretriever.Make(conf)
versionFiles := []types.VersionFile{
{Name: ".atmos-version", Parser: flatparser.RetrieveVersion},
Expand All @@ -43,20 +44,27 @@ func BuildAtmosManager(conf *config.Config, gruntParser terragruntparser.Terragr
return versionmanager.Make(conf, config.AtmosDefaultConstraintEnvName, "Atmos", nil, atmosRetriever, config.AtmosVersionEnvName, config.AtmosDefaultVersionEnvName, versionFiles)
}

func BuildTfManager(conf *config.Config, gruntParser terragruntparser.TerragruntParser) versionmanager.VersionManager {
func BuildTfManager(conf *config.Config, hclParser *hclparse.Parser) versionmanager.VersionManager {
tfRetriever := terraformretriever.Make(conf)
gruntParser := terragruntparser.Make(hclParser)
versionFiles := []types.VersionFile{
{Name: ".terraform-version", Parser: flatparser.RetrieveVersion},
{Name: ".tfswitchrc", Parser: flatparser.RetrieveVersion},
{Name: terragruntparser.HCLName, Parser: gruntParser.RetrieveTerraformVersionConstraintFromHCL},
{Name: terragruntparser.JSONName, Parser: gruntParser.RetrieveTerraformVersionConstraintFromJSON},
}

return versionmanager.Make(conf, config.TfDefaultConstraintEnvName, "Terraform", semantic.TfPredicateReaders, tfRetriever, config.TfVersionEnvName, config.TfDefaultVersionEnvName, versionFiles)
iacExts := []iacparser.ExtDescription{
{Value: ".tf", Parser: hclParser.ParseHCLFile},
{Value: ".tf.json", Parser: hclParser.ParseJSONFile},
}

return versionmanager.Make(conf, config.TfDefaultConstraintEnvName, "Terraform", iacExts, tfRetriever, config.TfVersionEnvName, config.TfDefaultVersionEnvName, versionFiles)
}

func BuildTgManager(conf *config.Config, gruntParser terragruntparser.TerragruntParser) versionmanager.VersionManager {
func BuildTgManager(conf *config.Config, hclParser *hclparse.Parser) versionmanager.VersionManager {
tgRetriever := terragruntretriever.Make(conf)
gruntParser := terragruntparser.Make(hclParser)
versionFiles := []types.VersionFile{
{Name: ".terragrunt-version", Parser: flatparser.RetrieveVersion},
{Name: ".tgswitchrc", Parser: flatparser.RetrieveVersion},
Expand All @@ -68,13 +76,21 @@ func BuildTgManager(conf *config.Config, gruntParser terragruntparser.Terragrunt
return versionmanager.Make(conf, config.TgDefaultConstraintEnvName, "Terragrunt", nil, tgRetriever, config.TgVersionEnvName, config.TgDefaultVersionEnvName, versionFiles)
}

func BuildTofuManager(conf *config.Config, gruntParser terragruntparser.TerragruntParser) versionmanager.VersionManager {
func BuildTofuManager(conf *config.Config, hclParser *hclparse.Parser) versionmanager.VersionManager {
tofuRetriever := tofuretriever.Make(conf)
gruntParser := terragruntparser.Make(hclParser)
versionFiles := []types.VersionFile{
{Name: ".opentofu-version", Parser: flatparser.RetrieveVersion},
{Name: terragruntparser.HCLName, Parser: gruntParser.RetrieveTerraformVersionConstraintFromHCL},
{Name: terragruntparser.JSONName, Parser: gruntParser.RetrieveTerraformVersionConstraintFromJSON},
}

return versionmanager.Make(conf, config.TofuDefaultConstraintEnvName, "OpenTofu", semantic.TfPredicateReaders, tofuRetriever, config.TofuVersionEnvName, config.TofuDefaultVersionEnvName, versionFiles)
iacExts := []iacparser.ExtDescription{
{Value: ".tofu", Parser: hclParser.ParseHCLFile},
{Value: ".tofu.json", Parser: hclParser.ParseJSONFile},
{Value: ".tf", Parser: hclParser.ParseHCLFile},
{Value: ".tf.json", Parser: hclParser.ParseJSONFile},
}

return versionmanager.Make(conf, config.TofuDefaultConstraintEnvName, "OpenTofu", iacExts, tofuRetriever, config.TofuVersionEnvName, config.TofuDefaultVersionEnvName, versionFiles)
}
21 changes: 8 additions & 13 deletions versionmanager/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
package versionmanager

import (
"bytes"
"errors"
"io/fs"
"os"
Expand All @@ -36,6 +35,7 @@ import (
"github.com/tofuutils/tenv/v2/pkg/reversecmp"
"github.com/tofuutils/tenv/v2/versionmanager/semantic"
flatparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/flat"
iacparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/iac"
"github.com/tofuutils/tenv/v2/versionmanager/semantic/types"
)

Expand All @@ -54,15 +54,15 @@ type VersionManager struct {
conf *config.Config
constraintEnvName string
FolderName string
predicateReaders []types.PredicateReader
iacExts []iacparser.ExtDescription
retriever ReleaseInfoRetriever
VersionEnvName string
defaultVersionEnvName string
VersionFiles []types.VersionFile
}

func Make(conf *config.Config, constraintEnvName string, folderName string, predicateReaders []types.PredicateReader, retriever ReleaseInfoRetriever, versionEnvName string, defaultVersionEnvName string, versionFiles []types.VersionFile) VersionManager {
return VersionManager{conf: conf, constraintEnvName: constraintEnvName, FolderName: folderName, predicateReaders: predicateReaders, retriever: retriever, VersionEnvName: versionEnvName, defaultVersionEnvName: defaultVersionEnvName, VersionFiles: versionFiles}
func Make(conf *config.Config, constraintEnvName string, folderName string, iacExts []iacparser.ExtDescription, retriever ReleaseInfoRetriever, versionEnvName string, defaultVersionEnvName string, versionFiles []types.VersionFile) VersionManager {
return VersionManager{conf: conf, constraintEnvName: constraintEnvName, FolderName: folderName, iacExts: iacExts, retriever: retriever, VersionEnvName: versionEnvName, defaultVersionEnvName: defaultVersionEnvName, VersionFiles: versionFiles}
}

// Detect version (resolve and evaluate, can install depending on auto install env var).
Expand Down Expand Up @@ -99,7 +99,7 @@ func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (strin
return cleanedVersion, m.installSpecificVersion(cleanedVersion, proxyCall)
}

predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.predicateReaders, m.conf)
predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf)
if err != nil {
m.conf.Displayer.Flush(proxyCall)

Expand Down Expand Up @@ -135,7 +135,7 @@ func (m VersionManager) Install(requestedVersion string) error {
return m.installSpecificVersion(parsedVersion.String(), false) // use a parsable version
}

predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.predicateReaders, m.conf)
predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf)
if err != nil {
return err
}
Expand Down Expand Up @@ -220,14 +220,9 @@ func (m VersionManager) ReadDefaultConstraint() string {
return constraint
}

data, err := os.ReadFile(m.RootConstraintFilePath())
if err != nil {
m.conf.Displayer.Log(loghelper.LevelWarnOrDebug(errors.Is(err, fs.ErrNotExist)), "Failed to read file", loghelper.Error, err)

return ""
}
constraint, _ := flatparser.Retrieve(m.RootConstraintFilePath(), m.conf, flatparser.NoMsg)

return string(bytes.TrimSpace(data))
return constraint
}

func (m VersionManager) ResetConstraint() error {
Expand Down
9 changes: 5 additions & 4 deletions versionmanager/proxy/agnostic.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ import (
"fmt"
"os"

"github.com/hashicorp/hcl/v2/hclparse"

"github.com/tofuutils/tenv/v2/config"
"github.com/tofuutils/tenv/v2/config/cmdconst"
"github.com/tofuutils/tenv/v2/versionmanager/builder"
terragruntparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/terragrunt"
)

func ExecAgnostic(conf *config.Config, builders map[string]builder.BuilderFunc, gruntParser terragruntparser.TerragruntParser, cmdArgs []string) {
func ExecAgnostic(conf *config.Config, builders map[string]builder.BuilderFunc, hclParser *hclparse.Parser, cmdArgs []string) {
conf.InitDisplayer(true)
manager := builders[cmdconst.TofuName](conf, gruntParser)
manager := builders[cmdconst.TofuName](conf, hclParser)
detectedVersion, err := manager.ResolveWithVersionFiles()
if err != nil {
fmt.Println("Failed to resolve a version allowing to call tofu :", err) //nolint
Expand All @@ -40,7 +41,7 @@ func ExecAgnostic(conf *config.Config, builders map[string]builder.BuilderFunc,
execName := cmdconst.TofuName
if detectedVersion == "" {
execName = cmdconst.TerraformName
manager = builders[cmdconst.TerraformName](conf, gruntParser)
manager = builders[cmdconst.TerraformName](conf, hclParser)
detectedVersion, err = manager.ResolveWithVersionFiles()
if err != nil {
fmt.Println("Failed to resolve a version allowing to call terraform :", err) //nolint
Expand Down
7 changes: 4 additions & 3 deletions versionmanager/proxy/proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@ import (
"os"
"path/filepath"

"github.com/hashicorp/hcl/v2/hclparse"

"github.com/tofuutils/tenv/v2/config"
cmdproxy "github.com/tofuutils/tenv/v2/pkg/cmdproxy"
"github.com/tofuutils/tenv/v2/versionmanager/builder"
terragruntparser "github.com/tofuutils/tenv/v2/versionmanager/semantic/parser/terragrunt"
)

var errDelimiter = errors.New("key and value should not contains delimiter")

func Exec(conf *config.Config, builderFunc builder.BuilderFunc, gruntParser terragruntparser.TerragruntParser, execName string, cmdArgs []string) {
func Exec(conf *config.Config, builderFunc builder.BuilderFunc, hclParser *hclparse.Parser, execName string, cmdArgs []string) {
conf.InitDisplayer(true)
versionManager := builderFunc(conf, gruntParser)
versionManager := builderFunc(conf, hclParser)
detectedVersion, err := versionManager.Detect(true)
if err != nil {
fmt.Println("Failed to detect a version allowing to call", execName, ":", err) //nolint
Expand Down
12 changes: 10 additions & 2 deletions versionmanager/semantic/parser/flat/flatparser.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,11 @@ import (
"github.com/tofuutils/tenv/v2/versionmanager/semantic/types"
)

func RetrieveVersion(filePath string, conf *config.Config) (string, error) {
func NoMsg(_ loghelper.Displayer, value string, _ string) string {
return value
}

func Retrieve(filePath string, conf *config.Config, displayMsg func(loghelper.Displayer, string, string) string) (string, error) {
data, err := os.ReadFile(filePath)
if err != nil {
conf.Displayer.Log(loghelper.LevelWarnOrDebug(errors.Is(err, fs.ErrNotExist)), "Failed to read file", loghelper.Error, err)
Expand All @@ -42,5 +46,9 @@ func RetrieveVersion(filePath string, conf *config.Config) (string, error) {
return "", nil
}

return types.DisplayDetectionInfo(conf.Displayer, resolvedVersion, filePath), nil
return displayMsg(conf.Displayer, resolvedVersion, filePath), nil
}

func RetrieveVersion(filePath string, conf *config.Config) (string, error) {
return Retrieve(filePath, conf, types.DisplayDetectionInfo)
}
Loading

0 comments on commit 57d31d0

Please sign in to comment.