diff --git a/README.md b/README.md index 3eef22b7..64f26224 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,7 @@ Terraform, Terragrunt and Atmos. - [Semver 2.0.0](https://semver.org/) Compatibility: Utilizes [go-version](https://github.com/hashicorp/go-version) for semantic versioning and use the [HCL](https://github.com/hashicorp/hcl) parser to extract required version constraint from OpenTofu/Terraform/Terragrunt files (see [required_version](#required_version) and [Terragrunt hcl](#terragrunt-hcl-file)). - Signature verification: Supports [cosign](https://github.com/sigstore/cosign) (if present on your machine) and PGP (via [gopenpgp](https://github.com/ProtonMail/gopenpgp)), see [signature support](#signature-support). - Intuitive installation: Simple installation process with Homebrew and manual options. +- Callable as [Go](https://go.dev) module, with a [Semver compatibility promise](https://semver.org/#summary) on [tenvlib](https://github.com/tofuutils/tenv/tree/main/versionmanager/tenvlib) wrapper package (get more information in [TENV_AS_LIB.md](https://github.com/tofuutils/tenv/blob/main/TENV_AS_LIB.md)). ### Difference with asdf diff --git a/TENV_AS_LIB.md b/TENV_AS_LIB.md new file mode 100644 index 00000000..6f0fe6a1 --- /dev/null +++ b/TENV_AS_LIB.md @@ -0,0 +1,71 @@ +# How to use tenv as a library + +## Get started + +### Prerequisites + +**tenv** requires [Go](https://go.dev) version [1.21](https://go.dev/doc/devel/release#go1.21.0) or above. + +### Getting tenv module + +`tenvlib` package is available since tenv v3.2 + +```console +go get -u github.com/tofuutils/tenv/v3@latest +``` + +### Basic example + +```go +package main + +import ( + "context" + "fmt" + + "github.com/tofuutils/tenv/v3/config/cmdconst" + "github.com/tofuutils/tenv/v3/versionmanager/tenvlib" +) + +func main() { + tenv, err := tenvlib.Make(tenvlib.AutoInstall, tenvlib.IgnoreEnv, tenvlib.DisableDisplay) + if err != nil { + fmt.Println("init failed :", err) + return + } + + err = tenv.DetectedCommandProxy(context.Background(), cmdconst.TofuName, "version") + if err != nil { + fmt.Println("proxy call failed :", err) + } +} +``` + +## Documentation + +See the [API documentation on go.dev](https://pkg.go.dev/github.com/tofuutils/tenv/v3/versionmanager/tenvlib) and [examples](https://github.com/tofuutils/tenv/tree/main/versionmanager/tenvlib/examples). + +### Overview + +Available Tenv struct creation options : + +- `AddTool(toolName string, builderFunc builder.BuilderFunc)`, extend `tenvlib` to support other tool use cases. +- `AutoInstall`, shortcut to force auto install feature enabling in `config.Config`. +- `DisableDisplay`, do not display or log anything. +- `IgnoreEnv`, ignore **tenv** environment variables (`TENV_AUTO_INSTALL`, `TOFUENV_TOFU_VERSION`, etc.). +- `WithConfig(conf *config.Config)`, replace default `Config` (one from a `InitConfigFromEnv` or `DefaultConfig` call depending on `IgnoreEnv` usage). +- `WithDisplayer(displayer loghelper.Displayer)`, replace default `Displayer` with a custom to handle `tenvlib` output (standard and log). +- `WithHCLParser(hclParser *hclparse.Parser)`, use passed `Parser` instead of creating a new one. + +Tenv methods list : + +- `[Detected]Command[Proxy]` +- `Detect` +- `Evaluate` +- `Install[Multiple]` +- `List[Local|Remote]` +- `LocallyInstalled` +- `[Res|S]etDefault[Constraint|Version]`, manage `constraint` and `version` files in `//` +- `Uninstall[Multiple]` + +Happy hacking ! diff --git a/cmd/tenv/subcmd.go b/cmd/tenv/subcmd.go index 899215d8..95ca626d 100644 --- a/cmd/tenv/subcmd.go +++ b/cmd/tenv/subcmd.go @@ -20,6 +20,7 @@ package main import ( "bytes" + "context" "os" "strconv" "strings" @@ -88,7 +89,8 @@ func newDetectCmd(conf *config.Config, versionManager versionmanager.VersionMana conf.InitDisplayer(false) conf.InitInstall(forceInstall, forceNoInstall) - detectedVersion, err := versionManager.Detect(false) + ctx := context.Background() + detectedVersion, err := versionManager.Detect(ctx, false) if err != nil { loghelper.StdDisplay(err.Error()) @@ -138,6 +140,7 @@ If a parameter is passed, available options: Run: func(_ *cobra.Command, args []string) { conf.InitDisplayer(false) + ctx := context.Background() if len(args) == 0 { version, err := versionManager.Resolve(semantic.LatestKey) if err != nil { @@ -146,14 +149,14 @@ If a parameter is passed, available options: return } - if err = versionManager.Install(version); err != nil { + if err = versionManager.Install(ctx, version); err != nil { loghelper.StdDisplay(err.Error()) } return } - if err := versionManager.Install(args[0]); err != nil { + if err := versionManager.Install(ctx, args[0]); err != nil { loghelper.StdDisplay(err.Error()) } }, @@ -244,7 +247,8 @@ func newListRemoteCmd(conf *config.Config, versionManager versionmanager.Version Run: func(_ *cobra.Command, _ []string) { conf.InitDisplayer(false) - versions, err := versionManager.ListRemote(reverseOrder) + ctx := context.Background() + versions, err := versionManager.ListRemote(ctx, reverseOrder) if err != nil { loghelper.StdDisplay(err.Error()) @@ -378,7 +382,8 @@ Available parameter options: conf.InitDisplayer(false) conf.InitInstall(forceInstall, forceNoInstall) - if err := versionManager.Use(args[0], workingDir); err != nil { + ctx := context.Background() + if err := versionManager.Use(ctx, args[0], workingDir); err != nil { loghelper.StdDisplay(err.Error()) } }, diff --git a/cmd/tenv/tenv.go b/cmd/tenv/tenv.go index fdcf4f30..9a3153d0 100644 --- a/cmd/tenv/tenv.go +++ b/cmd/tenv/tenv.go @@ -19,6 +19,7 @@ package main import ( + "context" "fmt" "os" "path/filepath" @@ -157,7 +158,8 @@ func manageNoArgsCmd(conf *config.Config, hclParser *hclparse.Parser) { return } - if err := toolUI(conf, hclParser); err != nil { + ctx := context.Background() + if err := toolUI(ctx, conf, hclParser); err != nil { fmt.Println(err.Error()) os.Exit(1) diff --git a/cmd/tenv/textui.go b/cmd/tenv/textui.go index 5878490b..066a5296 100644 --- a/cmd/tenv/textui.go +++ b/cmd/tenv/textui.go @@ -19,6 +19,7 @@ package main import ( + "context" "fmt" "io" "slices" @@ -172,7 +173,7 @@ func (m itemModel) View() string { return "\n" + m.list.View() } -func toolUI(conf *config.Config, hclParser *hclparse.Parser) error { +func toolUI(ctx context.Context, conf *config.Config, hclParser *hclparse.Parser) error { conf.InitDisplayer(false) // shared object @@ -212,7 +213,7 @@ func toolUI(conf *config.Config, hclParser *hclparse.Parser) error { for _, toolItem := range tools { tool := toolItem.FilterValue() if _, selected := selection[tool]; selected { - if err = manageUI(builder.Builders[tool](conf, hclParser)); err != nil { + if err = manageUI(ctx, builder.Builders[tool](conf, hclParser)); err != nil { return err } } @@ -221,10 +222,10 @@ func toolUI(conf *config.Config, hclParser *hclparse.Parser) error { return nil } -func manageUI(versionManager versionmanager.VersionManager) error { +func manageUI(ctx context.Context, versionManager versionmanager.VersionManager) error { installed := versionManager.LocalSet() - remoteVersions, err := versionManager.ListRemote(true) + remoteVersions, err := versionManager.ListRemote(ctx, true) if err != nil { return err } @@ -286,7 +287,7 @@ func manageUI(versionManager versionmanager.VersionManager) error { return nil } - return versionManager.InstallMultiple(toInstall) + return versionManager.InstallMultiple(ctx, toInstall) } func uninstallUI(versionManager versionmanager.VersionManager) error { diff --git a/config/config.go b/config/config.go index c7c56072..2857c4ee 100644 --- a/config/config.go +++ b/config/config.go @@ -35,6 +35,7 @@ import ( ) const ( + defaultDirName = ".tenv" githubActionsEnvName = "GITHUB_ACTIONS" archEnvName = "ARCH" @@ -132,10 +133,10 @@ type Config struct { ForceRemote bool GithubActions bool GithubToken string - NoInstall bool remoteConfLoaded bool RemoteConfPath string RootPath string + SkipInstall bool SkipSignature bool Tf RemoteConfig TfKeyPath string @@ -145,6 +146,25 @@ type Config struct { UserPath string } +func DefaultConfig() (Config, error) { + userPath, err := os.UserHomeDir() + if err != nil { + return Config{}, err + } + + return Config{ + Arch: runtime.GOARCH, + Atmos: makeDefaultRemoteConfig(defaultAtmosGithubURL, baseGithubURL), + remoteConfLoaded: true, + RootPath: filepath.Join(userPath, defaultDirName), + SkipInstall: true, + Tf: makeDefaultRemoteConfig(defaultHashicorpURL, defaultHashicorpURL), + Tg: makeDefaultRemoteConfig(defaultTerragruntGithubURL, baseGithubURL), + Tofu: makeDefaultRemoteConfig(DefaultTofuGithubURL, baseGithubURL), + UserPath: userPath, + }, nil +} + func InitConfigFromEnv() (Config, error) { userPath, err := os.UserHomeDir() if err != nil { @@ -168,7 +188,7 @@ func InitConfigFromEnv() (Config, error) { rootPath := configutils.GetenvFallback(tenvRootPathEnvName, tofuRootPathEnvName, tfRootPathEnvName) if rootPath == "" { - rootPath = filepath.Join(userPath, ".tenv") + rootPath = filepath.Join(userPath, defaultDirName) } quiet, err := configutils.GetenvBoolFallback(false, tenvQuietEnvName) @@ -188,9 +208,9 @@ func InitConfigFromEnv() (Config, error) { ForceRemote: forceRemote, GithubActions: gha, GithubToken: configutils.GetenvFallback(tenvTokenEnvName, tofuTokenEnvName), - NoInstall: !autoInstall, RemoteConfPath: os.Getenv(tenvRemoteConfEnvName), RootPath: rootPath, + SkipInstall: !autoInstall, Tf: makeRemoteConfig(TfRemoteURLEnvName, tfListURLEnvName, tfInstallModeEnvName, tfListModeEnvName, defaultHashicorpURL, defaultHashicorpURL), TfKeyPath: os.Getenv(tfHashicorpPGPKeyEnvName), Tg: makeRemoteConfig(TgRemoteURLEnvName, tgListURLEnvName, tgInstallModeEnvName, tgListModeEnvName, defaultTerragruntGithubURL, baseGithubURL), @@ -228,9 +248,9 @@ func (conf *Config) InitDisplayer(proxyCall bool) { func (conf *Config) InitInstall(forceInstall bool, forceNoInstall bool) { switch { case forceNoInstall: // higher priority to --no-install - conf.NoInstall = true + conf.SkipInstall = true case forceInstall: - conf.NoInstall = false + conf.SkipInstall = false } } diff --git a/config/remote.go b/config/remote.go index 0c0b7055..21a20998 100644 --- a/config/remote.go +++ b/config/remote.go @@ -56,6 +56,12 @@ type RemoteConfig struct { RemoteURLEnv string // value from env } +func makeDefaultRemoteConfig(defaultURL string, defaultBaseURL string) RemoteConfig { + return RemoteConfig{ + defaultBaseURL: defaultBaseURL, defaultURL: defaultURL, Data: map[string]string{}, + } +} + func makeRemoteConfig(remoteURLEnvName string, listURLEnvName string, installModeEnvName string, listModeEnvName string, defaultURL string, defaultBaseURL string) RemoteConfig { return RemoteConfig{ defaultBaseURL: defaultBaseURL, defaultURL: defaultURL, installMode: os.Getenv(installModeEnvName), listMode: os.Getenv(listModeEnvName), diff --git a/go.mod b/go.mod index 81fc39c9..60fe6a40 100644 --- a/go.mod +++ b/go.mod @@ -1,13 +1,15 @@ module github.com/tofuutils/tenv/v3 -go 1.21 +go 1.23 + +toolchain go1.23.1 require ( github.com/BurntSushi/toml v1.4.0 github.com/ProtonMail/gopenpgp/v2 v2.7.5 - github.com/PuerkitoBio/goquery v1.9.2 - github.com/charmbracelet/bubbles v0.19.0 - github.com/charmbracelet/bubbletea v0.27.1 + github.com/PuerkitoBio/goquery v1.10.0 + github.com/charmbracelet/bubbles v0.20.0 + github.com/charmbracelet/bubbletea v1.1.0 github.com/charmbracelet/lipgloss v0.13.0 github.com/fatih/color v1.17.0 github.com/hashicorp/go-hclog v1.6.3 @@ -27,11 +29,9 @@ require ( github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect github.com/atotto/clipboard v0.1.4 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect - github.com/charmbracelet/x/ansi v0.1.4 // indirect - github.com/charmbracelet/x/input v0.1.3 // indirect - github.com/charmbracelet/x/term v0.1.1 // indirect - github.com/charmbracelet/x/windows v0.1.2 // indirect - github.com/cloudflare/circl v1.3.9 // indirect + github.com/charmbracelet/x/ansi v0.2.3 // indirect + github.com/charmbracelet/x/term v0.2.0 // indirect + github.com/cloudflare/circl v1.4.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect github.com/go-test/deep v1.1.0 // indirect @@ -50,12 +50,11 @@ require ( github.com/rivo/uniseg v0.4.7 // indirect github.com/sahilm/fuzzy v0.1.1 // indirect github.com/stretchr/testify v1.8.4 // indirect - github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.26.0 // indirect - golang.org/x/mod v0.20.0 // indirect - golang.org/x/net v0.28.0 // indirect + golang.org/x/crypto v0.27.0 // indirect + golang.org/x/mod v0.21.0 // indirect + golang.org/x/net v0.29.0 // indirect golang.org/x/sync v0.8.0 // indirect - golang.org/x/sys v0.24.0 // indirect - golang.org/x/text v0.17.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/tools v0.24.0 // indirect ) diff --git a/go.sum b/go.sum index b387a237..d112c492 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,8 @@ github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f h1:tCbYj7/299ek github.com/ProtonMail/go-mime v0.0.0-20230322103455-7d82a3887f2f/go.mod h1:gcr0kNtGBqin9zDW9GOHcVntrwnjrK+qdJ06mWYBybw= github.com/ProtonMail/gopenpgp/v2 v2.7.5 h1:STOY3vgES59gNgoOt2w0nyHBjKViB/qSg7NjbQWPJkA= github.com/ProtonMail/gopenpgp/v2 v2.7.5/go.mod h1:IhkNEDaxec6NyzSI0PlxapinnwPVIESk8/76da3Ct3g= -github.com/PuerkitoBio/goquery v1.9.2 h1:4/wZksC3KgkQw7SQgkKotmKljk0M6V8TUvA8Wb4yPeE= -github.com/PuerkitoBio/goquery v1.9.2/go.mod h1:GHPCaP0ODyyxqcNoFGYlAprUFH81NuRPd0GX3Zu2Mvk= +github.com/PuerkitoBio/goquery v1.10.0 h1:6fiXdLuUvYs2OJSvNRqlNPoBm6YABE226xrbavY5Wv4= +github.com/PuerkitoBio/goquery v1.10.0/go.mod h1:TjZZl68Q3eGHNBA8CWaxAN7rOU1EbDz3CWuolcO5Yu4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= @@ -20,23 +20,19 @@ github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/charmbracelet/bubbles v0.19.0 h1:gKZkKXPP6GlDk6EcfujDK19PCQqRjaJZQ7QRERx1UF0= -github.com/charmbracelet/bubbles v0.19.0/go.mod h1:WILteEqZ+krG5c3ntGEMeG99nCupcuIk7V0/zOP0tOA= -github.com/charmbracelet/bubbletea v0.27.1 h1:/yhaJKX52pxG4jZVKCNWj/oq0QouPdXycriDRA6m6r8= -github.com/charmbracelet/bubbletea v0.27.1/go.mod h1:xc4gm5yv+7tbniEvQ0naiG9P3fzYhk16cTgDZQQW6YE= +github.com/charmbracelet/bubbles v0.20.0 h1:jSZu6qD8cRQ6k9OMfR1WlM+ruM8fkPWkHvQWD9LIutE= +github.com/charmbracelet/bubbles v0.20.0/go.mod h1:39slydyswPy+uVOHZ5x/GjwVAFkCsV8IIVy+4MhzwwU= +github.com/charmbracelet/bubbletea v1.1.0 h1:FjAl9eAL3HBCHenhz/ZPjkKdScmaS5SK69JAK2YJK9c= +github.com/charmbracelet/bubbletea v1.1.0/go.mod h1:9Ogk0HrdbHolIKHdjfFpyXJmiCzGwy+FesYkZr7hYU4= github.com/charmbracelet/lipgloss v0.13.0 h1:4X3PPeoWEDCMvzDvGmTajSyYPcZM4+y8sCA/SsA3cjw= github.com/charmbracelet/lipgloss v0.13.0/go.mod h1:nw4zy0SBX/F/eAO1cWdcvy6qnkDUxr8Lw7dvFrAIbbY= -github.com/charmbracelet/x/ansi v0.1.4 h1:IEU3D6+dWwPSgZ6HBH+v6oUuZ/nVawMiWj5831KfiLM= -github.com/charmbracelet/x/ansi v0.1.4/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= -github.com/charmbracelet/x/input v0.1.3 h1:oy4TMhyGQsYs/WWJwu1ELUMFnjiUAXwtDf048fHbCkg= -github.com/charmbracelet/x/input v0.1.3/go.mod h1:1gaCOyw1KI9e2j00j/BBZ4ErzRZqa05w0Ghn83yIhKU= -github.com/charmbracelet/x/term v0.1.1 h1:3cosVAiPOig+EV4X9U+3LDgtwwAoEzJjNdwbXDjF6yI= -github.com/charmbracelet/x/term v0.1.1/go.mod h1:wB1fHt5ECsu3mXYusyzcngVWWlu1KKUmmLhfgr/Flxw= -github.com/charmbracelet/x/windows v0.1.2 h1:Iumiwq2G+BRmgoayww/qfcvof7W/3uLoelhxojXlRWg= -github.com/charmbracelet/x/windows v0.1.2/go.mod h1:GLEO/l+lizvFDBPLIOk+49gdX49L9YWMB5t+DZd0jkQ= +github.com/charmbracelet/x/ansi v0.2.3 h1:VfFN0NUpcjBRd4DnKfRaIRo53KRgey/nhOoEqosGDEY= +github.com/charmbracelet/x/ansi v0.2.3/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw= +github.com/charmbracelet/x/term v0.2.0 h1:cNB9Ot9q8I711MyZ7myUR5HFWL/lc3OpU8jZ4hwm0x0= +github.com/charmbracelet/x/term v0.2.0/go.mod h1:GVxgxAbjUrmpvIINHIQnJJKpMlHiZ4cktEQCN6GWyF0= github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.9 h1:QFrlgFYf2Qpi8bSpVPK1HBvWpx16v/1TZivyo7pGuBE= -github.com/cloudflare/circl v1.3.9/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= +github.com/cloudflare/circl v1.4.0 h1:BV7h5MgrktNzytKmWjpOtdYrf0lkkbF8YMlBGPhJQrY= +github.com/cloudflare/circl v1.4.0/go.mod h1:PDRU+oXvdD7KCtgKxW95M5Z8BpSCJXQORiZFnBQS5QU= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -104,8 +100,6 @@ github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= -github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/zclconf/go-cty v1.15.0 h1:tTCRWxsexYUmtt/wVxgDClUe+uQusuI443uL6e+5sXQ= github.com/zclconf/go-cty v1.15.0/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= @@ -115,14 +109,12 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.26.0 h1:RrRspgV4mU+YwB4FYnuBoKsUapNIL5cohGAmSH3azsw= -golang.org/x/crypto v0.26.0/go.mod h1:GY7jblb9wI+FOo5y8/S2oY4zWP07AkOJ4+jxCqdqn54= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.20.0 h1:utOm6MM3R3dnawAiJgn0y+xvuYRsm1RKM/4giyfDgV0= -golang.org/x/mod v0.20.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= @@ -130,8 +122,8 @@ golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -154,8 +146,8 @@ golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= -golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= @@ -169,8 +161,8 @@ golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc= -golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/pkg/cmdproxy/proxy.go b/pkg/cmdproxy/proxy.go index 247789fa..25beb9ed 100644 --- a/pkg/cmdproxy/proxy.go +++ b/pkg/cmdproxy/proxy.go @@ -32,24 +32,23 @@ import ( var errDelimiter = errors.New("key and value should not contains delimiter") -func Run(execPath string, cmdArgs []string, gha bool) { +// Always call os.Exit. +func Run(cmd *exec.Cmd, gha bool) { exitCode := 0 defer func() { os.Exit(exitCode) }() - // proxy to selected version - cmd := exec.Command(execPath, cmdArgs...) - done, err := initIO(cmd, execPath, &exitCode, gha) + done, err := initIO(cmd, &exitCode, gha) if err != nil { - exitWithErrorMsg(execPath, err, &exitCode) + exitWithErrorMsg(cmd.Path, err, &exitCode) return } defer done() if err = cmd.Start(); err != nil { - exitWithErrorMsg(execPath, err, &exitCode) + exitWithErrorMsg(cmd.Path, err, &exitCode) return } @@ -65,18 +64,18 @@ func Run(execPath string, cmdArgs []string, gha bool) { return } - exitWithErrorMsg(execPath, err, &exitCode) + exitWithErrorMsg(cmd.Path, err, &exitCode) } } -func exitWithErrorMsg(execName string, err error, pExitCode *int) { - fmt.Println("Failure during", execName, "call :", err) //nolint +func exitWithErrorMsg(execPath string, err error, pExitCode *int) { + fmt.Println("Failure during", execPath, "call :", err) //nolint if *pExitCode == 0 { *pExitCode = 1 } } -func initIO(cmd *exec.Cmd, execName string, pExitCode *int, gha bool) (func(), error) { +func initIO(cmd *exec.Cmd, pExitCode *int, gha bool) (func(), error) { cmd.Stdin = os.Stdin if !gha { cmd.Stderr = os.Stderr @@ -100,27 +99,27 @@ func initIO(cmd *exec.Cmd, execName string, pExitCode *int, gha bool) (func(), e err = writeMultiline(outputFile, "stderr", errBuffer.String()) if err != nil { - exitWithErrorMsg(execName, err, pExitCode) + exitWithErrorMsg(cmd.Path, err, pExitCode) return } if err = writeMultiline(outputFile, "stdout", outBuffer.String()); err != nil { - exitWithErrorMsg(execName, err, pExitCode) + exitWithErrorMsg(cmd.Path, err, pExitCode) return } exitCode := *pExitCode if err = writeMultiline(outputFile, "exitcode", strconv.Itoa(exitCode)); err != nil { - exitWithErrorMsg(execName, err, pExitCode) + exitWithErrorMsg(cmd.Path, err, pExitCode) return } if exitCode != 0 && exitCode != 2 { err = fmt.Errorf("exited with code %d", exitCode) - exitWithErrorMsg(execName, err, pExitCode) + exitWithErrorMsg(cmd.Path, err, pExitCode) } }, nil } diff --git a/pkg/download/download.go b/pkg/download/download.go index 37c74b61..48831f8c 100644 --- a/pkg/download/download.go +++ b/pkg/download/download.go @@ -19,13 +19,14 @@ package download import ( + "context" "encoding/json" "io" "net/http" "net/url" ) -type RequestOption = func(*http.Request) *http.Request +type RequestOption = func(*http.Request) func ApplyUrlTranformer(urlTransformer func(string) (string, error), baseURLs ...string) ([]string, error) { transformedURLs := make([]string, 0, len(baseURLs)) @@ -41,16 +42,16 @@ func ApplyUrlTranformer(urlTransformer func(string) (string, error), baseURLs .. return transformedURLs, nil } -func Bytes(url string, display func(string), requestOptions ...RequestOption) ([]byte, error) { +func Bytes(ctx context.Context, url string, display func(string), requestOptions ...RequestOption) ([]byte, error) { display("Downloading " + url) - request, err := http.NewRequest(http.MethodGet, url, http.NoBody) + request, err := http.NewRequestWithContext(ctx, http.MethodGet, url, http.NoBody) if err != nil { return nil, err } for _, option := range requestOptions { - request = option(request) + option(request) } response, err := http.DefaultClient.Do(request) @@ -62,8 +63,8 @@ func Bytes(url string, display func(string), requestOptions ...RequestOption) ([ return io.ReadAll(response.Body) } -func JSON(url string, display func(string), requestOptions ...RequestOption) (any, error) { - data, err := Bytes(url, display, requestOptions...) +func JSON(ctx context.Context, url string, display func(string), requestOptions ...RequestOption) (any, error) { + data, err := Bytes(ctx, url, display, requestOptions...) if err != nil { return nil, err } @@ -98,10 +99,8 @@ func UrlTranformer(rewriteRule []string) func(string) (string, error) { } func WithBasicAuth(username string, password string) RequestOption { - return func(r *http.Request) *http.Request { + return func(r *http.Request) { r.SetBasicAuth(username, password) - - return r } } diff --git a/pkg/github/github.go b/pkg/github/github.go index 6c532d07..354d9043 100644 --- a/pkg/github/github.go +++ b/pkg/github/github.go @@ -19,6 +19,7 @@ package github import ( + "context" "errors" "net/http" "net/url" @@ -38,7 +39,7 @@ const ( var errContinue = errors.New("continue") -func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL string, githubToken string, display func(string)) ([]string, error) { +func AssetDownloadURL(ctx context.Context, tag string, searchedAssetNames []string, githubReleaseURL string, githubToken string, display func(string)) ([]string, error) { releaseUrl, err := url.JoinPath(githubReleaseURL, "tags", tag) //nolint if err != nil { return nil, err @@ -47,7 +48,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL display(apimsg.MsgFetchRelease + releaseUrl) authorizationHeader := buildAuthorizationHeader(githubToken) - value, err := apiGetRequest(releaseUrl, authorizationHeader) + value, err := apiGetRequest(ctx, releaseUrl, authorizationHeader) if err != nil { return nil, err } @@ -69,7 +70,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL baseAssetsURL += pageQuery for { assetsURL := baseAssetsURL + strconv.Itoa(page) - value, err = apiGetRequest(assetsURL, authorizationHeader) + value, err = apiGetRequest(ctx, assetsURL, authorizationHeader) if err != nil { return nil, err } @@ -88,7 +89,7 @@ func AssetDownloadURL(tag string, searchedAssetNames []string, githubReleaseURL } } -func ListReleases(githubReleaseURL string, githubToken string) ([]string, error) { +func ListReleases(ctx context.Context, githubReleaseURL string, githubToken string) ([]string, error) { basePageURL := githubReleaseURL + pageQuery authorizationHeader := buildAuthorizationHeader(githubToken) @@ -96,7 +97,7 @@ func ListReleases(githubReleaseURL string, githubToken string) ([]string, error) var releases []string for { pageURL := basePageURL + strconv.Itoa(page) - value, err := apiGetRequest(pageURL, authorizationHeader) + value, err := apiGetRequest(ctx, pageURL, authorizationHeader) if err != nil { return nil, err } @@ -111,15 +112,13 @@ func ListReleases(githubReleaseURL string, githubToken string) ([]string, error) } } -func apiGetRequest(callURL string, authorizationHeader string) (any, error) { - return download.JSON(callURL, download.NoDisplay, func(request *http.Request) *http.Request { +func apiGetRequest(ctx context.Context, callURL string, authorizationHeader string) (any, error) { + return download.JSON(ctx, callURL, download.NoDisplay, func(request *http.Request) { request.Header.Set("Accept", "application/vnd.github+json") if authorizationHeader != "" { request.Header.Set("Authorization", authorizationHeader) } request.Header.Set("X-GitHub-Api-Version", "2022-11-28") - - return request }) } diff --git a/pkg/htmlquery/html.go b/pkg/htmlquery/html.go index 79c9ff6a..ee4edec0 100644 --- a/pkg/htmlquery/html.go +++ b/pkg/htmlquery/html.go @@ -20,6 +20,7 @@ package htmlquery import ( "bytes" + "context" "strings" "github.com/PuerkitoBio/goquery" @@ -27,8 +28,8 @@ import ( "github.com/tofuutils/tenv/v3/pkg/download" ) -func Request(callURL string, selector string, extractor func(*goquery.Selection) string, ro ...download.RequestOption) ([]string, error) { - data, err := download.Bytes(callURL, download.NoDisplay, ro...) +func Request(ctx context.Context, callURL string, selector string, extractor func(*goquery.Selection) string, ro ...download.RequestOption) ([]string, error) { + data, err := download.Bytes(ctx, callURL, download.NoDisplay, ro...) if err != nil { return nil, err } diff --git a/versionmanager/manager.go b/versionmanager/manager.go index 29927c9e..5dbcb5a8 100644 --- a/versionmanager/manager.go +++ b/versionmanager/manager.go @@ -19,6 +19,7 @@ package versionmanager import ( + "context" "errors" "io/fs" "os" @@ -48,8 +49,8 @@ var ( ) type ReleaseInfoRetriever interface { - InstallRelease(version string, targetPath string) error - ListReleases() ([]string, error) + InstallRelease(ctx context.Context, version string, targetPath string) error + ListReleases(ctx context.Context) ([]string, error) } type DatedVersion struct { @@ -73,7 +74,7 @@ func Make(conf *config.Config, constraintEnvName string, folderName string, iacE } // Detect version (resolve and evaluate, can install depending on auto install env var). -func (m VersionManager) Detect(proxyCall bool) (string, error) { +func (m VersionManager) Detect(ctx context.Context, proxyCall bool) (string, error) { configVersion, err := m.Resolve(semantic.LatestAllowedKey) if err != nil { m.conf.Displayer.Flush(proxyCall) @@ -81,15 +82,15 @@ func (m VersionManager) Detect(proxyCall bool) (string, error) { return "", err } - return m.Evaluate(configVersion, proxyCall) + return m.Evaluate(ctx, configVersion, proxyCall) } // Evaluate version resolution strategy or version constraint (can install depending on auto install env var). -func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (string, error) { +func (m VersionManager) Evaluate(ctx context.Context, requestedVersion string, proxyCall bool) (string, error) { parsedVersion, err := version.NewVersion(requestedVersion) if err == nil { cleanedVersion := parsedVersion.String() // use a parsable version - if m.conf.NoInstall { + if m.conf.SkipInstall { _, installed, err := m.checkVersionInstallation("", cleanedVersion) if err != nil { return "", err @@ -103,7 +104,7 @@ func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (strin return cleanedVersion, nil } - return cleanedVersion, m.installSpecificVersion(cleanedVersion, proxyCall) + return cleanedVersion, m.installSpecificVersion(ctx, cleanedVersion, proxyCall) } predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf) @@ -140,13 +141,13 @@ func (m VersionManager) Evaluate(requestedVersion string, proxyCall bool) (strin m.conf.Displayer.Display("No compatible version found locally, search a remote one...") } - return m.searchInstallRemote(predicateInfo, m.conf.NoInstall, proxyCall) + return m.searchInstallRemote(ctx, predicateInfo, m.conf.SkipInstall, proxyCall) } -func (m VersionManager) Install(requestedVersion string) error { +func (m VersionManager) Install(ctx context.Context, requestedVersion string) error { parsedVersion, err := version.NewVersion(requestedVersion) if err == nil { - return m.installSpecificVersion(parsedVersion.String(), false) // use a parsable version + return m.installSpecificVersion(ctx, parsedVersion.String(), false) // use a parsable version } predicateInfo, err := semantic.ParsePredicate(requestedVersion, m.FolderName, m, m.iacExts, m.conf) @@ -155,12 +156,12 @@ func (m VersionManager) Install(requestedVersion string) error { } // noInstall is set to false to force install regardless of conf - _, err = m.searchInstallRemote(predicateInfo, false, false) + _, err = m.searchInstallRemote(ctx, predicateInfo, false, false) return err } -func (m VersionManager) InstallMultiple(versions []string) error { +func (m VersionManager) InstallMultiple(ctx context.Context, versions []string) error { installPath, err := m.InstallPath() if err != nil { return err @@ -172,7 +173,7 @@ func (m VersionManager) InstallMultiple(versions []string) error { defer deleteLock() for _, version := range versions { - if err = m.installSpecificVersionWithoutLock(installPath, version, false); err != nil { + if err = m.installSpecificVersionWithoutLock(ctx, installPath, version, false); err != nil { return err } } @@ -210,8 +211,8 @@ func (m VersionManager) ListLocal(reverseOrder bool) ([]DatedVersion, error) { return datedVersions, nil } -func (m VersionManager) ListRemote(reverseOrder bool) ([]string, error) { - versions, err := m.retriever.ListReleases() +func (m VersionManager) ListRemote(ctx context.Context, reverseOrder bool) ([]string, error) { + versions, err := m.retriever.ListReleases(ctx) if err != nil { return nil, err } @@ -384,8 +385,8 @@ func (m VersionManager) UninstallMultiple(versions []string) error { return nil } -func (m VersionManager) Use(requestedVersion string, workingDir bool) error { - detectedVersion, err := m.Evaluate(requestedVersion, false) +func (m VersionManager) Use(ctx context.Context, requestedVersion string, workingDir bool) error { + detectedVersion, err := m.Evaluate(ctx, requestedVersion, false) if err != nil { if err != ErrNoCompatibleLocally { return err @@ -454,7 +455,7 @@ func (m VersionManager) innerListLocal(installPath string, reverseOrder bool) ([ return versions, nil } -func (m VersionManager) installSpecificVersion(version string, proxyCall bool) error { +func (m VersionManager) installSpecificVersion(ctx context.Context, version string, proxyCall bool) error { if version == "" { m.conf.Displayer.Flush(proxyCall) @@ -478,10 +479,10 @@ func (m VersionManager) installSpecificVersion(version string, proxyCall bool) e defer disableExit() defer deleteLock() - return m.installSpecificVersionWithoutLock(installPath, version, proxyCall) + return m.installSpecificVersionWithoutLock(ctx, installPath, version, proxyCall) } -func (m VersionManager) installSpecificVersionWithoutLock(installPath string, version string, proxyCall bool) error { +func (m VersionManager) installSpecificVersionWithoutLock(ctx context.Context, installPath string, version string, proxyCall bool) error { // second check with lock to ensure there is no ongoing install _, installed, err := m.checkVersionInstallation(installPath, version) if err != nil { @@ -498,7 +499,7 @@ func (m VersionManager) installSpecificVersionWithoutLock(installPath string, ve m.conf.Displayer.Flush(false) m.conf.Displayer.Display(loghelper.Concat("Installing ", m.FolderName, " ", version)) - err = m.retriever.InstallRelease(version, filepath.Join(installPath, version)) + err = m.retriever.InstallRelease(ctx, version, filepath.Join(installPath, version)) if err == nil { m.conf.Displayer.Display(loghelper.Concat("Installation of ", m.FolderName, " ", version, " successful")) } @@ -506,8 +507,8 @@ func (m VersionManager) installSpecificVersionWithoutLock(installPath string, ve return err } -func (m VersionManager) searchInstallRemote(predicateInfo types.PredicateInfo, noInstall bool, proxyCall bool) (string, error) { - versions, err := m.ListRemote(predicateInfo.ReverseOrder) +func (m VersionManager) searchInstallRemote(ctx context.Context, predicateInfo types.PredicateInfo, noInstall bool, proxyCall bool) (string, error) { + versions, err := m.ListRemote(ctx, predicateInfo.ReverseOrder) if err != nil { m.conf.Displayer.Flush(proxyCall) @@ -521,7 +522,7 @@ func (m VersionManager) searchInstallRemote(predicateInfo types.PredicateInfo, n return version, m.autoInstallDisabledMsg(version) } - return version, m.installSpecificVersion(version, proxyCall) + return version, m.installSpecificVersion(ctx, version, proxyCall) } } m.conf.Displayer.Flush(proxyCall) diff --git a/versionmanager/proxy/agnostic.go b/versionmanager/proxy/agnostic.go index 39f27de0..21fd4b54 100644 --- a/versionmanager/proxy/agnostic.go +++ b/versionmanager/proxy/agnostic.go @@ -19,18 +19,23 @@ package proxy import ( + "context" "fmt" "os" + "os/exec" "github.com/hashicorp/hcl/v2/hclparse" "github.com/tofuutils/tenv/v3/config" "github.com/tofuutils/tenv/v3/config/cmdconst" + cmdproxy "github.com/tofuutils/tenv/v3/pkg/cmdproxy" "github.com/tofuutils/tenv/v3/versionmanager/builder" ) +// Always call os.Exit. func ExecAgnostic(conf *config.Config, hclParser *hclparse.Parser, cmdArgs []string) { conf.InitDisplayer(true) + ctx := context.Background() manager := builder.BuildTofuManager(conf, hclParser) detectedVersion, err := manager.ResolveWithVersionFiles() if err != nil { @@ -60,11 +65,13 @@ func ExecAgnostic(conf *config.Config, hclParser *hclparse.Parser, cmdArgs []str os.Exit(1) } - detectedVersion, err = manager.Evaluate(detectedVersion, true) + detectedVersion, err = manager.Evaluate(ctx, detectedVersion, true) if err != nil { fmt.Println("Failed to evaluate the requested version in a specific version allowing to call", execName, ":", err) //nolint os.Exit(1) } - RunCmd(installPath, detectedVersion, execName, cmdArgs, conf.GithubActions, conf.Displayer) + execPath := ExecPath(installPath, detectedVersion, execName, conf.Displayer) + + cmdproxy.Run(exec.CommandContext(ctx, execPath, cmdArgs...), conf.GithubActions) } diff --git a/versionmanager/proxy/proxy.go b/versionmanager/proxy/proxy.go index b362ff2d..8657eed9 100644 --- a/versionmanager/proxy/proxy.go +++ b/versionmanager/proxy/proxy.go @@ -19,9 +19,11 @@ package proxy import ( + "context" "errors" "fmt" "os" + "os/exec" "path/filepath" "github.com/hashicorp/hcl/v2/hclparse" @@ -35,10 +37,12 @@ import ( var errDelimiter = errors.New("key and value should not contains delimiter") +// Always call os.Exit. func Exec(conf *config.Config, builderFunc builder.BuilderFunc, hclParser *hclparse.Parser, execName string, cmdArgs []string) { conf.InitDisplayer(true) + ctx := context.Background() versionManager := builderFunc(conf, hclParser) - detectedVersion, err := versionManager.Detect(true) + detectedVersion, err := versionManager.Detect(ctx, true) if err != nil { fmt.Println("Failed to detect a version allowing to call", execName, ":", err) //nolint os.Exit(1) @@ -50,13 +54,14 @@ func Exec(conf *config.Config, builderFunc builder.BuilderFunc, hclParser *hclpa os.Exit(1) } - RunCmd(installPath, detectedVersion, execName, cmdArgs, conf.GithubActions, conf.Displayer) -} + execPath := ExecPath(installPath, detectedVersion, execName, conf.Displayer) -func RunCmd(installPath string, detectedVersion string, execName string, cmdArgs []string, gha bool, displayer loghelper.Displayer) { - versionPath := filepath.Join(installPath, detectedVersion) + cmdproxy.Run(exec.CommandContext(ctx, execPath, cmdArgs...), conf.GithubActions) +} +func ExecPath(installPath string, version string, execName string, displayer loghelper.Displayer) string { + versionPath := filepath.Join(installPath, version) lastuse.WriteNow(versionPath, displayer) - cmdproxy.Run(filepath.Join(versionPath, execName), cmdArgs, gha) + return filepath.Join(versionPath, execName) } diff --git a/versionmanager/retriever/atmos/atmosretriever.go b/versionmanager/retriever/atmos/atmosretriever.go index 40e5137f..fecc52a2 100644 --- a/versionmanager/retriever/atmos/atmosretriever.go +++ b/versionmanager/retriever/atmos/atmosretriever.go @@ -19,6 +19,7 @@ package atmosretriever import ( + "context" "net/url" "os" "path/filepath" @@ -50,7 +51,7 @@ func Make(conf *config.Config) AtmosRetriever { return AtmosRetriever{conf: conf} } -func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) error { +func (r AtmosRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error { err := r.conf.InitRemoteConf() if err != nil { return err @@ -80,7 +81,7 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, fileName, shaFileName) case config.ModeAPI: - assetURLs, err = github.AssetDownloadURL(tag, []string{fileName, shaFileName}, r.conf.Atmos.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) + assetURLs, err = github.AssetDownloadURL(ctx, tag, []string{fileName, shaFileName}, r.conf.Atmos.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) default: return config.ErrInstallMode } @@ -95,12 +96,12 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err } ro := config.GetBasicAuthOption(config.AtmosRemoteUserEnvName, config.AtmosRemotePassEnvName) - data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...) + data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...) if err != nil { return err } - dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...) + dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...) if err != nil { return err } @@ -117,7 +118,7 @@ func (r AtmosRetriever) InstallRelease(versionStr string, targetPath string) err return os.WriteFile(filepath.Join(targetPath, winbin.GetBinaryName(cmdconst.AtmosName)), data, 0o755) } -func (r AtmosRetriever) ListReleases() ([]string, error) { +func (r AtmosRetriever) ListReleases(ctx context.Context) ([]string, error) { err := r.conf.InitRemoteConf() if err != nil { return nil, err @@ -135,11 +136,11 @@ func (r AtmosRetriever) ListReleases() ([]string, error) { r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL) - return htmlretriever.ListReleases(baseURL, r.conf.Atmos.Data, ro) + return htmlretriever.ListReleases(ctx, baseURL, r.conf.Atmos.Data, ro) case config.ModeAPI: r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL) - return github.ListReleases(listURL, r.conf.GithubToken) + return github.ListReleases(ctx, listURL, r.conf.GithubToken) default: return nil, config.ErrListMode } diff --git a/versionmanager/retriever/html/htmlretriever.go b/versionmanager/retriever/html/htmlretriever.go index 4bc71b51..ee733761 100644 --- a/versionmanager/retriever/html/htmlretriever.go +++ b/versionmanager/retriever/html/htmlretriever.go @@ -19,6 +19,7 @@ package htmlretriever import ( + "context" "net/url" "github.com/PuerkitoBio/goquery" @@ -37,12 +38,12 @@ func BuildAssetURLs(baseAssetURL string, assetNames ...string) ([]string, error) return download.ApplyUrlTranformer(joinTransformer, assetNames...) } -func ListReleases(baseURL string, remoteConf map[string]string, ro []download.RequestOption) ([]string, error) { +func ListReleases(ctx context.Context, baseURL string, remoteConf map[string]string, ro []download.RequestOption) ([]string, error) { selector := config.MapGetDefault(remoteConf, "selector", "a") extractor := htmlquery.SelectionExtractor(config.MapGetDefault(remoteConf, "part", "href")) versionExtractor := func(s *goquery.Selection) string { return versionfinder.Find(extractor(s)) } - return htmlquery.Request(baseURL, selector, versionExtractor, ro...) + return htmlquery.Request(ctx, baseURL, selector, versionExtractor, ro...) } diff --git a/versionmanager/retriever/terraform/terraformretriever.go b/versionmanager/retriever/terraform/terraformretriever.go index 4b4984a4..c7d4acbd 100644 --- a/versionmanager/retriever/terraform/terraformretriever.go +++ b/versionmanager/retriever/terraform/terraformretriever.go @@ -19,6 +19,7 @@ package terraformretriever import ( + "context" "net/url" "os" "runtime" @@ -54,7 +55,7 @@ func Make(conf *config.Config) TerraformRetriever { return TerraformRetriever{conf: conf} } -func (r TerraformRetriever) InstallRelease(version string, targetPath string) error { +func (r TerraformRetriever) InstallRelease(ctx context.Context, version string, targetPath string) error { err := r.conf.InitRemoteConf() if err != nil { return err @@ -94,7 +95,7 @@ func (r TerraformRetriever) InstallRelease(version string, targetPath string) er r.conf.Displayer.Display(apimsg.MsgFetchRelease + versionUrl) - value, err := download.JSON(versionUrl, download.NoDisplay, ro...) + value, err := download.JSON(ctx, versionUrl, download.NoDisplay, ro...) if err != nil { return err } @@ -124,19 +125,19 @@ func (r TerraformRetriever) InstallRelease(version string, targetPath string) er return err } - data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...) + data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...) if err != nil { return err } - if err = r.checkSumAndSig(fileName, data, assetURLs[1], assetURLs[2], ro); err != nil { + if err = r.checkSumAndSig(ctx, fileName, data, assetURLs[1], assetURLs[2], ro); err != nil { return err } return zip.UnzipToDir(data, targetPath, pathfilter.NameEqual(winbin.GetBinaryName(cmdconst.TerraformName))) } -func (r TerraformRetriever) ListReleases() ([]string, error) { +func (r TerraformRetriever) ListReleases(ctx context.Context) ([]string, error) { err := r.conf.InitRemoteConf() if err != nil { return nil, err @@ -153,7 +154,7 @@ func (r TerraformRetriever) ListReleases() ([]string, error) { case config.ListModeHTML: r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL) - return htmlretriever.ListReleases(baseURL, r.conf.Tf.Data, ro) + return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tf.Data, ro) case config.ModeAPI: releasesURL, err := url.JoinPath(baseURL, indexJson) //nolint if err != nil { @@ -162,7 +163,7 @@ func (r TerraformRetriever) ListReleases() ([]string, error) { r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + releasesURL) - value, err := download.JSON(releasesURL, download.NoDisplay, ro...) + value, err := download.JSON(ctx, releasesURL, download.NoDisplay, ro...) if err != nil { return nil, err } @@ -173,8 +174,8 @@ func (r TerraformRetriever) ListReleases() ([]string, error) { } } -func (r TerraformRetriever) checkSumAndSig(fileName string, data []byte, downloadSumsURL string, downloadSumsSigURL string, ro []download.RequestOption) error { - dataSums, err := download.Bytes(downloadSumsURL, r.conf.Displayer.Display, ro...) +func (r TerraformRetriever) checkSumAndSig(ctx context.Context, fileName string, data []byte, downloadSumsURL string, downloadSumsSigURL string, ro []download.RequestOption) error { + dataSums, err := download.Bytes(ctx, downloadSumsURL, r.conf.Displayer.Display, ro...) if err != nil { return err } @@ -187,14 +188,14 @@ func (r TerraformRetriever) checkSumAndSig(fileName string, data []byte, downloa return nil } - dataSumsSig, err := download.Bytes(downloadSumsSigURL, r.conf.Displayer.Display, ro...) + dataSumsSig, err := download.Bytes(ctx, downloadSumsSigURL, r.conf.Displayer.Display, ro...) if err != nil { return err } var dataPublicKey []byte if r.conf.TfKeyPath == "" { - dataPublicKey, err = download.Bytes(publicKeyURL, r.conf.Displayer.Display) + dataPublicKey, err = download.Bytes(ctx, publicKeyURL, r.conf.Displayer.Display) } else { dataPublicKey, err = os.ReadFile(r.conf.TfKeyPath) } diff --git a/versionmanager/retriever/terragrunt/terragruntretriever.go b/versionmanager/retriever/terragrunt/terragruntretriever.go index 9f876ad7..95a26a3f 100644 --- a/versionmanager/retriever/terragrunt/terragruntretriever.go +++ b/versionmanager/retriever/terragrunt/terragruntretriever.go @@ -19,6 +19,7 @@ package terragruntretriever import ( + "context" "net/url" "os" "path/filepath" @@ -50,7 +51,7 @@ func Make(conf *config.Config) TerragruntRetriever { return TerragruntRetriever{conf: conf} } -func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string) error { +func (r TerragruntRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error { err := r.conf.InitRemoteConf() if err != nil { return err @@ -77,7 +78,7 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, fileName, shaFileName) case config.ModeAPI: - assetURLs, err = github.AssetDownloadURL(tag, []string{fileName, shaFileName}, r.conf.Tg.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) + assetURLs, err = github.AssetDownloadURL(ctx, tag, []string{fileName, shaFileName}, r.conf.Tg.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) default: return config.ErrInstallMode } @@ -92,12 +93,12 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string } ro := config.GetBasicAuthOption(config.TgRemoteUserEnvName, config.TgRemotePassEnvName) - data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...) + data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...) if err != nil { return err } - dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...) + dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...) if err != nil { return err } @@ -114,7 +115,7 @@ func (r TerragruntRetriever) InstallRelease(versionStr string, targetPath string return os.WriteFile(filepath.Join(targetPath, winbin.GetBinaryName(cmdconst.TerragruntName)), data, 0o755) } -func (r TerragruntRetriever) ListReleases() ([]string, error) { +func (r TerragruntRetriever) ListReleases(ctx context.Context) ([]string, error) { err := r.conf.InitRemoteConf() if err != nil { return nil, err @@ -132,11 +133,11 @@ func (r TerragruntRetriever) ListReleases() ([]string, error) { r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL) - return htmlretriever.ListReleases(baseURL, r.conf.Tg.Data, ro) + return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tg.Data, ro) case config.ModeAPI: r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL) - return github.ListReleases(listURL, r.conf.GithubToken) + return github.ListReleases(ctx, listURL, r.conf.GithubToken) default: return nil, config.ErrListMode } diff --git a/versionmanager/retriever/tofu/tofuretriever.go b/versionmanager/retriever/tofu/tofuretriever.go index 68fbc987..64e5243e 100644 --- a/versionmanager/retriever/tofu/tofuretriever.go +++ b/versionmanager/retriever/tofu/tofuretriever.go @@ -19,6 +19,7 @@ package tofuretriever import ( + "context" "net/url" "os" "runtime" @@ -67,7 +68,7 @@ func Make(conf *config.Config) TofuRetriever { return TofuRetriever{conf: conf} } -func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) error { +func (r TofuRetriever) InstallRelease(ctx context.Context, versionStr string, targetPath string) error { err := r.conf.InitRemoteConf() if err != nil { return err @@ -103,7 +104,7 @@ func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) erro assetURLs, err = htmlretriever.BuildAssetURLs(baseAssetURL, assetNames...) case config.ModeAPI: - assetURLs, err = github.AssetDownloadURL(tag, assetNames, r.conf.Tofu.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) + assetURLs, err = github.AssetDownloadURL(ctx, tag, assetNames, r.conf.Tofu.GetRemoteURL(), r.conf.GithubToken, r.conf.Displayer.Display) case modeMirroring: urlTemplate := os.Getenv(config.TofuURLTemplateEnvName) if urlTemplate == "" { @@ -130,19 +131,19 @@ func (r TofuRetriever) InstallRelease(versionStr string, targetPath string) erro } ro := config.GetBasicAuthOption(config.TofuRemoteUserEnvName, config.TofuRemotePassEnvName) - data, err := download.Bytes(assetURLs[0], r.conf.Displayer.Display, ro...) + data, err := download.Bytes(ctx, assetURLs[0], r.conf.Displayer.Display, ro...) if err != nil { return err } - if err = r.checkSumAndSig(v, stable, data, assetNames[0], assetURLs, ro); err != nil { + if err = r.checkSumAndSig(ctx, v, stable, data, assetNames[0], assetURLs, ro); err != nil { return err } return zip.UnzipToDir(data, targetPath, pathfilter.NameEqual(winbin.GetBinaryName(cmdconst.TofuName))) } -func (r TofuRetriever) ListReleases() ([]string, error) { +func (r TofuRetriever) ListReleases(ctx context.Context) ([]string, error) { err := r.conf.InitRemoteConf() if err != nil { return nil, err @@ -160,11 +161,11 @@ func (r TofuRetriever) ListReleases() ([]string, error) { r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + baseURL) - return htmlretriever.ListReleases(baseURL, r.conf.Tofu.Data, ro) + return htmlretriever.ListReleases(ctx, baseURL, r.conf.Tofu.Data, ro) case config.ModeAPI: r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL) - return github.ListReleases(listURL, r.conf.GithubToken) + return github.ListReleases(ctx, listURL, r.conf.GithubToken) case modeMirroring: if listURL == config.DefaultTofuGithubURL { listURL = defaultTofuMirroringURL @@ -172,7 +173,7 @@ func (r TofuRetriever) ListReleases() ([]string, error) { r.conf.Displayer.Display(apimsg.MsgFetchAllReleases + listURL) - value, err := download.JSON(listURL, download.NoDisplay, ro...) + value, err := download.JSON(ctx, listURL, download.NoDisplay, ro...) if err != nil { return nil, err } @@ -183,8 +184,8 @@ func (r TofuRetriever) ListReleases() ([]string, error) { } } -func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, data []byte, fileName string, assetURLs []string, ro []download.RequestOption) error { - dataSums, err := download.Bytes(assetURLs[1], r.conf.Displayer.Display, ro...) +func (r TofuRetriever) checkSumAndSig(ctx context.Context, version *version.Version, stable bool, data []byte, fileName string, assetURLs []string, ro []download.RequestOption) error { + dataSums, err := download.Bytes(ctx, assetURLs[1], r.conf.Displayer.Display, ro...) if err != nil { return err } @@ -197,12 +198,12 @@ func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, dat return nil } - dataSumsSig, err := download.Bytes(assetURLs[3], r.conf.Displayer.Display, ro...) + dataSumsSig, err := download.Bytes(ctx, assetURLs[3], r.conf.Displayer.Display, ro...) if err != nil { return err } - dataSumsCert, err := download.Bytes(assetURLs[2], r.conf.Displayer.Display, ro...) + dataSumsCert, err := download.Bytes(ctx, assetURLs[2], r.conf.Displayer.Display, ro...) if err != nil { return err } @@ -221,14 +222,14 @@ func (r TofuRetriever) checkSumAndSig(version *version.Version, stable bool, dat r.conf.Displayer.Display("cosign executable not found, fallback to pgp check") - dataSumsSig, err = download.Bytes(assetURLs[4], r.conf.Displayer.Display, ro...) + dataSumsSig, err = download.Bytes(ctx, assetURLs[4], r.conf.Displayer.Display, ro...) if err != nil { return err } var dataPublicKey []byte if r.conf.TofuKeyPath == "" { - dataPublicKey, err = download.Bytes(publicKeyURL, r.conf.Displayer.Display) + dataPublicKey, err = download.Bytes(ctx, publicKeyURL, r.conf.Displayer.Display) } else { dataPublicKey, err = os.ReadFile(r.conf.TofuKeyPath) } diff --git a/versionmanager/tenvlib/examples/dtofuver/ex1.go b/versionmanager/tenvlib/examples/dtofuver/ex1.go new file mode 100644 index 00000000..17fe3058 --- /dev/null +++ b/versionmanager/tenvlib/examples/dtofuver/ex1.go @@ -0,0 +1,23 @@ +package main + +import ( + "context" + "fmt" + + "github.com/tofuutils/tenv/v3/config/cmdconst" + "github.com/tofuutils/tenv/v3/versionmanager/tenvlib" +) + +func main() { + tenv, err := tenvlib.Make(tenvlib.AutoInstall, tenvlib.IgnoreEnv, tenvlib.DisableDisplay) + if err != nil { + fmt.Println("init failed :", err) + + return + } + + err = tenv.DetectedCommandProxy(context.Background(), cmdconst.TofuName, "version") + if err != nil { + fmt.Println("proxy call failed :", err) + } +} diff --git a/versionmanager/tenvlib/examples/ltfver/ex2.go b/versionmanager/tenvlib/examples/ltfver/ex2.go new file mode 100644 index 00000000..69eb6ced --- /dev/null +++ b/versionmanager/tenvlib/examples/ltfver/ex2.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "fmt" + + "github.com/tofuutils/tenv/v3/config" + "github.com/tofuutils/tenv/v3/config/cmdconst" + "github.com/tofuutils/tenv/v3/versionmanager/semantic" + "github.com/tofuutils/tenv/v3/versionmanager/tenvlib" +) + +func main() { + conf, err := config.DefaultConfig() // does not read environment variables + if err != nil { + fmt.Println("init failed :", err) + + return + } + + conf.SkipInstall = false // tenvlib.AutoInstall option equivalent + + tenv, err := tenvlib.Make(tenvlib.WithConfig(&conf), tenvlib.DisableDisplay) + if err != nil { + fmt.Println("should not occur when calling WithConfig :", err) + + return + } + + ctx := context.Background() + version, err := tenv.Evaluate(ctx, cmdconst.TerraformName, semantic.LatestKey) + if err != nil { + fmt.Println("eval failed :", err) + + return + } + + conf.ForceRemote = true + + remoteVersion, err := tenv.Evaluate(ctx, cmdconst.TerraformName, semantic.LatestKey) + if err != nil { + fmt.Println("eval remote failed :", err) + + return + } + + if version != remoteVersion { + err = tenv.Uninstall(ctx, cmdconst.TerraformName, version) + if err != nil { + fmt.Println("uninstall failed :", err) + } + } + + fmt.Println("Last Terraform version :", version, "(local),", remoteVersion, "(remote)") +} diff --git a/versionmanager/tenvlib/lib.go b/versionmanager/tenvlib/lib.go new file mode 100644 index 00000000..6620a70a --- /dev/null +++ b/versionmanager/tenvlib/lib.go @@ -0,0 +1,353 @@ +/* + * + * Copyright 2024 tofuutils authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +package tenvlib + +import ( + "context" + "errors" + "os/exec" + + "github.com/hashicorp/hcl/v2/hclparse" + + "github.com/tofuutils/tenv/v3/config" + "github.com/tofuutils/tenv/v3/pkg/cmdproxy" + "github.com/tofuutils/tenv/v3/pkg/loghelper" + "github.com/tofuutils/tenv/v3/versionmanager" + "github.com/tofuutils/tenv/v3/versionmanager/builder" + "github.com/tofuutils/tenv/v3/versionmanager/proxy" + "github.com/tofuutils/tenv/v3/versionmanager/semantic" + flatparser "github.com/tofuutils/tenv/v3/versionmanager/semantic/parser/flat" +) + +var errNoBuilder = errors.New("no builder for this tool") + +type tenvConfig struct { + autoInstall bool + builders map[string]builder.BuilderFunc + conf *config.Config + displayer loghelper.Displayer + hclParser *hclparse.Parser + ignoreEnv bool + initConfigFunc func() (config.Config, error) +} + +type TenvOption func(*tenvConfig) + +// add builder or override default builder (see builder.Builders). +func AddTool(toolName string, builderFunc builder.BuilderFunc) TenvOption { + return func(tc *tenvConfig) { + tc.builders[toolName] = builderFunc + } +} + +func AutoInstall(tc *tenvConfig) { + tc.autoInstall = true +} + +func DisableDisplay(tc *tenvConfig) { + tc.displayer = loghelper.InertDisplayer +} + +func IgnoreEnv(tc *tenvConfig) { + tc.ignoreEnv = true +} + +func WithConfig(conf *config.Config) TenvOption { + return func(tc *tenvConfig) { + tc.conf = conf + } +} + +func WithDisplayer(displayer loghelper.Displayer) TenvOption { + return func(tc *tenvConfig) { + tc.displayer = displayer + } +} + +func WithHCLParser(hclParser *hclparse.Parser) TenvOption { + return func(tc *tenvConfig) { + tc.hclParser = hclParser + } +} + +// Not concurrent safe. +type Tenv struct { + builders map[string]builder.BuilderFunc + conf *config.Config + hclParser *hclparse.Parser + ignoreEnv bool + managers map[string]versionmanager.VersionManager +} + +// The returned wrapper is not concurrent safe. +func Make(options ...TenvOption) (Tenv, error) { + builders := map[string]builder.BuilderFunc{} + for toolName, builderFunc := range builder.Builders { + builders[toolName] = builderFunc + } + + tc := tenvConfig{ + builders: builders, + initConfigFunc: config.InitConfigFromEnv, + } + + for _, option := range options { + option(&tc) + } + + if tc.ignoreEnv { + tc.initConfigFunc = config.DefaultConfig + } + + if tc.conf == nil { + conf, err := tc.initConfigFunc() + if err != nil { + return Tenv{}, err + } + + tc.conf = &conf + } + + if tc.autoInstall { + tc.conf.SkipInstall = false + } + + if tc.displayer == nil { + tc.conf.InitDisplayer(false) + } else { + tc.conf.Displayer = tc.displayer + } + + if tc.hclParser == nil { + tc.hclParser = hclparse.NewParser() + } + + return Tenv{ + builders: builders, + conf: tc.conf, + hclParser: tc.hclParser, + ignoreEnv: tc.ignoreEnv, + managers: map[string]versionmanager.VersionManager{}, + }, nil +} + +// return an exec.Cmd in order to call the specified tool version (need to have it installed for the Cmd call to work). +func (t Tenv) Command(ctx context.Context, toolName string, requestedVersion string, cmdArgs ...string) (*exec.Cmd, error) { + err := t.init(toolName) + if err != nil { + return nil, err + } + + installPath, err := t.managers[toolName].InstallPath() + if err != nil { + return nil, err + } + + execPath := proxy.ExecPath(installPath, requestedVersion, toolName, t.conf.Displayer) + + return exec.CommandContext(ctx, execPath, cmdArgs...), nil +} + +// Use the result of Tenv.Command to call cmdproxy.Run (Always call os.Exit). +func (t Tenv) CommandProxy(ctx context.Context, toolName string, requestedVersion string, cmdArgs ...string) error { + cmd, err := t.Command(ctx, toolName, requestedVersion, cmdArgs...) + if err != nil { + return err + } + + cmdproxy.Run(cmd, t.conf.GithubActions) + + return nil +} + +// Detect version (resolve and evaluate, can install depending on configuration). +func (t Tenv) Detect(ctx context.Context, toolName string) (string, error) { + err := t.init(toolName) + if err != nil { + return "", err + } + + manager := t.managers[toolName] + if !t.ignoreEnv { + return manager.Detect(ctx, false) + } + + resolvedVersion, err := manager.ResolveWithVersionFiles() + if err != nil { + return "", err + } + + if resolvedVersion != "" { + return manager.Evaluate(ctx, resolvedVersion, false) + } + + resolvedVersion, err = flatparser.RetrieveVersion(manager.RootVersionFilePath(), t.conf) + if err != nil { + return "", err + } + + if resolvedVersion == "" { + resolvedVersion = semantic.LatestAllowedKey + } + + return manager.Evaluate(ctx, resolvedVersion, false) +} + +// Use the result of Tenv.Detect to call Tenv.Command. +func (t Tenv) DetectedCommand(ctx context.Context, toolName string, cmdArgs ...string) (*exec.Cmd, error) { + detectedVersion, err := t.Detect(ctx, toolName) + if err != nil { + return nil, err + } + + // t.managers[toolName] is initialized by Tenv.Detect + installPath, err := t.managers[toolName].InstallPath() + if err != nil { + return nil, err + } + + execPath := proxy.ExecPath(installPath, detectedVersion, toolName, t.conf.Displayer) + + return exec.CommandContext(ctx, execPath, cmdArgs...), nil +} + +// Use the result of Tenv.DetectedCommand to call cmdproxy.Run (Always call os.Exit). +func (t Tenv) DetectedCommandProxy(ctx context.Context, toolName string, cmdArgs ...string) error { + cmd, err := t.DetectedCommand(ctx, toolName, cmdArgs...) + if err != nil { + return err + } + + cmdproxy.Run(cmd, t.conf.GithubActions) + + return nil +} + +// Evaluate version resolution strategy or version constraint (can install depending on configuration). +func (t Tenv) Evaluate(ctx context.Context, toolName string, requestedVersion string) (string, error) { + if err := t.init(toolName); err != nil { + return "", err + } + + return t.managers[toolName].Evaluate(ctx, requestedVersion, false) +} + +func (t Tenv) Install(ctx context.Context, toolName string, requestedVersion string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].Install(ctx, requestedVersion) +} + +func (t Tenv) InstallMultiple(ctx context.Context, toolName string, versions []string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].InstallMultiple(ctx, versions) +} + +func (t Tenv) ListLocal(ctx context.Context, toolName string, reverseOrder bool) ([]versionmanager.DatedVersion, error) { + if err := t.init(toolName); err != nil { + return nil, err + } + + return t.managers[toolName].ListLocal(reverseOrder) +} + +func (t Tenv) ListRemote(ctx context.Context, toolName string, reverseOrder bool) ([]string, error) { + if err := t.init(toolName); err != nil { + return nil, err + } + + return t.managers[toolName].ListRemote(ctx, reverseOrder) +} + +func (t Tenv) LocallyInstalled(ctx context.Context, toolName string) (map[string]struct{}, error) { + if err := t.init(toolName); err != nil { + return nil, err + } + + return t.managers[toolName].LocalSet(), nil +} + +func (t Tenv) ResetDefaultConstraint(ctx context.Context, toolName string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].ResetConstraint() +} + +func (t Tenv) ResetDefaultVersion(ctx context.Context, toolName string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].ResetVersion() +} + +func (t Tenv) SetDefaultConstraint(ctx context.Context, toolName string, constraint string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].SetConstraint(constraint) +} + +func (t Tenv) SetDefaultVersion(ctx context.Context, toolName string, requestedVersion string, workingDir bool) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].Use(ctx, requestedVersion, workingDir) +} + +// Does not handle special behavior. +func (t Tenv) Uninstall(ctx context.Context, toolName string, requestedVersion string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].UninstallMultiple([]string{requestedVersion}) +} + +func (t Tenv) UninstallMultiple(ctx context.Context, toolName string, versions []string) error { + if err := t.init(toolName); err != nil { + return err + } + + return t.managers[toolName].UninstallMultiple(versions) +} + +func (t Tenv) init(toolName string) error { + if _, ok := t.managers[toolName]; ok { + return nil + } + + builderFunc := t.builders[toolName] + if builderFunc == nil { + return errNoBuilder + } + + t.managers[toolName] = builderFunc(t.conf, t.hclParser) + + return nil +}