diff --git a/CHANGELOG.md b/CHANGELOG.md index 55586886b..58f5b1754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. allow find WAL files inside nested subdirectories. - `tt search tcm` - the command performs a search for tarantool cluster manager (TCM) in the customer zone or local `distfiles` directory. +- `tt install tcm` - the command performs an install for tarantool cluster + manager (TCM) from the customer zone or local `distfiles` directory. - `tt tcm status`: added command to check TCM runtime status (modes: `watchdog` or `interactive`). - `tt tcm stop`: add command for graceful termination of TCM processes (modes: `watchdog` or `interactive`). @@ -766,4 +768,4 @@ Additionally, several fixes were implemented to improve stability. - Module ``tt create``, to create an application from a template. - Module ``tt build``, to build an application. - Module ``tt install``, to install tarantool/tt. -- Module ``tt remove``, to remove tarantool/tt. \ No newline at end of file +- Module ``tt remove``, to remove tarantool/tt. diff --git a/cli/binary/list.go b/cli/binary/list.go index 3df85e2e4..4aeab1b39 100644 --- a/cli/binary/list.go +++ b/cli/binary/list.go @@ -31,19 +31,16 @@ func printVersion(versionString string) { } // ParseBinaries seeks through fileList returning array of found versions of program. -func ParseBinaries(fileList []fs.DirEntry, programName string, +func ParseBinaries(fileList []fs.DirEntry, program search.ProgramType, binDir string) ([]version.Version, error) { var binaryVersions []version.Version - symlinkName := programName - if programName == search.ProgramEe || programName == search.ProgramDev { - symlinkName = search.ProgramCe - } + symlinkName := program.Exec() binActive := "" programPath := filepath.Join(binDir, symlinkName) if fileInfo, err := os.Lstat(programPath); err == nil { - if programName == search.ProgramDev && + if program == search.ProgramDev && fileInfo.Mode()&os.ModeSymlink == os.ModeSymlink { binActive, isTarantoolBinary, err := install.IsTarantoolDev(programPath, binDir) if err != nil { @@ -51,10 +48,10 @@ func ParseBinaries(fileList []fs.DirEntry, programName string, } if isTarantoolBinary { binaryVersions = append(binaryVersions, - version.Version{Str: programName + " -> " + binActive + " [active]"}) + version.Version{Str: program.String() + " -> " + binActive + " [active]"}) } return binaryVersions, nil - } else if programName == search.ProgramCe && fileInfo.Mode()&os.ModeSymlink == 0 { + } else if program == search.ProgramCe && fileInfo.Mode()&os.ModeSymlink == 0 { tntCli := cmdcontext.TarantoolCli{Executable: programPath} binaryVersion, err := tntCli.GetVersion() if err != nil { @@ -71,7 +68,7 @@ func ParseBinaries(fileList []fs.DirEntry, programName string, } } - versionPrefix := programName + version.FsSeparator + versionPrefix := program.String() + version.FsSeparator var err error for _, f := range fileList { if strings.HasPrefix(f.Name(), versionPrefix) { @@ -111,22 +108,22 @@ func ListBinaries(cmdCtx *cmdcontext.CmdCtx, cliOpts *config.CliOpts) (err error return fmt.Errorf("error reading directory %q: %s", binDir, err) } - programs := [...]string{ + programs := [...]search.ProgramType{ search.ProgramTt, search.ProgramCe, search.ProgramDev, search.ProgramEe, } fmt.Println("List of installed binaries:") - for _, programName := range programs { - binaryVersions, err := ParseBinaries(binDirFilesList, programName, binDir) + for _, program := range programs { + binaryVersions, err := ParseBinaries(binDirFilesList, program, binDir) if err != nil { return err } if len(binaryVersions) > 0 { sort.Stable(sort.Reverse(version.VersionSlice(binaryVersions))) - log.Infof(programName + ":") + log.Infof(program.String() + ":") for _, binVersion := range binaryVersions { printVersion(binVersion.Str) } diff --git a/cli/binary/list_test.go b/cli/binary/list_test.go index 5067eab47..fb7a4172e 100644 --- a/cli/binary/list_test.go +++ b/cli/binary/list_test.go @@ -11,13 +11,14 @@ import ( "github.com/otiai10/copy" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/tarantool/tt/cli/search" "github.com/tarantool/tt/cli/version" ) func TestParseBinaries(t *testing.T) { fileList, err := os.ReadDir("./testdata/bin") require.NoError(t, err) - versions, err := ParseBinaries(fileList, "tarantool", "./testdata/bin") + versions, err := ParseBinaries(fileList, search.ProgramCe, "./testdata/bin") require.NoError(t, err) sort.Stable(sort.Reverse(version.VersionSlice(versions))) expectedSortedVersions := []string{"master", "2.10.5", "2.8.6 [active]", "1.10.0", "0000000"} @@ -33,7 +34,7 @@ func TestParseBinariesTarantoolDev(t *testing.T) { testDir := fmt.Sprintf("./testdata/tarantool_dev/%s", dir) fileList, err := os.ReadDir(testDir) assert.NoError(t, err) - versions, err := ParseBinaries(fileList, "tarantool-dev", testDir) + versions, err := ParseBinaries(fileList, search.ProgramDev, testDir) assert.NoError(t, err) require.Equal(t, 1, len(versions)) version := versions[0].Str @@ -49,7 +50,7 @@ func TestParseBinariesNoSymlink(t *testing.T) { fileList, err := os.ReadDir(tmpDir) require.NoError(t, err) - versions, err := ParseBinaries(fileList, "tarantool", tmpDir) + versions, err := ParseBinaries(fileList, search.ProgramCe, tmpDir) require.NoError(t, err) sort.Stable(sort.Reverse(version.VersionSlice(versions))) expectedSortedVersions := []string{"3.1.0-entrypoint-83-gcb0264c3c [active]", "2.10.1"} @@ -57,7 +58,7 @@ func TestParseBinariesNoSymlink(t *testing.T) { for i := 0; i < len(expectedSortedVersions); i++ { assert.Equal(t, expectedSortedVersions[i], versions[i].Str) } - versions, err = ParseBinaries(fileList, "tarantool-ee", tmpDir) + versions, err = ParseBinaries(fileList, search.ProgramEe, tmpDir) require.NoError(t, err) sort.Stable(sort.Reverse(version.VersionSlice(versions))) expectedSortedVersions = []string{"2.11.1"} @@ -68,7 +69,7 @@ func TestParseBinariesNoSymlink(t *testing.T) { // Tarantool exists, but not executable. require.NoError(t, os.Chmod(filepath.Join(tmpDir, "tarantool"), 0440)) - versions, err = ParseBinaries(fileList, "tarantool-ee", tmpDir) + versions, err = ParseBinaries(fileList, search.ProgramEe, tmpDir) require.NoError(t, err) sort.Stable(sort.Reverse(version.VersionSlice(versions))) expectedSortedVersions = []string{"2.11.1"} @@ -78,6 +79,6 @@ func TestParseBinariesNoSymlink(t *testing.T) { } require.NoError(t, os.Chmod(filepath.Join(tmpDir, "tarantool"), 0440)) - _, err = ParseBinaries(fileList, "tarantool", tmpDir) + _, err = ParseBinaries(fileList, search.ProgramCe, tmpDir) require.ErrorContains(t, err, "failed to get tarantool version") } diff --git a/cli/binary/switch.go b/cli/binary/switch.go index 8172cc0f0..6deaf4db7 100644 --- a/cli/binary/switch.go +++ b/cli/binary/switch.go @@ -23,8 +23,8 @@ type SwitchCtx struct { BinDir string // IncDir is a directory witch stores include files. IncDir string - // ProgramName is a program name to switch to. - ProgramName string + // ProgramType is a program name to switch to. + ProgramType search.ProgramType // Version of the program to switch to. Version string } @@ -39,7 +39,7 @@ func cleanString(str string) string { } // ChooseProgram shows a menu in terminal to choose program for switch. -func ChooseProgram(supportedPrograms []string) (string, error) { +func ChooseProgram(supportedPrograms []string) (search.ProgramType, error) { programSelect := promptui.Select{ Label: "Select program", Items: supportedPrograms, @@ -47,11 +47,11 @@ func ChooseProgram(supportedPrograms []string) (string, error) { } _, program, err := programSelect.Run() - return program, err + return search.NewProgramType(program), err } // ChooseVersion shows a menu in terminal to choose version of program to switch to. -func ChooseVersion(binDir string, programName string) (string, error) { +func ChooseVersion(binDir string, program search.ProgramType) (string, error) { binDirFilesList, err := os.ReadDir(binDir) if len(binDirFilesList) == 0 || errors.Is(err, fs.ErrNotExist) { @@ -59,12 +59,12 @@ func ChooseVersion(binDir string, programName string) (string, error) { } else if err != nil { return "", fmt.Errorf("error reading directory %q: %s", binDir, err) } - versions, err := ParseBinaries(binDirFilesList, programName, binDir) + versions, err := ParseBinaries(binDirFilesList, program, binDir) if err != nil { return "", err } if len(versions) == 0 { - return "", fmt.Errorf("there are no %s installed in this environment of 'tt'", programName) + return "", fmt.Errorf("there are no %s installed in this environment of 'tt'", program) } var versionStr []string for _, version := range versions { @@ -90,13 +90,13 @@ func ChooseVersion(binDir string, programName string) (string, error) { // switchTt switches 'tt' program. func switchTt(switchCtx SwitchCtx) error { - log.Infof("Switching to %s %s.", switchCtx.ProgramName, switchCtx.Version) + log.Infof("Switching to %s %s.", switchCtx.ProgramType, switchCtx.Version) ttVersion := switchCtx.Version if !strings.HasPrefix(switchCtx.Version, "v") { ttVersion = "v" + ttVersion } - versionStr := search.ProgramTt + version.FsSeparator + ttVersion + versionStr := search.ProgramTt.String() + version.FsSeparator + ttVersion if util.IsRegularFile(filepath.Join(switchCtx.BinDir, versionStr)) { err := util.CreateSymlink(versionStr, filepath.Join(switchCtx.BinDir, "tt"), true) @@ -106,19 +106,19 @@ func switchTt(switchCtx SwitchCtx) error { log.Infof("Done") } else { return fmt.Errorf("%s %s is not installed in current environment", - switchCtx.ProgramName, switchCtx.Version) + switchCtx.ProgramType, switchCtx.Version) } return nil } // switchTarantool switches 'tarantool' program. func switchTarantool(switchCtx SwitchCtx, enterprise bool) error { - log.Infof("Switching to %s %s.", switchCtx.ProgramName, switchCtx.Version) + log.Infof("Switching to %s %s.", switchCtx.ProgramType, switchCtx.Version) var versionStr string if enterprise { - versionStr = search.ProgramEe + version.FsSeparator + switchCtx.Version + versionStr = search.ProgramEe.String() + version.FsSeparator + switchCtx.Version } else { - versionStr = search.ProgramCe + version.FsSeparator + switchCtx.Version + versionStr = search.ProgramCe.String() + version.FsSeparator + switchCtx.Version } if util.IsRegularFile(filepath.Join(switchCtx.BinDir, versionStr)) && util.IsDir(filepath.Join(switchCtx.IncDir, "include", versionStr)) { @@ -135,7 +135,7 @@ func switchTarantool(switchCtx SwitchCtx, enterprise bool) error { log.Infof("Done") } else { return fmt.Errorf("%s %s is not installed in current environment", - switchCtx.ProgramName, switchCtx.Version) + switchCtx.ProgramType, switchCtx.Version) } return nil } @@ -144,7 +144,7 @@ func switchTarantool(switchCtx SwitchCtx, enterprise bool) error { func Switch(switchCtx SwitchCtx) error { var err error - switch switchCtx.ProgramName { + switch switchCtx.ProgramType { case search.ProgramTt: err = switchTt(switchCtx) case search.ProgramCe: @@ -152,7 +152,7 @@ func Switch(switchCtx SwitchCtx) error { case search.ProgramEe: err = switchTarantool(switchCtx, true) default: - return fmt.Errorf("unknown application: %s", switchCtx.ProgramName) + return fmt.Errorf("unknown application: %s", switchCtx.ProgramType) } return err diff --git a/cli/binary/switch_test.go b/cli/binary/switch_test.go index 8238c5155..d14a9ef1a 100644 --- a/cli/binary/switch_test.go +++ b/cli/binary/switch_test.go @@ -8,6 +8,7 @@ import ( "github.com/fatih/color" "github.com/otiai10/copy" "github.com/stretchr/testify/assert" + "github.com/tarantool/tt/cli/search" "github.com/tarantool/tt/cli/util" ) @@ -30,7 +31,7 @@ func TestSwitchTarantool(t *testing.T) { var testCtx SwitchCtx testCtx.IncDir = filepath.Join(tempDir, "include") testCtx.BinDir = filepath.Join(tempDir, "bin") - testCtx.ProgramName = "tarantool" + testCtx.ProgramType = search.NewProgramType("tarantool") testCtx.Version = "2.10.3" err = Switch(testCtx) assert.Nil(t, err) @@ -48,17 +49,17 @@ func TestSwitchUnknownProgram(t *testing.T) { var testCtx SwitchCtx testCtx.IncDir = filepath.Join(".", "include") testCtx.BinDir = filepath.Join(".", "bin") - testCtx.ProgramName = "tarantool-foo" + testCtx.ProgramType = search.NewProgramType("tarantool-foo") testCtx.Version = "2.10.3" err := Switch(testCtx) - assert.Equal(t, err.Error(), "unknown application: tarantool-foo") + assert.Equal(t, err.Error(), "unknown application: unknown(0)") } func TestSwitchNotInstalledVersion(t *testing.T) { var testCtx SwitchCtx testCtx.IncDir = filepath.Join(".", "include") testCtx.BinDir = filepath.Join(".", "bin") - testCtx.ProgramName = "tarantool" + testCtx.ProgramType = search.NewProgramType("tarantool") testCtx.Version = "2.10.3" err := Switch(testCtx) assert.Equal(t, err.Error(), "tarantool 2.10.3 is not installed in current environment") diff --git a/cli/cmd/binaries.go b/cli/cmd/binaries.go index b28a03883..29aec46b4 100644 --- a/cli/cmd/binaries.go +++ b/cli/cmd/binaries.go @@ -11,18 +11,18 @@ import ( ) var binariesSupportedPrograms = []string{ - search.ProgramCe, - search.ProgramEe, - search.ProgramTt, + search.ProgramCe.String(), + search.ProgramEe.String(), + search.ProgramTt.String(), } // NewBinariesCmd creates binaries command. func NewBinariesCmd() *cobra.Command { - var binariesCmd = &cobra.Command{ + binariesCmd := &cobra.Command{ Use: "binaries", } - var switchCmd = &cobra.Command{ + switchCmd := &cobra.Command{ Use: "switch [program] [version]", Short: "Switch to installed binary", Example: ` @@ -44,7 +44,7 @@ You will need to choose version using arrow keys in your console. Run: RunModuleFunc(internalSwitchModule), Args: cobra.MatchAll(cobra.MaximumNArgs(2), binariesSwitchValidateArgs), } - var listCmd = &cobra.Command{ + listCmd := &cobra.Command{ Use: "list", Short: "Show a list of installed binaries and their versions.", Run: RunModuleFunc(internalListModule), @@ -73,9 +73,9 @@ func internalSwitchModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { var err error if len(args) > 0 { - switchCtx.ProgramName = args[0] + switchCtx.ProgramType = search.NewProgramType(args[0]) } else { - switchCtx.ProgramName, err = binary.ChooseProgram(binariesSupportedPrograms) + switchCtx.ProgramType, err = binary.ChooseProgram(binariesSupportedPrograms) if err != nil { return err } @@ -84,7 +84,7 @@ func internalSwitchModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { if len(args) > 1 { switchCtx.Version = args[1] } else { - switchCtx.Version, err = binary.ChooseVersion(cliOpts.Env.BinDir, switchCtx.ProgramName) + switchCtx.Version, err = binary.ChooseVersion(cliOpts.Env.BinDir, switchCtx.ProgramType) if err != nil { return err } diff --git a/cli/cmd/install.go b/cli/cmd/install.go index 475169365..a2bf2a909 100644 --- a/cli/cmd/install.go +++ b/cli/cmd/install.go @@ -4,6 +4,7 @@ import ( "github.com/spf13/cobra" "github.com/tarantool/tt/cli/cmdcontext" "github.com/tarantool/tt/cli/install" + "github.com/tarantool/tt/cli/search" ) var installCtx install.InstallCtx @@ -11,7 +12,7 @@ var installCtx install.InstallCtx // newInstallTtCmd creates a command to install tt. func newInstallTtCmd() *cobra.Command { var tntCmd = &cobra.Command{ - Use: "tt [version|commit hash|pull-request]", + Use: search.ProgramTt.String() + " [version|commit hash|pull-request]", Short: "Install tt", Run: RunModuleFunc(internalInstallModule), Args: cobra.MaximumNArgs(1), @@ -23,7 +24,7 @@ func newInstallTtCmd() *cobra.Command { // newInstallTarantoolCmd creates a command to install tarantool. func newInstallTarantoolCmd() *cobra.Command { var tntCmd = &cobra.Command{ - Use: "tarantool [version|commit hash|pull-request]", + Use: search.ProgramCe.String() + " [version|commit hash|pull-request]", Short: "Install tarantool community edition", Run: RunModuleFunc(internalInstallModule), Args: cobra.MaximumNArgs(1), @@ -40,7 +41,7 @@ func newInstallTarantoolCmd() *cobra.Command { // newInstallTarantoolEeCmd creates a command to install tarantool-ee. func newInstallTarantoolEeCmd() *cobra.Command { var tntCmd = &cobra.Command{ - Use: "tarantool-ee [version]", + Use: search.ProgramEe.String() + " [version]", Short: "Install tarantool enterprise edition", Run: RunModuleFunc(internalInstallModule), Args: cobra.MaximumNArgs(1), @@ -51,6 +52,19 @@ func newInstallTarantoolEeCmd() *cobra.Command { return tntCmd } +func newInstallTcmCmd() *cobra.Command { + var tntCmd = &cobra.Command{ + Use: search.ProgramTcm.String() + " [version]", + Short: "Install tarantool cluster manager", + Run: RunModuleFunc(internalInstallModule), + Args: cobra.MaximumNArgs(1), + } + + tntCmd.Flags().BoolVar(&installCtx.DevBuild, "dev", false, "install development build") + + return tntCmd +} + // newInstallTarantoolDevCmd creates a command to install tarantool // from the local build directory. func newInstallTarantoolDevCmd() *cobra.Command { @@ -91,7 +105,7 @@ func NewInstallCmd() *cobra.Command { } installCmd.Flags().BoolVarP(&installCtx.Force, "force", "f", false, "don't do a dependency check before installing") - installCmd.Flags().BoolVarP(&installCtx.Noclean, "no-clean", "", false, + installCmd.Flags().BoolVarP(&installCtx.KeepTemp, "no-clean", "", false, "don't delete temporary files") installCmd.Flags().BoolVarP(&installCtx.Reinstall, "reinstall", "", false, "reinstall program") installCmd.Flags().BoolVarP(&installCtx.Local, "local-repo", "", false, @@ -102,6 +116,7 @@ func NewInstallCmd() *cobra.Command { newInstallTarantoolCmd(), newInstallTarantoolEeCmd(), newInstallTarantoolDevCmd(), + newInstallTcmCmd(), ) return installCmd @@ -118,7 +133,6 @@ func internalInstallModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { return err } - err = install.Install(cliOpts.Env.BinDir, cliOpts.Env.IncludeDir, - installCtx, cliOpts.Repo.Install, cliOpts) + err = install.Install(installCtx, cliOpts) return err } diff --git a/cli/cmd/modules.go b/cli/cmd/modules.go index 1673468d1..17d2f50f9 100644 --- a/cli/cmd/modules.go +++ b/cli/cmd/modules.go @@ -21,7 +21,6 @@ func newModulesListCmd() *cobra.Command { Use: "list", Short: "List available modules", Run: func(cmd *cobra.Command, args []string) { - searchCtx.ProgramName = cmd.Name() err := modules.RunCmd(&cmdCtx, cmd.CommandPath(), &modulesInfo, internalModulesList, args) util.HandleCmdErr(cmd, err) diff --git a/cli/cmd/search.go b/cli/cmd/search.go index 54512c1d2..faba7e99b 100644 --- a/cli/cmd/search.go +++ b/cli/cmd/search.go @@ -15,7 +15,7 @@ var ( // newSearchTtCmd creates a command to search tt. func newSearchTtCmd() *cobra.Command { tntCmd := &cobra.Command{ - Use: search.ProgramTt, + Use: search.ProgramTt.String(), Short: "Search for available tt versions", Run: RunModuleFunc(internalSearchModule), Args: cobra.ExactArgs(0), @@ -27,7 +27,7 @@ func newSearchTtCmd() *cobra.Command { // newSearchTarantoolCmd creates a command to search tarantool. func newSearchTarantoolCmd() *cobra.Command { tntCmd := &cobra.Command{ - Use: search.ProgramCe, + Use: search.ProgramCe.String(), Short: "Search for available tarantool community edition versions", Run: RunModuleFunc(internalSearchModule), Args: cobra.ExactArgs(0), @@ -39,7 +39,7 @@ func newSearchTarantoolCmd() *cobra.Command { // newSearchTarantoolEeCmd creates a command to search tarantool-ee. func newSearchTarantoolEeCmd() *cobra.Command { tntCmd := &cobra.Command{ - Use: search.ProgramEe, + Use: search.ProgramEe.String(), Short: "Search for available tarantool enterprise edition versions", Run: RunModuleFunc(internalSearchModule), Args: cobra.ExactArgs(0), @@ -57,7 +57,7 @@ func newSearchTarantoolEeCmd() *cobra.Command { // newSearchTcmCmd creates a command to search tcm. func newSearchTcmCmd() *cobra.Command { tcmCmd := &cobra.Command{ - Use: search.ProgramTcm, + Use: search.ProgramTcm.String(), Short: "Search for available tarantool cluster manager versions", Run: RunModuleFunc(internalSearchModule), Args: cobra.ExactArgs(0), @@ -104,7 +104,7 @@ func NewSearchCmd() *cobra.Command { // internalSearchModule is a default search module. func internalSearchModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { var err error - searchCtx.ProgramName = cmdCtx.CommandName + searchCtx.Program = search.NewProgramType(cmdCtx.CommandName) if local { err = search.SearchVersionsLocal(searchCtx, cliOpts, cmdCtx.Cli.ConfigPath) } else { diff --git a/cli/cmd/uninstall.go b/cli/cmd/uninstall.go index 2839ad00c..9384e68ad 100644 --- a/cli/cmd/uninstall.go +++ b/cli/cmd/uninstall.go @@ -3,6 +3,7 @@ package cmd import ( "github.com/spf13/cobra" "github.com/tarantool/tt/cli/cmdcontext" + "github.com/tarantool/tt/cli/search" "github.com/tarantool/tt/cli/uninstall" ) @@ -30,8 +31,8 @@ func newUninstallTtCmd() *cobra.Command { // newUninstallTarantoolCmd creates a command to install tarantool. func newUninstallTarantoolCmd() *cobra.Command { - var tntCmd = &cobra.Command{ - Use: "tarantool [version]", + tntCmd := &cobra.Command{ + Use: search.ProgramCe.String() + " [version]", Short: "Uninstall tarantool community edition", Run: RunModuleFunc(InternalUninstallModule), Args: cobra.MaximumNArgs(1), @@ -52,8 +53,8 @@ func newUninstallTarantoolCmd() *cobra.Command { // newUninstallTarantoolEeCmd creates a command to install tarantool-ee. func newUninstallTarantoolEeCmd() *cobra.Command { - var tntCmd = &cobra.Command{ - Use: "tarantool-ee [version]", + tntCmd := &cobra.Command{ + Use: search.ProgramEe.String() + " [version]", Short: "Uninstall tarantool enterprise edition", Run: RunModuleFunc(InternalUninstallModule), Args: cobra.MaximumNArgs(1), @@ -111,13 +112,13 @@ func InternalUninstallModule(cmdCtx *cmdcontext.CmdCtx, args []string) error { return errNoConfig } - programName := cmdCtx.CommandName + program := search.NewProgramType(cmdCtx.CommandName) programVersion := "" if len(args) == 1 { programVersion = args[0] } - err := uninstall.UninstallProgram(programName, programVersion, cliOpts.Env.BinDir, + err := uninstall.UninstallProgram(program, programVersion, cliOpts.Env.BinDir, cliOpts.Env.IncludeDir+"/include", cmdCtx) return err } diff --git a/cli/download/download.go b/cli/download/download.go index ef783ec2f..3d1aa6c19 100644 --- a/cli/download/download.go +++ b/cli/download/download.go @@ -23,9 +23,28 @@ type DownloadCtx struct { DevBuild bool } +// searchSDKVersionToDownload wrapper to call search package. +func searchSDKVersionToDownload(downloadCtx DownloadCtx, cliOpts *config.CliOpts) ( + search.BundleInfo, error, +) { + log.Info("Search for the requested version...") + searchCtx := search.NewSearchCtx(search.NewPlatformInformer(), search.NewTntIoDoer()) + searchCtx.Program = search.ProgramEe + searchCtx.Filter = search.SearchAll + searchCtx.Package = "enterprise" + searchCtx.DevBuilds = downloadCtx.DevBuild + + bundles, err := search.FetchBundlesInfo(&searchCtx, cliOpts) + if err != nil { + return search.BundleInfo{}, fmt.Errorf("cannot get SDK bundles list: %s", err) + } + return search.SelectVersion(bundles, downloadCtx.Version) +} + // DownloadSDK Downloads and saves the SDK. func DownloadSDK(cmdCtx *cmdcontext.CmdCtx, downloadCtx DownloadCtx, - cliOpts *config.CliOpts) error { + cliOpts *config.CliOpts, +) error { var err error if len(downloadCtx.DirectoryPrefix) == 0 { @@ -39,11 +58,9 @@ func DownloadSDK(cmdCtx *cmdcontext.CmdCtx, downloadCtx DownloadCtx, return fmt.Errorf("bad directory prefix: %s", err) } - log.Info("Search for the requested version...") - ver, err := search.GetEeBundleInfo(cliOpts, false, - downloadCtx.DevBuild, nil, downloadCtx.Version) + ver, err := searchSDKVersionToDownload(downloadCtx, cliOpts) if err != nil { - return fmt.Errorf("cannot get SDK bundle info: %s", err) + return fmt.Errorf("no version for download: %s", err) } bundleName := ver.Version.Tarball @@ -61,14 +78,21 @@ func DownloadSDK(cmdCtx *cmdcontext.CmdCtx, downloadCtx DownloadCtx, } log.Infof("Downloading %s...", bundleName) - bundleSource, err := search.TntIoMakePkgURI(ver.Package, ver.Release, - bundleName, downloadCtx.DevBuild) + searchCtx := search.NewSearchCtx( + search.NewPlatformInformer(), + install_ee.NewTntIoDownloader(ver.Token), + ) + searchCtx.Program = search.ProgramEe + searchCtx.DevBuilds = downloadCtx.DevBuild + searchCtx.ReleaseVersion = ver.Release + + bundleSource, err := search.TntIoMakePkgURI(&searchCtx, bundleName) if err != nil { return fmt.Errorf("failed to make URI for downloading: %s", err) } - err = install_ee.GetTarantoolEE(cliOpts, bundleName, bundleSource, - ver.Token, downloadCtx.DirectoryPrefix) + err = install_ee.DownloadBundle(searchCtx.TntIoDoer, + bundleName, bundleSource, downloadCtx.DirectoryPrefix) if err != nil { return fmt.Errorf("download error: %s", err) } diff --git a/cli/install/install.go b/cli/install/install.go index af29fad18..1afa34d1d 100644 --- a/cli/install/install.go +++ b/cli/install/install.go @@ -20,7 +20,6 @@ import ( "github.com/tarantool/tt/cli/config" "github.com/tarantool/tt/cli/configure" "github.com/tarantool/tt/cli/docker" - "github.com/tarantool/tt/cli/install_ee" "github.com/tarantool/tt/cli/search" "github.com/tarantool/tt/cli/templates" "github.com/tarantool/tt/cli/util" @@ -74,7 +73,7 @@ const ( // 0755 - drwxr-xr-x // We need to give permission for all to execute // read,write for user and only read for others. - defaultDirPermissions = 0755 + defaultDirPermissions = 0o755 ) // programGitRepoUrls contains URLs of programs git repositories. @@ -90,16 +89,16 @@ type InstallCtx struct { Reinstall bool // Force is a flag which disables dependency check if enabled. Force bool - // Noclean is a flag. If it is set, + // KeepTemp is a flag. If it is set, // install will don't remove tmp files. - Noclean bool + KeepTemp bool // Local is a flag. If it is set, // install will do local installation. Local bool // BuildInDocker is set if tarantool must be built in docker container. BuildInDocker bool - // ProgramName is a program name to install. - ProgramName string + // Program is a program type to install. + Program search.ProgramType // verbose flag enables verbose logging. verbose bool // Version of the program to install. @@ -109,7 +108,8 @@ type InstallCtx struct { // buildDir is the directory, where the tarantool executable is searched, // in case of installation from the local build directory. buildDir string - // IncDir is the directory, where the tarantool headers are located. + // IncDir is the directory, where the tarantool headers are located for development install. + // Or the directory, where the headers should be installed from a bundle. IncDir string // Install development build. DevBuild bool @@ -203,8 +203,8 @@ func detectOsName() (string, error) { // getVersionsFromRepo returns all available versions from github repository. func getVersionsFromRepo(local bool, distfiles string, program string, - repolink string) ([]version.Version, error) { - + repolink string, +) ([]version.Version, error) { if local { return search.GetVersionsFromGitLocal(filepath.Join(distfiles, program)) } @@ -213,7 +213,8 @@ func getVersionsFromRepo(local bool, distfiles string, program string, // getCommit returns all available commits from repository. func getCommit(local bool, distfiles string, programName string, - line string) (string, error) { + line string, +) (string, error) { commit := "" var err error @@ -279,7 +280,7 @@ func isPackageInstalled(packageName string) bool { } // programDependenciesInstalled checks if dependencies are installed. -func programDependenciesInstalled(program string) error { +func programDependenciesInstalled(program search.ProgramType) error { programs := []Package{} packages := []string{} osName, _ := detectOsName() @@ -287,20 +288,35 @@ func programDependenciesInstalled(program string) error { programs = []Package{{"mage", "mage"}, {"git", "git"}} } else if program == search.ProgramCe { if osName == "darwin" { - programs = []Package{{"cmake", "cmake"}, {"git", "git"}, - {"make", "make"}, {"clang", "clang"}, {"openssl", "openssl"}} + programs = []Package{ + {"cmake", "cmake"}, + {"git", "git"}, + {"make", "make"}, + {"clang", "clang"}, + {"openssl", "openssl"}, + } } else if strings.Contains(osName, "Ubuntu") || strings.Contains(osName, "Debian") { - programs = []Package{{"cmake", "cmake"}, {"git", "git"}, {"make", "make"}, - {"gcc", " build-essential"}} + programs = []Package{ + {"cmake", "cmake"}, + {"git", "git"}, + {"make", "make"}, + {"gcc", " build-essential"}, + } packages = []string{"coreutils", "sed"} } else if strings.Contains(osName, "CentOs") { - programs = []Package{{"cmake", "cmake"}, {"git", "git"}, {"make", "make"}, - {"gcc", "gcc"}, {"g++", "gcc-c++ "}} + programs = []Package{ + {"cmake", "cmake"}, + {"git", "git"}, + {"make", "make"}, + {"gcc", "gcc"}, + {"g++", "gcc-c++ "}, + } packages = []string{"libstdc++-static", "perl"} } else { - answer, err := util.AskConfirm(os.Stdin, "Unknown OS, can't check if dependencies"+ - " are installed.\n"+ - "Proceed without checking?") + answer, err := util.AskConfirm(os.Stdin, + "Unknown OS, can't check if dependencies"+ + " are installed.\n"+ + "Proceed without checking?") if err != nil { return err } @@ -391,7 +407,8 @@ func downloadRepo(repoLink string, tag string, dst string, logFile *os.File, ver // copyBuildedTT copies tt binary. func copyBuildedTT(binDir, path, version string, installCtx InstallCtx, - logFile *os.File) error { + logFile *os.File, +) error { var err error if _, err := os.Stat(binDir); os.IsNotExist(err) { err = os.MkdirAll(binDir, defaultDirPermissions) @@ -420,7 +437,8 @@ func copyBuildedTT(binDir, path, version string, installCtx InstallCtx, // checkCommit checks the existence of a commit by hash. func checkCommit(input string, programName string, installCtx InstallCtx, - distfiles string) (string, string, error) { + distfiles string, +) (string, string, error) { pullRequestHash := "" isPullRequest, _ := util.IsPullRequest(input) if isPullRequest { @@ -530,12 +548,12 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error { versionStr := "" if versionFound { - versionStr = search.ProgramTt + version.FsSeparator + ttVersion + versionStr = search.ProgramTt.Exec() + version.FsSeparator + ttVersion } else { if isPullRequest { - versionStr = search.ProgramTt + version.FsSeparator + pullRequestHash + versionStr = search.ProgramTt.Exec() + version.FsSeparator + pullRequestHash } else { - versionStr = search.ProgramTt + version.FsSeparator + + versionStr = search.ProgramTt.Exec() + version.FsSeparator + ttVersion[0:util.Min(len(ttVersion), util.MinCommitHashLength)] } } @@ -563,7 +581,8 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error { if !isUpdatePossible { log.Infof("%s version of tt already exists, updating symlink...", versionStr) - err := util.CreateSymlink(versionStr, filepath.Join(binDir, search.ProgramTt), true) + err := util.CreateSymlink(versionStr, + filepath.Join(binDir, search.ProgramTt.Exec()), true) if err != nil { return err } @@ -603,7 +622,7 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error { } os.Chmod(path, defaultDirPermissions) - if !installCtx.Noclean { + if !installCtx.KeepTemp { defer os.RemoveAll(path) } @@ -680,7 +699,7 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error { log.Infof("Made default by symlink %q", symlinkPath) log.Info("Use the following command to add the bin_dir directory to the PATH: . <(tt env)") log.Infof("Done.") - if installCtx.Noclean { + if installCtx.KeepTemp { log.Infof("Artifacts can be found at: %s", path) } return nil @@ -688,7 +707,8 @@ func installTt(binDir string, installCtx InstallCtx, distfiles string) error { // patchTarantool applies patches to specific versions of tarantool. func patchTarantool(srcPath string, tarVersion string, - installCtx InstallCtx, logFile *os.File) error { + installCtx InstallCtx, logFile *os.File, +) error { log.Infof("Patching tarantool...") if tarVersion == "master" { @@ -733,7 +753,8 @@ func patchTarantool(srcPath string, tarVersion string, // prepareCmakeOpts prepares cmake command line options for tarantool building. func prepareCmakeOpts(buildPath string, tntVersion string, - installCtx InstallCtx) ([]string, error) { + installCtx InstallCtx, +) ([]string, error) { cmakeOpts := []string{".."} // Disable backtrace feature for versions 1.10.X. @@ -776,8 +797,8 @@ func prepareMakeOpts(installCtx InstallCtx) []string { // buildTarantool builds tarantool from source. Returns a path, where build artifacts are placed. func buildTarantool(srcPath string, tarVersion string, - installCtx InstallCtx, logFile *os.File) (string, error) { - + installCtx InstallCtx, logFile *os.File, +) (string, error) { buildPath := filepath.Join(srcPath, "/static-build/build") if installCtx.Dynamic { buildPath = filepath.Join(srcPath, "/dynamic-build") @@ -805,7 +826,8 @@ func buildTarantool(srcPath string, tarVersion string, // copyLocalTarantool finds and copies local tarantool folder to tmp folder. func copyLocalTarantool(distfiles string, path string, tarVersion string, - installCtx InstallCtx, logFile *os.File) error { + installCtx InstallCtx, logFile *os.File, +) error { var err error if util.IsDir(filepath.Join(distfiles, "tarantool")) { log.Infof("Local files found, installing from them...") @@ -822,8 +844,7 @@ func copyLocalTarantool(distfiles string, path string, tarVersion string, } // copyBuildedTarantool copies binary and include dir. -func copyBuildedTarantool(binPath, incPath, binDir, includeDir, version string, - installCtx InstallCtx, logFile *os.File) error { +func copyBuildedTarantool(binPath, incPath, binDir, includeDir, version string) error { var err error log.Infof("Copying executable...") if _, err := os.Stat(binDir); os.IsNotExist(err) { @@ -851,9 +872,16 @@ func copyBuildedTarantool(binPath, incPath, binDir, includeDir, version string, return fmt.Errorf("unable to create %s\n Error: %s", includeDir, err) } - err = copy.Copy(incPath, filepath.Join(includeDir, version)+"/") - if err != nil { - return err + if incPath != "" { + if !strings.HasSuffix(incPath, "/") { + // Note: copy.Copy expects the directory path to end with '/'. + incPath += "/" + } + + err = copy.Copy(incPath, filepath.Join(includeDir, version)+"/") + if err != nil { + return err + } } log.Infof("Tarantool executable is installed to: %q", execPath) @@ -864,7 +892,8 @@ func copyBuildedTarantool(binPath, incPath, binDir, includeDir, version string, var tarantoolBuildDockerfile []byte func installTarantoolInDocker(tntVersion, binDir, incDir string, installCtx InstallCtx, - distfiles string) error { + distfiles string, +) error { tmpDir, err := os.MkdirTemp("", "docker_build_ctx") if err != nil { return err @@ -885,7 +914,7 @@ func installTarantoolInDocker(tntVersion, binDir, incDir string, installCtx Inst // Write docker file (rw-rw-r-- permissions). if err = os.WriteFile(filepath.Join(tmpDir, "Dockerfile"), []byte(dockerfileText), - 0664); err != nil { + 0o664); err != nil { return err } @@ -912,8 +941,8 @@ func installTarantoolInDocker(tntVersion, binDir, incDir string, installCtx Inst if installCtx.verbose { tntInstallCommandLine = append(tntInstallCommandLine, "-V") } - tntInstallCommandLine = append(tntInstallCommandLine, "install", "-f", search.ProgramCe, - tntVersion) + tntInstallCommandLine = append(tntInstallCommandLine, "install", "-f", + search.ProgramCe.Exec(), tntVersion) if installCtx.Reinstall { tntInstallCommandLine = append(tntInstallCommandLine, "--reinstall") } @@ -968,12 +997,12 @@ func changeActiveTarantoolVersion(versionStr, binDir, incDir string) error { } // installTarantool installs selected version of tarantool. -func installTarantool(binDir string, incDir string, installCtx InstallCtx, - distfiles string) error { +func installTarantool(binDir string, installCtx InstallCtx, distfiles string) error { // Check bin and header dirs. if binDir == "" { return fmt.Errorf("bin_dir is not set, check %s", configure.ConfigName) } + incDir := installCtx.IncDir if incDir == "" { return fmt.Errorf("inc_dir is not set, check %s", configure.ConfigName) } @@ -1041,12 +1070,12 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx, versionStr := "" if versionFound { - versionStr = search.ProgramCe + version.FsSeparator + tarVersion + versionStr = search.ProgramCe.String() + version.FsSeparator + tarVersion } else { if isPullRequest { - versionStr = search.ProgramCe + version.FsSeparator + pullRequestHash + versionStr = search.ProgramCe.String() + version.FsSeparator + pullRequestHash } else { - versionStr = search.ProgramCe + version.FsSeparator + + versionStr = search.ProgramCe.String() + version.FsSeparator + tarVersion[0:util.Min(len(tarVersion), util.MinCommitHashLength)] } } @@ -1114,7 +1143,7 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx, } os.Chmod(path, defaultDirPermissions) - if !installCtx.Noclean { + if !installCtx.KeepTemp { defer os.RemoveAll(path) } @@ -1190,9 +1219,8 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx, return err } binPath := filepath.Join(buildPath, "tarantool-prefix", "bin", "tarantool") - incPath := filepath.Join(buildPath, "tarantool-prefix", "include", "tarantool") + "/" - err = copyBuildedTarantool(binPath, incPath, binDir, incDir, versionStr, installCtx, - logFile) + incPath := filepath.Join(buildPath, "tarantool-prefix", "include", "tarantool") + err = copyBuildedTarantool(binPath, incPath, binDir, incDir, versionStr) if err != nil { printLog(logFile.Name()) return err @@ -1209,7 +1237,7 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx, log.Infof("Made default by symlink %q", filepath.Join(incDir, "tarantool")) log.Info("Use the following command to add the bin_dir directory to the PATH: . <(tt env)") log.Infof("Done.") - if installCtx.Noclean { + if installCtx.KeepTemp { log.Infof("Artifacts can be found at: %s", path) } return nil @@ -1219,11 +1247,12 @@ func installTarantool(binDir string, incDir string, installCtx InstallCtx, // and if so, asks user about checking for update to latest commit. // Function returns boolean variable if update of master is possible or not. func isUpdatePossible(installCtx InstallCtx, - pathToBin, - program, + pathToBin string, + program search.ProgramType, progVer, distfiles string, - isBinExecutable bool) (bool, error) { + isBinExecutable bool, +) (bool, error) { var curBinHash, lastCommitHash string // We need to make sure that we check newest commits only for // production 'master' branch. Also we want to ask if user wants @@ -1232,8 +1261,8 @@ func isUpdatePossible(installCtx InstallCtx, var err error answer := false if !installCtx.skipMasterUpdate { - answer, err = util.AskConfirm(os.Stdin, "The 'master' version of the "+program+ - " has already been installed.\n"+ + answer, err = util.AskConfirm(os.Stdin, "The 'master' version of the "+ + program.String()+" has already been installed.\n"+ "Would you like to update it if there are newer commits available?") if err != nil { return false, err @@ -1242,7 +1271,7 @@ func isUpdatePossible(installCtx InstallCtx, if answer { lastCommitHash, err = getCommit(installCtx.Local, distfiles, - program, progVer) + program.String(), progVer) if err != nil { return false, err } @@ -1278,168 +1307,6 @@ func isUpdatePossible(installCtx InstallCtx, return true, nil } -// installTarantoolEE installs selected version of tarantool-ee. -func installTarantoolEE(binDir string, includeDir string, installCtx InstallCtx, - distfiles string, cliOpts *config.CliOpts) error { - var err error - - // Check bin and header directories. - if binDir == "" { - return fmt.Errorf("bin_dir is not set, check %s", configure.ConfigName) - } - if includeDir == "" { - return fmt.Errorf("inc_dir is not set, check %s", configure.ConfigName) - } - - files := []string{} - if installCtx.Local { - localFiles, err := os.ReadDir(cliOpts.Repo.Install) - if err != nil { - return err - } - - for _, file := range localFiles { - if strings.Contains(file.Name(), "tarantool-enterprise-sdk") && !file.IsDir() { - files = append(files, file.Name()) - } - } - } - - tarVersion := installCtx.version - if tarVersion == "" { - return fmt.Errorf("to install tarantool-ee, you need to specify the version") - } - - // Check if program is already installed. - versionStr := search.ProgramEe + version.FsSeparator + tarVersion - if !installCtx.Reinstall { - log.Infof("Checking existing...") - if util.IsRegularFile(filepath.Join(binDir, versionStr)) && - util.IsDir(filepath.Join(includeDir, versionStr)) { - log.Infof("%s version of tarantool already exists, updating symlinks...", versionStr) - err = changeActiveTarantoolVersion(versionStr, binDir, includeDir) - if err != nil { - return err - } - log.Infof("Done") - return err - } - } - - ver, err := search.GetEeBundleInfo(cliOpts, installCtx.Local, - installCtx.DevBuild, files, tarVersion) - if err != nil { - return err - } - - logFile, err := os.CreateTemp("", "tarantool_install") - if err != nil { - return err - } - defer os.Remove(logFile.Name()) - - log.Infof("Installing tarantool-ee=" + tarVersion) - - // Check dependencies. - if !installCtx.Force { - log.Infof("Checking dependencies...") - if err := programDependenciesInstalled(search.ProgramCe); err != nil { - return err - } - } - - log.Infof("Getting bundle name for %s", tarVersion) - bundleName := ver.Version.Tarball - bundleSource, err := search.TntIoMakePkgURI(ver.Package, ver.Release, - bundleName, installCtx.DevBuild) - if err != nil { - return err - } - - path, err := os.MkdirTemp("", "tarantool_install") - if err != nil { - return err - } - os.Chmod(path, defaultDirPermissions) - - if !installCtx.Noclean { - defer os.RemoveAll(path) - } - - // Download tarantool. - if installCtx.Local { - log.Infof("Checking local files...") - if util.IsRegularFile(filepath.Join(distfiles, bundleName)) { - log.Infof("Local files found, installing from them...") - localPath, _ := util.JoinAbspath(distfiles, - bundleName) - err = util.CopyFilePreserve(localPath, - filepath.Join(path, bundleName)) - if err != nil { - printLog(logFile.Name()) - return err - } - } else { - return fmt.Errorf("can't find distfiles directory") - } - } else { - log.Infof("Downloading tarantool-ee...") - err := install_ee.GetTarantoolEE(cliOpts, bundleName, bundleSource, ver.Token, path) - if err != nil { - printLog(logFile.Name()) - return err - } - } - - // Unpack archive. - log.Infof("Unpacking archive...") - err = util.ExtractTar(filepath.Join(path, - bundleName)) - if err != nil { - return err - } - - // Copy binary and headers. - if installCtx.Reinstall { - if util.IsRegularFile(filepath.Join(binDir, versionStr)) { - log.Infof("%s version of tarantool-ee already exists, removing files...", - versionStr) - err = os.RemoveAll(filepath.Join(binDir, versionStr)) - if err != nil { - printLog(logFile.Name()) - return err - } - err = os.RemoveAll(filepath.Join(includeDir, versionStr)) - } - } - if err != nil { - printLog(logFile.Name()) - return err - } - binPath := filepath.Join(path, "/tarantool-enterprise/tarantool") - incPath := filepath.Join(path, "/tarantool-enterprise/include/tarantool") + "/" - err = copyBuildedTarantool(binPath, incPath, binDir, includeDir, versionStr, installCtx, - logFile) - if err != nil { - printLog(logFile.Name()) - return err - } - - // Set symlinks. - log.Infof("Changing symlinks...") - err = changeActiveTarantoolVersion(versionStr, binDir, includeDir) - if err != nil { - printLog(logFile.Name()) - return err - } - - log.Infof("Done.") - if installCtx.Noclean { - log.Infof("Artifacts can be found at: %s", path) - } - return nil -} - // dirIsWritable checks if the current user has the write access to the passed directory. func dirIsWritable(dir string) bool { return unix.Access(dir, unix.W_OK) == nil @@ -1471,7 +1338,8 @@ func searchTarantoolHeaders(buildDir, includeDir string) (string, error) { // installTarantoolDev installs tarantool from the local build directory. func installTarantoolDev(ttBinDir string, ttIncludeDir, buildDir, - includeDir string) error { + includeDir string, +) error { var err error // Validate build directory. @@ -1555,8 +1423,11 @@ func subDirIsWritable(dir string) bool { } // Install installs program. -func Install(binDir string, includeDir string, installCtx InstallCtx, - local string, cliOpts *config.CliOpts) error { +func Install(installCtx InstallCtx, cliOpts *config.CliOpts) error { + binDir := cliOpts.Env.BinDir + includeDir := cliOpts.Env.IncludeDir + local := cliOpts.Repo.Install + var err error // This check is needed for knowing that we will be able to copy @@ -1572,30 +1443,33 @@ func Install(binDir string, includeDir string, installCtx InstallCtx, } } includeDir = filepath.Join(includeDir, "include") + if installCtx.IncDir == "" && installCtx.Program != search.ProgramDev { + installCtx.IncDir = includeDir + } - switch installCtx.ProgramName { + switch installCtx.Program { case search.ProgramTt: err = installTt(binDir, installCtx, local) case search.ProgramCe: - err = installTarantool(binDir, includeDir, installCtx, local) - case search.ProgramEe: - err = installTarantoolEE(binDir, includeDir, installCtx, local, cliOpts) + err = installTarantool(binDir, installCtx, local) + case search.ProgramEe, search.ProgramTcm: + err = installBundleProgram(&installCtx, cliOpts) case search.ProgramDev: err = installTarantoolDev(binDir, includeDir, installCtx.buildDir, installCtx.IncDir) default: - return fmt.Errorf("unknown application: %s", installCtx.ProgramName) + return fmt.Errorf("unknown application: %s", installCtx.Program) } return err } func FillCtx(cmdCtx *cmdcontext.CmdCtx, installCtx *InstallCtx, args []string) error { - installCtx.ProgramName = cmdCtx.CommandName + installCtx.Program = search.NewProgramType(cmdCtx.CommandName) installCtx.verbose = cmdCtx.Cli.Verbose installCtx.skipMasterUpdate = cmdCtx.Cli.NoPrompt - if installCtx.ProgramName == search.ProgramDev { + if installCtx.Program == search.ProgramDev { installCtx.buildDir = args[0] return nil } diff --git a/cli/install/install_bundle.go b/cli/install/install_bundle.go new file mode 100644 index 000000000..9cabff375 --- /dev/null +++ b/cli/install/install_bundle.go @@ -0,0 +1,476 @@ +package install + +import ( + "fmt" + "io" + "os" + "path/filepath" + + "github.com/apex/log" + "github.com/tarantool/tt/cli/config" + "github.com/tarantool/tt/cli/configure" + "github.com/tarantool/tt/cli/install_ee" // Keep for DownloadBundle (GetTarantoolEE) for now + "github.com/tarantool/tt/cli/search" + "github.com/tarantool/tt/cli/util" + "github.com/tarantool/tt/cli/version" +) + +// bundleParams holds the parameters required for the bundle installation process. +type bundleParams struct { + // bundleInfo is the structure that holds information about the bundle. + // It is filled in during the installation process in getBundleInfoForInstall. + bundleInfo search.BundleInfo + + // prgVersion is the specific string is joined program with version. + // It's filled in during the installation process in acquireBundleInfo. + prgVersion string + + // inst is the install context for the installation process. + inst *InstallCtx + + // opts contains command-line options and configurations. + opts *config.CliOpts + + // tmpDir is the temporary directory used for extraction. + tmpDir string + + // logFile is the file where installation logs are written for debugging fails. + logFile *os.File +} + +// checkInstallDirs validates that binary and include directories are configured and writable. +func checkInstallDirs(binDir, includeDir string) error { + if binDir == "" { + return fmt.Errorf("bin_dir is not set, check %s", configure.ConfigName) + } + + if includeDir == "" { + // For bundle installs, includeDir is usually required. + return fmt.Errorf("include_dir is not set, check %s", configure.ConfigName) + } + + // Reuse the writability checks from the main Install function. + for _, dir := range []string{binDir, includeDir} { + if _, err := os.Stat(dir); os.IsNotExist(err) && subDirIsWritable(dir) { + continue // Directory doesn't exist but can be created. + } + + if !dirIsWritable(dir) { + return fmt.Errorf("the directory %s is not writeable for the current user", dir) + } + } + return nil +} + +// checkExistingInstallation checks if the specific version of the program is already installed. +// Returns true if the installation exists, false otherwise. +func checkExistingInstallation(versionStr, binDir, includeDir string) (bool, error) { + binPath := filepath.Join(binDir, versionStr) + incPath := filepath.Join(includeDir, versionStr) + + binExists := util.IsRegularFile(binPath) + // FIXME: For TCM, this path may not exist and that's okay too. + incExists := util.IsDir(incPath) + + log.Debugf("Checking existence: bin=%s (%t), inc=%s (%t)", + binPath, binExists, incPath, incExists) + return binExists && incExists, nil +} + +// prepareTemporaryDirs creates temporary directories for installation and logging. +func prepareTemporaryDirs(bp *bundleParams) error { + var err error + + bp.tmpDir, err = os.MkdirTemp("", bp.inst.Program.String()+"_install_*") + if err != nil { + return fmt.Errorf("failed to create temporary install directory: %w", err) + } + os.Chmod(bp.tmpDir, defaultDirPermissions) + + bp.logFile, err = os.CreateTemp("", bp.inst.Program.String()+"_install_log_*") + if err != nil { + os.RemoveAll(bp.tmpDir) + return fmt.Errorf("failed to create temporary log file: %w", err) + } + + log.Debugf("Created temporary install directory: %s", bp.tmpDir) + log.Debugf("Created temporary log file: %s", bp.logFile.Name()) + return nil +} + +// checkDependencies checks if the required system dependencies are installed. +func checkDependencies(program search.ProgramType, force bool) error { + if force { + log.Debugf("Skipping dependency check due to --force flag.") + return nil + } + + if err := programDependenciesInstalled(program); err != nil { + return err + } + return nil +} + +// copyBundle copies the bundle from a local cache. +func copyBundle(bp *bundleParams) error { + distfiles := bp.opts.Repo.Install + if distfiles == "" { + return fmt.Errorf("cannot install from local repository: " + + "distribution files directory (repo.install) is not set") + } + + log.Infof("Checking local files...") + bundleName := bp.bundleInfo.Version.Tarball + localBundlePath := filepath.Join(distfiles, bundleName) + if !util.IsRegularFile(localBundlePath) { + return fmt.Errorf("local bundle file not found: %s", localBundlePath) + } + + log.Infof("Local files found, installing from %s...", bundleName) + err := util.CopyFilePreserve(localBundlePath, filepath.Join(bp.tmpDir, bundleName)) + if err != nil { + fmt.Fprintf(bp.logFile, "Error copying local bundle: %v\n", err) + return fmt.Errorf("failed to copy local bundle: %w", err) + } + return nil +} + +// downloadBundle downloads the bundle from a remote source. +func downloadBundle(bp *bundleParams) error { + bundleName := bp.bundleInfo.Version.Tarball + + searchCtx := search.NewSearchCtx( + search.NewPlatformInformer(), + install_ee.NewTntIoDownloader(bp.bundleInfo.Token), + ) + searchCtx.Program = bp.inst.Program + searchCtx.DevBuilds = bp.inst.DevBuild + searchCtx.ReleaseVersion = bp.bundleInfo.Release + + bundleSource, err := search.TntIoMakePkgURI(&searchCtx, bundleName) + if err != nil { + return fmt.Errorf("failed to construct bundle download URI: %w", err) + } + + log.Infof("Downloading %s... (%s)", bp.inst.Program, bundleSource) + err = install_ee.DownloadBundle(searchCtx.TntIoDoer, bundleName, bundleSource, bp.tmpDir) + if err != nil { + fmt.Fprintf(bp.logFile, "Error downloading bundle: %v\n", err) + return fmt.Errorf("failed to download bundle: %w", err) + } + return nil +} + +// obtainBundle downloads the bundle from a remote source or copies it from the local cache. +func obtainBundle(bp *bundleParams) error { + if bp.inst.Local { + return copyBundle(bp) + } + return downloadBundle(bp) +} + +// unpackBundle extracts the contents of the bundle archive. +func unpackBundle(bundlePath string, logFile io.Writer) error { + log.Infof("Unpacking archive %s...", filepath.Base(bundlePath)) + err := util.ExtractTar(bundlePath) + if err != nil { + fmt.Fprintf(logFile, "Error unpacking bundle: %v\n", err) + return fmt.Errorf("failed to extract bundle %s: %w", filepath.Base(bundlePath), err) + } + + log.Debugf("Bundle %s unpacked successfully.", filepath.Base(bundlePath)) + return nil +} + +// getSubDirForProgram returns the subdirectory inside archive for the specified program type. +func getSubDirForProgram(program search.ProgramType) string { + switch program { + case search.ProgramEe: + return "tarantool-enterprise" + case search.ProgramTcm: + return "" // The TCM bundle is flat (no subdirectories). + default: + return "" + } +} + +// findBundlePathsInDir makes path to binary and include directory. +func findBundlePathsInDir(baseDir string, program search.ProgramType) ( + string, string, error, +) { + subDir := getSubDirForProgram(program) + + binPath := filepath.Join(baseDir, subDir, program.Exec()) + if !util.IsRegularFile(binPath) { + return "", "", fmt.Errorf("could not find binary at %q", binPath) + } + + incPath := filepath.Join(baseDir, subDir, "include", program.Exec()) + if !util.IsDir(incPath) { + incPath = "" // No include directory found in bundle. + } + return binPath, incPath, nil +} + +// prepareForReinstall remove existing destination directories/files if needed. +func prepareForReinstall(bp *bundleParams) error { + destBinPath := filepath.Join(bp.opts.Env.BinDir, bp.prgVersion) + destIncPath := filepath.Join(bp.inst.IncDir, bp.prgVersion) + + if bp.inst.Reinstall { + if util.IsRegularFile(destBinPath) { + log.Infof("%s version of %q already exists, removing...", + bp.prgVersion, bp.inst.Program) + + if err := os.RemoveAll(destBinPath); err != nil { + fmt.Fprintf(bp.logFile, "Error removing binary: %v\n", err) + return fmt.Errorf("failed to remove binary %s: %w", destBinPath, err) + } + } + + if util.IsDir(destIncPath) { + log.Infof("Include directory for %s version already exists, removing...", + bp.prgVersion) + if err := os.RemoveAll(destIncPath); err != nil { + fmt.Fprintf(bp.logFile, "Error removing include dir: %v\n", err) + return fmt.Errorf("failed to remove include directory %s: %w", + destIncPath, err) + } + } + + log.Debugf("Existing files removed to reinstall version %q for program %q", + bp.prgVersion, bp.inst.Program) + + } else { + // If no Reinstall option, ensure that we don't have already installed version. + if util.IsRegularFile(destBinPath) || util.IsDir(destIncPath) { + return fmt.Errorf("installation path %s or %s already exists", + destBinPath, destIncPath) + } + } + + return nil +} + +// copyNewArtifacts locates the binary and include files in the unpacked directory +// and copies them to the final destination. +func copyNewArtifacts(bp *bundleParams) error { + srcBinPath, srcIncPath, err := findBundlePathsInDir(bp.tmpDir, bp.inst.Program) + if err != nil { + fmt.Fprintf(bp.logFile, "Error finding artifacts: %v\n", err) + return fmt.Errorf("failed to locate artifacts after extraction: %w", err) + } + + err = prepareForReinstall(bp) + if err != nil { + fmt.Fprintf(bp.logFile, "Error preparing for reinstall: %v\n", err) + return fmt.Errorf("failed to prepare for reinstall: %w", err) + } + + err = copyBuildedTarantool( + srcBinPath, + srcIncPath, + bp.opts.Env.BinDir, + bp.inst.IncDir, + bp.prgVersion, + ) + if err != nil { + return fmt.Errorf("failed to copy artifacts: %w", err) + } + + log.Debugf("Artifacts copied successfully.") + return nil +} + +// changeActiveBundleVersion changes symlinks to the specified bundle executable version. +func changeActiveBundleVersion(bp *bundleParams) error { + execPath := filepath.Join(bp.opts.Env.BinDir, bp.inst.Program.Exec()) + err := util.CreateSymlink(bp.prgVersion, execPath, true) + if err != nil { + return err + } + + if util.IsDir(filepath.Join(bp.inst.IncDir, bp.prgVersion)) { + incPath := filepath.Join(bp.inst.IncDir, bp.inst.Program.Exec()) + return util.CreateSymlink(bp.prgVersion, incPath, true) + } + + return nil +} + +// updateSymlinks updates the default symlinks to point to the newly installed version. +// Uses the existing changeActiveTarantoolVersion function. +func updateSymlinks(bp *bundleParams) error { + log.Infof("Updating symlinks to point to %s...", bp.prgVersion) + err := changeActiveBundleVersion(bp) + if err != nil { + log.Errorf("Failed to update symlinks: %v", err) + return fmt.Errorf("failed to update symlinks: %w", err) + } + + log.Infof("Symlinks updated successfully.") + // Log the final symlink paths for clarity + log.Infof("Active version set by symlinks: %q and %q", + filepath.Join(bp.opts.Env.BinDir, bp.inst.Program.Exec()), + filepath.Join(bp.inst.IncDir, bp.inst.Program.Exec())) + return nil +} + +// performInitialChecks performs initial validation before starting the installation. +func performInitialChecks(bp *bundleParams) error { + if bp.inst.version == "" { + return fmt.Errorf("a specific version must be provided to install %s", bp.inst.Program) + } + + if err := checkInstallDirs(bp.opts.Env.BinDir, bp.inst.IncDir); err != nil { + return err + } + + log.Infof("Requested version: %s", bp.inst.version) + return nil +} + +// acquireBundleInfoToInstall finds local candidates and fetches bundle information +// using the search package. +func acquireBundleInfoToInstall(bp *bundleParams) error { + var bundles search.BundleInfoSlice + var err error + + log.Infof("Search for the requested %q version...", bp.inst.version) + if bp.inst.Local { + bundles, err = search.FindLocalBundles(bp.inst.Program, os.DirFS(bp.opts.Repo.Install)) + if err != nil { + return err + } + + } else { + searchCtx := search.NewSearchCtx(search.NewPlatformInformer(), search.NewTntIoDoer()) + searchCtx.Program = bp.inst.Program + searchCtx.Filter = search.SearchAll + searchCtx.Package = search.GetApiPackage(bp.inst.Program) + searchCtx.DevBuilds = bp.inst.DevBuild + + bundles, err = search.FetchBundlesInfo(&searchCtx, bp.opts) + if err != nil { + return err + } + } + + bp.bundleInfo, err = search.SelectVersion(bundles, bp.inst.version) + if err != nil { + return err + } + + bp.prgVersion = bp.inst.Program.String() + version.FsSeparator + bp.bundleInfo.Version.Str + log.Infof("Found bundle: %s", bp.bundleInfo.Version.Tarball) + log.Infof("Version: %s", bp.bundleInfo.Version.Str) + return nil +} + +// executeBundleInstallation performs the core installation steps: +// dependency check, download/copy, unpack, copy artifacts. +// It manages temporary directories and log files. +func executeBundleInstallation(bp *bundleParams) (logFilePath string, errRet error) { + err := prepareTemporaryDirs(bp) + if err != nil { + return "", err + } + logFilePath = bp.logFile.Name() + + if !bp.inst.KeepTemp { + defer func() { + bp.logFile.Close() + os.Remove(bp.logFile.Name()) + os.RemoveAll(bp.tmpDir) + }() + } + + defer func() { + // Note: capture the error, if any, and dump the saved log on the screen. + if errRet != nil { + log.Errorf("Installation failed: %v", errRet) + log.Infof("See log for details: %s", logFilePath) + printLog(logFilePath) // Attempt to print log content + } + }() + + log.Infof("Starting installation steps in %s...", bp.tmpDir) + fmt.Fprintf(bp.logFile, "Installation started for %s version %s\n", + bp.inst.Program, bp.bundleInfo.Version.Str) + + if err = checkDependencies(bp.inst.Program, bp.inst.Force); err != nil { + fmt.Fprintf(bp.logFile, "Dependency check failed: %v\n", err) + return logFilePath, err + } + fmt.Fprintf(bp.logFile, "Dependency check passed.\n") + + err = obtainBundle(bp) + if err != nil { + return logFilePath, err + } + fmt.Fprintf(bp.logFile, "Bundle obtained successfully.\n") + bundlePath := filepath.Join(bp.tmpDir, bp.bundleInfo.Version.Tarball) + + if err = unpackBundle(bundlePath, bp.logFile); err != nil { + return logFilePath, err + } + fmt.Fprintf(bp.logFile, "Bundle unpacked successfully.\n") + + err = copyNewArtifacts(bp) + if err != nil { + return logFilePath, err + } + fmt.Fprintf(bp.logFile, "Artifacts copied successfully.\n") + + log.Infof("Core installation steps completed successfully.") + fmt.Fprintf(bp.logFile, "Core installation steps completed successfully.\n") + return logFilePath, nil +} + +// installBundleProgram orchestrates the installation process for programs distributed as bundles. +func installBundleProgram(installCtx *InstallCtx, cliOpts *config.CliOpts) error { + bp := bundleParams{ + inst: installCtx, + opts: cliOpts, + } + + if err := performInitialChecks(&bp); err != nil { + return err + } + + err := acquireBundleInfoToInstall(&bp) + if err != nil { + log.Errorf("Failed to find bundles to install: %v", err) + return err + } + + if !bp.inst.Reinstall { + log.Infof("Checking existing installation...") + exists, err := checkExistingInstallation(bp.prgVersion, + bp.opts.Env.BinDir, bp.inst.IncDir) + if err != nil { + return fmt.Errorf("failed to check existing installation: %w", err) + } + + if exists { + log.Infof("%s version %s already exists.", bp.inst.Program, bp.prgVersion) + return updateSymlinks(&bp) + } + log.Debugf("No existing installation found for %s.", bp.prgVersion) + } + + log.Infof("Installing %s=%s", bp.inst.Program, bp.bundleInfo.Version.Str) + logFilePath, err := executeBundleInstallation(&bp) + if err != nil { + return fmt.Errorf("installation failed during execution phase (see log: %s)", logFilePath) + } + + err = updateSymlinks(&bp) + if err != nil { + return fmt.Errorf("installation failed during finale update symlinks") + } + + log.Infof("Successfully installed %s version %s", bp.inst.Program, bp.bundleInfo.Version.Str) + log.Info("Done.") + return nil +} diff --git a/cli/install_ee/install_ee.go b/cli/install_ee/install_ee.go index 66ddd2d69..11160c387 100644 --- a/cli/install_ee/install_ee.go +++ b/cli/install_ee/install_ee.go @@ -6,66 +6,143 @@ import ( "net/http" "os" "path/filepath" + "reflect" - "github.com/tarantool/tt/cli/config" + "github.com/tarantool/tt/cli/search" "github.com/tarantool/tt/cli/util" ) -// GetTarantoolEE downloads given tarantool-ee bundle into directory. -func GetTarantoolEE(cliOpts *config.CliOpts, bundleName, bundleSource string, - token string, dst string) error { +// httpDoer is a struct that implements the search.TntIoDoer interface using the http package. +type httpDoer struct { + client *http.Client + token string +} + +// Do implement TntIoDoer interface. +// It sends an HTTP request and returns Body data from HTTP response. +func (d *httpDoer) Do(req *http.Request) ([]byte, error) { + res, err := d.client.Do(req) + if err != nil { + return nil, fmt.Errorf("HTTP request failed: %w", err) + } + defer res.Body.Close() + + if res.StatusCode != http.StatusOK { + return nil, fmt.Errorf("HTTP request error: %s", http.StatusText(res.StatusCode)) + } + + respBody, err := io.ReadAll(res.Body) + if err != nil { + return nil, fmt.Errorf("failed to read API response body: %w", err) + } + return respBody, nil +} + +func (d *httpDoer) Token() string { + return d.token +} + +// validateDestination checks if the destination path exists and is a directory. +func validateDestination(dst string) error { if _, err := os.Stat(dst); os.IsNotExist(err) { - return fmt.Errorf("directory doesn't exist: %s", dst) + return fmt.Errorf("destination directory doesn't exist: %s", dst) } + if !util.IsDir(dst) { - return fmt.Errorf("incorrect path: %s", dst) + return fmt.Errorf("destination path is not a directory: %s", dst) } - client := http.Client{ - Timeout: 0, - CheckRedirect: func(req *http.Request, via []*http.Request) error { - // API uses signed 'host' header, it must be set explicitly, - // because when redirecting it is empty. - req.Host = req.URL.Hostname() + return nil +} + +// addSessionIdCookie adds a session ID cookie to the request if the token is not empty. +func addSessionIdCookie(req *http.Request, token string) { + if token != "" { + cookie := &http.Cookie{ + Name: "sessionid", + Value: token, + } + req.AddCookie(cookie) + } +} - return nil +// NewTntIoDownloader configures and returns an HTTP client suitable for downloading bundles. +func NewTntIoDownloader(token string) *httpDoer { + return &httpDoer{ + client: &http.Client{ + Timeout: 0, + CheckRedirect: func(req *http.Request, via []*http.Request) error { + req.Host = req.URL.Hostname() + addSessionIdCookie(req, token) + return nil + }, }, + token: token, } +} +// createHttpRequest creates a new GET HTTP request with the necessary headers and cookies. +func createHttpRequest(bundleSource, token string) (*http.Request, error) { req, err := http.NewRequest(http.MethodGet, bundleSource, http.NoBody) if err != nil { - return err + return nil, fmt.Errorf("failed to create HTTP request: %w", err) } - cookie := &http.Cookie{ - Name: "sessionid", - Value: token, - } - req.AddCookie(cookie) + addSessionIdCookie(req, token) req.Header.Set("User-Agent", "tt") + return req, nil +} - res, err := client.Do(req) +// saveResponseBodyToFile creates the destination file and copies the response body content into it. +func saveResponseBodyToFile(body []byte, destFilePath string) (errRet error) { + file, err := os.Create(destFilePath) if err != nil { - return err - } else if res.StatusCode != http.StatusOK { - res.Body.Close() - return fmt.Errorf("HTTP request error: %s", http.StatusText(res.StatusCode)) + return fmt.Errorf("failed to create destination file %s: %w", destFilePath, err) } - defer res.Body.Close() + defer func() { + // Report close error only if no other error occurred during copy + if closeErr := file.Close(); closeErr != nil && errRet == nil { + errRet = fmt.Errorf("failed to close destination file %s: %w", destFilePath, closeErr) + } + }() + + _, err = file.Write(body) + if err != nil { + file.Close() + os.Remove(destFilePath) + return fmt.Errorf("failed to write downloaded content to %s: %w", destFilePath, err) + } + + return nil +} + +// DownloadBundle downloads a bundle file from the given source URL into the destination directory. +// It handles potential redirects and uses the provided token for authentication via cookies. +func DownloadBundle(doer search.TntIoDoer, bundleName, bundleSource, dst string) error { + if doer == nil || reflect.ValueOf(doer).IsNil() { + return fmt.Errorf("no tarantool.io doer was applied") + } + + if err := validateDestination(dst); err != nil { + return err + } - resBody, err := io.ReadAll(res.Body) + req, err := createHttpRequest(bundleSource, doer.Token()) if err != nil { return err } - file, err := os.Create(filepath.Join(dst, bundleName)) + responseBody, err := doer.Do(req) if err != nil { return err } - file.Write(resBody) - file.Close() + + destFilePath := filepath.Join(dst, bundleName) + if err := saveResponseBodyToFile(responseBody, destFilePath); err != nil { + return err + } return nil } diff --git a/cli/install_ee/install_ee_test.go b/cli/install_ee/install_ee_test.go index 83ef819fb..e0f120f65 100644 --- a/cli/install_ee/install_ee_test.go +++ b/cli/install_ee/install_ee_test.go @@ -1,121 +1,264 @@ package install_ee import ( + "errors" "fmt" + "net/http" "os" + "path/filepath" "testing" - "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tarantool/tt/cli/search" ) -type getCredsFromFileInputValue struct { - path string -} +const ( + // testToken is a token for the TntIoDownloader. + testToken = "test-token" +) -type getCredsFromFileOutputValue struct { - result UserCredentials - err error +// mockBundleDoer is a mock implementation of search.TntIoDoer for DownloadBundle tests. +type mockBundleDoer struct { + t *testing.T + token string + resBody []byte + resErr error + expectedUrl string } -func TestGetCredsFromFile(t *testing.T) { - assert := assert.New(t) +// Do mocks the Do method of search.TntIoDoer. +// It verifies the request's URL, method, User-Agent header, and sessionid cookie. +func (m *mockBundleDoer) Do(req *http.Request) ([]byte, error) { + m.t.Helper() - testCases := make(map[getCredsFromFileInputValue]getCredsFromFileOutputValue) + require.Equal(m.t, m.expectedUrl, req.URL.String(), "Request URL mismatch") + require.Equal(m.t, http.MethodGet, req.Method, "Request method mismatch") + require.Equal(m.t, "tt", req.Header.Get("User-Agent"), "User-Agent header mismatch") - testCases[getCredsFromFileInputValue{path: "./testdata/nonexisting"}] = - getCredsFromFileOutputValue{ - result: UserCredentials{}, - err: fmt.Errorf("open ./testdata/nonexisting: no such file or directory"), - } - - file, err := os.CreateTemp("/tmp", "tt-unittest-*.bat") - assert.Nil(err) - file.WriteString("user\npass") - defer os.Remove(file.Name()) + if m.token != "" { + cookie, err := req.Cookie("sessionid") + require.NoError(m.t, err, "sessionid cookie not found when token is non-empty") + require.NotNil(m.t, cookie, "sessionid cookie is nil when token is non-empty") + require.Equal(m.t, m.token, cookie.Value, "sessionid cookie value mismatch") + } else { + _, err := req.Cookie("sessionid") + require.ErrorIs(m.t, err, http.ErrNoCookie, + "sessionid cookie should not be present when token is empty") + } - testCases[getCredsFromFileInputValue{path: file.Name()}] = - getCredsFromFileOutputValue{ - result: UserCredentials{ - Username: "user", - Password: "pass", - }, - err: nil, - } - - file, err = os.CreateTemp("/tmp", "tt-unittest-*.bat") - assert.Nil(err) - file.WriteString("") - defer os.Remove(file.Name()) - - testCases[getCredsFromFileInputValue{path: file.Name()}] = - getCredsFromFileOutputValue{ - result: UserCredentials{}, - err: fmt.Errorf("login not set"), - } - - file, err = os.CreateTemp("/tmp", "tt-unittest-*.bat") - assert.Nil(err) - file.WriteString("user") - defer os.Remove(file.Name()) - - testCases[getCredsFromFileInputValue{path: file.Name()}] = - getCredsFromFileOutputValue{ - result: UserCredentials{}, - err: fmt.Errorf("password not set"), - } - - for input, output := range testCases { - creds, err := getCredsFromFile(input.path) - - if output.err == nil { - assert.Nil(err) - assert.Equal(output.result, creds) - } else { - assert.Equal(output.err.Error(), err.Error()) - } + if m.resErr != nil { + return nil, m.resErr } + return m.resBody, nil +} + +// Token mocks the Token method of search.TntIoDoer. +func (m *mockBundleDoer) Token() string { + return m.token } -func Test_getCredsFromEnvVars(t *testing.T) { - tests := []struct { - name string - prepare func() - want UserCredentials - wantErr assert.ErrorAssertionFunc +func TestDownloadBundle(t *testing.T) { + defaultDstSetup := func(t *testing.T, dir string) string { + t.Helper() + return dir + } + + tests := map[string]struct { + bundleName string + bundleSource string + dstSetup func(t *testing.T, dir string) string + doer *mockBundleDoer + errMsg string }{ - { - name: "Environment variables are not passed", - prepare: func() {}, - want: UserCredentials{Username: "", Password: ""}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - if err.Error() == "no credentials in environment variables were found" { - return true - } - return false + "successful download with token": { + bundleName: "bundle_with_token.tar.gz", + bundleSource: "http://tarantool.io/bundle_with_token.tar.gz", + dstSetup: defaultDstSetup, + doer: &mockBundleDoer{ + token: testToken, + resBody: []byte("bundle content with token"), + }, + }, + + "successful download no token": { + bundleName: "bundle_no_token.tar.gz", + bundleSource: "http://tarantool.io/bundle_no_token.tar.gz", + dstSetup: defaultDstSetup, + doer: &mockBundleDoer{ + token: "", + resBody: []byte("bundle content no token"), + }, + }, + + "error from TntIoDoer": { + bundleName: "bundle.tar.gz", + bundleSource: "http://tarantool.io/bundle.tar.gz", + dstSetup: defaultDstSetup, + doer: &mockBundleDoer{ + token: testToken, + resErr: errors.New("simulated network error"), + }, + errMsg: "simulated network error", + }, + + "nil TntIoDoer in searchCtx": { + bundleName: "bundle.tar.gz", + bundleSource: "http://tarantool.io/bundle.tar.gz", + dstSetup: defaultDstSetup, + doer: nil, + errMsg: "no tarantool.io doer was applied", + }, + + "destination directory does not exist": { + bundleName: "bundle.tar.gz", + bundleSource: "http://tarantool.io/bundle.tar.gz", + dstSetup: func(t *testing.T, baseDir string) string { + t.Helper() + nonExistentDir := filepath.Join(baseDir, "non_existent_subdir_for_sure") + return nonExistentDir + }, + doer: &mockBundleDoer{ + token: testToken, + }, + errMsg: "destination directory doesn't exist", + }, + + "destination is not a directory": { + bundleName: "bundle.tar.gz", + bundleSource: "http://tarantool.io/bundle.tar.gz", + dstSetup: func(t *testing.T, baseDir string) string { + t.Helper() + file, err := os.CreateTemp(baseDir, "destination_as_file_*") + require.NoError(t, err) + filePath := file.Name() + file.Close() + return filePath + }, + doer: &mockBundleDoer{ + token: testToken, + }, + errMsg: "destination path is not a directory", + }, + + "invalid bundleSource URL format": { + bundleName: "bundle.tar.gz", + bundleSource: "::invalid_url_format", + dstSetup: defaultDstSetup, + doer: &mockBundleDoer{ + token: testToken, }, + errMsg: "failed to create HTTP request: " + + `parse "::invalid_url_format": missing protocol scheme`, }, - { - name: "Environment variables are passed", - prepare: func() { - t.Setenv(EnvSdkUsername, "tt_test") - t.Setenv(EnvSdkPassword, "tt_test") + + "fails create file due to path conflict": { + bundleName: "conflict_dir/bundle.tar.gz", + bundleSource: "http://tarantool.io/bundle.tar.gz", + dstSetup: func(t *testing.T, baseDir string) string { + t.Helper() + conflictingFile := filepath.Join(baseDir, "conflict_dir") + f, err := os.Create(conflictingFile) + require.NoError(t, err) + f.Close() + return baseDir }, - want: UserCredentials{Username: "tt_test", Password: "tt_test"}, - wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { - return true + doer: &mockBundleDoer{ + token: testToken, + resBody: []byte("bundle content"), }, + errMsg: "failed to create destination file", }, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - t.Setenv(EnvSdkUsername, "") - t.Setenv(EnvSdkPassword, "") - tt.prepare() - got, err := getCredsFromEnvVars() - if !tt.wantErr(t, err, fmt.Sprintf("getCredsFromEnvVars()")) { - return + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + tmpDir := t.TempDir() + dst := tc.dstSetup(t, tmpDir) + + if tc.doer != nil { + tc.doer.t = t + tc.doer.expectedUrl = tc.bundleSource + } + err := DownloadBundle(tc.doer, tc.bundleName, tc.bundleSource, dst) + + if tc.errMsg != "" { + require.Error(t, err) + require.Contains(t, err.Error(), tc.errMsg, "Error message mismatch") + + } else { + require.NoError(t, err, "DownloadBundle failed unexpectedly") + + require.FileExists(t, filepath.Join(dst, tc.bundleName)) + destFilePath := filepath.Join(dst, tc.bundleName) + content, readErr := os.ReadFile(destFilePath) + require.NoError(t, readErr, "Failed to read downloaded file") + + require.NotNil(t, tc.doer, "tc.doer is nil in checkDownloadedFile block") + require.Equal(t, tc.doer.resBody, content, "Downloaded file content mismatch") + } + }) + } +} + +func TestNewTntIoDownloader(t *testing.T) { + tests := map[string]struct { + token string + expectToken string + requestHost string + }{ + "empty token": { + token: "", + expectToken: "", + requestHost: "tarantool.io", + }, + + "non-empty token": { + token: testToken, + expectToken: testToken, + requestHost: "tarantool.io", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + downloader := NewTntIoDownloader(tc.token) + require.NotNil(t, downloader, "NewTntIoDownloader should return a non-nil object") + require.Implements(t, (*search.TntIoDoer)(nil), downloader, + "NewTntIoDownloader should return an object implementing search.TntIoDoer") + + require.Equal(t, tc.expectToken, downloader.token, "Internal token field mismatch") + require.Equal(t, tc.expectToken, downloader.Token(), + "interface Token() method should return the correct token") + + client := downloader.client + require.NotNil(t, client, "downloader.client should not be nil") + + require.NotNil(t, client.CheckRedirect, "CheckRedirect function is nil") + + // Using a dummy request to test the CheckRedirect behavior. + requestURL := fmt.Sprintf("http://%s/testpath", tc.requestHost) + dummyReq, err := http.NewRequest(http.MethodGet, requestURL, nil) + require.NoError(t, err, "Failed to create dummy HTTP request") + + // Call the CheckRedirect to add cookies to dummyReq and set Host. + err = client.CheckRedirect(dummyReq, nil) + require.NoError(t, err, "CheckRedirect function returned an error") + + // Verify the Host field was set correctly. + require.Equal(t, tc.requestHost, dummyReq.Host, + "request Host mismatch after CheckRedirect") + + // Verify the sessionid cookie based on the token. + if tc.token != "" { + cookie, err := dummyReq.Cookie("sessionid") + require.NoError(t, err, "sessionid cookie not found when token is non-empty") + require.NotNil(t, cookie, "sessionid cookie is nil when token is non-empty") + require.Equal(t, tc.expectToken, cookie.Value, "sessionid cookie value mismatch") + } else { + _, err := dummyReq.Cookie("sessionid") + require.ErrorIs(t, err, http.ErrNoCookie, + "sessionid cookie should not be present when token is empty") } - assert.Equalf(t, tt.want, got, "getCredsFromEnvVars()") }) } } diff --git a/cli/search/bundle.go b/cli/search/bundle.go index 42195f91f..4c6df8f51 100644 --- a/cli/search/bundle.go +++ b/cli/search/bundle.go @@ -7,9 +7,9 @@ import ( "strings" "github.com/tarantool/tt/cli/config" - "github.com/tarantool/tt/cli/install_ee" "github.com/tarantool/tt/cli/util" "github.com/tarantool/tt/cli/version" + "github.com/tarantool/tt/lib/connect" ) // BundleInfo is a structure that contains specific information about SDK bundle. @@ -79,7 +79,7 @@ func Less(verLeft, verRight version.Version) bool { } // compileVersionRegexp compiles a regular expression for cutting version from SDK bundle names. -func compileVersionRegexp(prg string) (*regexp.Regexp, error) { +func compileVersionRegexp(prg ProgramType) (*regexp.Regexp, error) { var expr string switch prg { @@ -101,13 +101,13 @@ func getBundles(rawBundleInfoList map[string][]string, searchCtx *SearchCtx) ( BundleInfoSlice, error, ) { token := "" - if searchCtx.tntIoDoer != nil { - token = searchCtx.tntIoDoer.Token() + if searchCtx.TntIoDoer != nil { + token = searchCtx.TntIoDoer.Token() } bundles := BundleInfoSlice{} - re, err := compileVersionRegexp(searchCtx.ProgramName) + re, err := compileVersionRegexp(searchCtx.Program) if err != nil { return nil, err } @@ -127,7 +127,7 @@ func getBundles(rawBundleInfoList map[string][]string, searchCtx *SearchCtx) ( version.Tarball = pkg eeVer := BundleInfo{ Version: version, - Package: "enterprise", + Package: searchCtx.Package, Release: release, Token: token, } @@ -156,12 +156,18 @@ func getBundles(rawBundleInfoList map[string][]string, searchCtx *SearchCtx) ( return bundles, nil } -// fetchBundlesInfo returns slice of information about all available tarantool-ee bundles. +// FetchBundlesInfo returns slice of information about all available tarantool-ee bundles. // The result will be sorted in ascending order. -func fetchBundlesInfo(searchCtx *SearchCtx, cliOpts *config.CliOpts) ( +func FetchBundlesInfo(searchCtx *SearchCtx, cliOpts *config.CliOpts) ( BundleInfoSlice, error, ) { - credentials, err := install_ee.GetCreds(cliOpts) + searchCtx.Package = GetApiPackage(searchCtx.Program) + if searchCtx.Package == "" { + return nil, fmt.Errorf("there is no tarantool.io package for program: %s", + searchCtx.Program) + } + + credentials, err := connect.GetCreds(cliOpts) if err != nil { return nil, err } @@ -178,3 +184,23 @@ func fetchBundlesInfo(searchCtx *SearchCtx, cliOpts *config.CliOpts) ( return bundles, nil } + +// SelectVersion selects a specific version from the list of available bundles. +// If no version is specified, it returns the latest version. +func SelectVersion(bs BundleInfoSlice, ver string) (BundleInfo, error) { + if bs == nil || bs.Len() == 0 { + return BundleInfo{}, fmt.Errorf("no available versions") + } + if ver == "" { + // No version specified, return the latest one. + return bs[bs.Len()-1], nil + } + + for _, bundle := range bs { + if bundle.Version.Str == ver { + return bundle, nil + } + } + + return BundleInfo{}, fmt.Errorf("%q version doesn't found", ver) +} diff --git a/cli/search/bundle_test.go b/cli/search/bundle_test.go new file mode 100644 index 000000000..da6eaaf41 --- /dev/null +++ b/cli/search/bundle_test.go @@ -0,0 +1,277 @@ +package search_test + +import ( + "os" + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/tt/cli/config" + "github.com/tarantool/tt/cli/search" + "github.com/tarantool/tt/cli/util" + "github.com/tarantool/tt/cli/version" +) + +func TestFetchBundlesInfo(t *testing.T) { + os.Setenv("TT_CLI_EE_USERNAME", testingUsername) + os.Setenv("TT_CLI_EE_PASSWORD", testingPassword) + defer os.Unsetenv("TT_CLI_EE_USERNAME") + defer os.Unsetenv("TT_CLI_EE_PASSWORD") + + tests := map[string]struct { + program search.ProgramType + platform platformInfo + doerContent map[string][]string + specificVersion string + searchDebug bool // Applied for tarantool EE search. + expectedQuery string + expectedBundles search.BundleInfoSlice + errMsg string + }{ + "tcm_release_bundles": { + program: search.ProgramTcm, + platform: platformInfo{arch: "x86_64", os: util.OsLinux}, + doerContent: map[string][]string{ + "1.3": { + "tcm-1.3.1-0-g074b5ffa.linux.amd64.tar.gz", + "tcm-1.3.0-0-g3857712a.linux.amd64.tar.gz", + }, + "1.2": { + "tcm-1.2.3-0-geae7e7d49.linux.amd64.tar.gz", + "tcm-1.2.1-0-gc2199e13e.linux.amd64.tar.gz", + }, + }, + expectedQuery: "tarantool-cluster-manager/release/linux/amd64", + expectedBundles: search.BundleInfoSlice{ + { + Package: "tarantool-cluster-manager", + Release: "1.2", + Token: "mock-token", + Version: version.Version{ + Major: 1, + Minor: 2, + Patch: 1, + Release: version.Release{Type: version.TypeRelease}, + Hash: "gc2199e13e", + Tarball: "tcm-1.2.1-0-gc2199e13e.linux.amd64.tar.gz", + Str: "1.2.1-0-gc2199e13e", + }, + }, + { + Package: "tarantool-cluster-manager", + Release: "1.2", + Token: "mock-token", + Version: version.Version{ + Major: 1, + Minor: 2, + Patch: 3, + Release: version.Release{Type: version.TypeRelease}, + Hash: "geae7e7d49", + Tarball: "tcm-1.2.3-0-geae7e7d49.linux.amd64.tar.gz", + Str: "1.2.3-0-geae7e7d49", + }, + }, + { + Package: "tarantool-cluster-manager", + Release: "1.3", + Token: "mock-token", + Version: version.Version{ + Major: 1, + Minor: 3, + Patch: 0, + Release: version.Release{Type: version.TypeRelease}, + Hash: "g3857712a", + Tarball: "tcm-1.3.0-0-g3857712a.linux.amd64.tar.gz", + Str: "1.3.0-0-g3857712a", + }, + }, + { + Package: "tarantool-cluster-manager", + Release: "1.3", + Token: "mock-token", + Version: version.Version{ + Major: 1, + Minor: 3, + Patch: 1, + Release: version.Release{Type: version.TypeRelease}, + Hash: "g074b5ffa", + Tarball: "tcm-1.3.1-0-g074b5ffa.linux.amd64.tar.gz", + Str: "1.3.1-0-g074b5ffa", + }, + }, + }, + }, + + "tarantool_ee_debug_release_specific_versions": { + program: search.ProgramEe, + platform: platformInfo{arch: "aarch64", os: util.OsMacos}, + specificVersion: "3.2", + searchDebug: true, + doerContent: map[string][]string{ + "3.2": { + "tarantool-enterprise-sdk-gc64-3.2.0-0-r40.macos.aarch64.tar.gz", + "tarantool-enterprise-sdk-gc64-3.2.0-0-r40.macos.aarch64.tar.gz.sha256", + "tarantool-enterprise-sdk-debug-gc64-3.2.0-0-r40.macos.aarch64.tar.gz", + "tarantool-enterprise-sdk-debug-gc64-3.2.0-0-r40.macos.aarch64.tar.gz.sha256", + }, + }, + expectedQuery: "enterprise/release/macos/aarch64/3.2", + expectedBundles: search.BundleInfoSlice{ + { + Package: "enterprise", + Release: "3.2", + Token: "mock-token", + Version: version.Version{ + Major: 3, + Minor: 2, + Patch: 0, + Release: version.Release{Type: version.TypeRelease}, + Tarball: "tarantool-enterprise-sdk-debug-gc64-3.2.0-0-r40" + + ".macos.aarch64.tar.gz", + Str: "debug-gc64-3.2.0-0-r40", + BuildName: "debug-gc64", + Revision: 40, + }, + }, + }, + }, + + "unknown_os": { + program: search.ProgramTcm, + platform: platformInfo{arch: "x86_64", os: util.OsUnknown}, + errMsg: "unsupported OS", + }, + + "unsupported_arch": { + program: search.ProgramTcm, + platform: platformInfo{arch: "arm", os: util.OsLinux}, + errMsg: "unsupported architecture", + }, + + "unknown package": { + program: search.ProgramCe, + platform: platformInfo{arch: "arm", os: util.OsLinux}, + errMsg: "there is no tarantool.io package for program:", + }, + + "invalid version": { + program: search.ProgramEe, + platform: platformInfo{arch: "x86_64", os: util.OsLinux}, + doerContent: map[string][]string{ + "3.1": { + "tarantool-enterprise-sdk-gc64-Ver3.1.linux.x86_64.tar.gz", + }, + }, + expectedQuery: "enterprise/release/linux/x86_64", + errMsg: `failed to parse version "gc64-Ver3": format is not valid`, + }, + } + + opts := config.CliOpts{ + Env: &config.TtEnvOpts{ + BinDir: "/test/bin", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + mockDoer := mockDoer{ + t: t, + content: tt.doerContent, + query: tt.expectedQuery, + } + + sCtx := search.NewSearchCtx(&tt.platform, &mockDoer) + sCtx.Program = tt.program + sCtx.ReleaseVersion = tt.specificVersion + if tt.searchDebug { + sCtx.Filter = search.SearchDebug + } + + bundles, err := search.FetchBundlesInfo(&sCtx, &opts) + + if tt.errMsg != "" { + require.Error(t, err, "Expected an error, but got nil") + require.Contains(t, err.Error(), tt.errMsg, + "Expected error message does not match") + } else { + require.NoError(t, err, "Expected no error, but got: %v", err) + + require.Equal(t, len(tt.expectedBundles), bundles.Len(), + "Bundles length mismatch; got=%v", bundles) + + require.Equal(t, tt.expectedBundles, bundles, "Bundles mismatch") + } + }) + } +} + +func TestSelectVersion(t *testing.T) { + tests := map[string]struct { + bs search.BundleInfoSlice + ver string + want string + errMsg string + }{ + "single version": { + bs: search.BundleInfoSlice{ + {Version: version.Version{Str: "1.0.0"}}, + }, + ver: "1.0.0", + want: "1.0.0", + }, + + "multiple version": { + bs: search.BundleInfoSlice{ + {Version: version.Version{Str: "1.0.0"}}, + {Version: version.Version{Str: "2.0.0"}}, + {Version: version.Version{Str: "3.0.0"}}, + }, + ver: "2.0.0", + want: "2.0.0", + }, + + "non existent version": { + bs: search.BundleInfoSlice{ + {Version: version.Version{Str: "1.0.0"}}, + {Version: version.Version{Str: "2.0.0"}}, + }, + ver: "3.0.0", + errMsg: `"3.0.0" version doesn't found`, + }, + + "empty bundle": { + bs: search.BundleInfoSlice{}, + ver: "1.0.0", + errMsg: "no available versions", + }, + + "nil bundle": { + bs: nil, + ver: "1.0.0", + errMsg: "no available versions", + }, + + "get last version": { + bs: search.BundleInfoSlice{ + {Version: version.Version{Str: "1.0.0"}}, + {Version: version.Version{Str: "2.0.0"}}, + {Version: version.Version{Str: "3.0.0"}}, + }, + ver: "", + want: "3.0.0", + }, + } + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + got, err := search.SelectVersion(tt.bs, tt.ver) + if tt.errMsg != "" { + require.ErrorContains(t, err, tt.errMsg, + "Expected error message does not match") + return + } + + require.NoError(t, err, "Expected no error, but got: %v", err) + require.Equal(t, got.Version.Str, tt.want) + }) + } +} diff --git a/cli/search/const.go b/cli/search/const.go deleted file mode 100644 index 1ae169615..000000000 --- a/cli/search/const.go +++ /dev/null @@ -1,9 +0,0 @@ -package search - -const ( - ProgramCe = "tarantool" - ProgramEe = "tarantool-ee" - ProgramTt = "tt" - ProgramDev = "tarantool-dev" - ProgramTcm = "tcm" -) diff --git a/cli/search/program_types.go b/cli/search/program_types.go new file mode 100644 index 000000000..4c3cee21c --- /dev/null +++ b/cli/search/program_types.go @@ -0,0 +1,72 @@ +package search + +import "fmt" + +// ProgramType represents a strictly typed enum for program types. +type ProgramType int + +const ( + ProgramUnknown ProgramType = iota + ProgramCe // tarantool + ProgramEe // tarantool-ee + ProgramTt // tt + ProgramDev // tarantool-dev + ProgramTcm // tcm +) + +// programTypeToExec contains executables matched for each ProgramType. +var programTypeToExec = map[ProgramType]string{ + ProgramCe: "tarantool", + ProgramEe: "tarantool", + ProgramTt: "tt", + ProgramDev: "tarantool", + ProgramTcm: "tcm", +} + +// programTypeToString contains string representations for each type. +var programTypeToString = map[ProgramType]string{ + ProgramCe: "tarantool", + ProgramEe: "tarantool-ee", + ProgramDev: "tarantool-dev", + ProgramTt: "tt", + ProgramTcm: "tcm", +} + +// stringToProgramType contains the reverse mapping for efficient lookup. +var stringToProgramType = make(map[string]ProgramType, len(programTypeToString)) + +// init initialize the reverse map. +func init() { + for k, v := range programTypeToString { + stringToProgramType[v] = k + } +} + +// String returns a string representation of ProgramType. +func (p ProgramType) String() string { + if s, ok := programTypeToString[p]; ok { + return s + } + return fmt.Sprintf("unknown(%d)", p) +} + +// NewProgramType converts the string to ProgramType. +func NewProgramType(s string) ProgramType { + if p, ok := stringToProgramType[s]; ok { + return p + } + return ProgramUnknown +} + +// Exec returns an executable name of the ProgramType. +func (p ProgramType) Exec() string { + if s, ok := programTypeToExec[p]; ok { + return s + } + return fmt.Sprintf("unknown(%d)", p) +} + +// IsTarantool checks if the ProgramType is a Tarantool program. +func (p ProgramType) IsTarantool() bool { + return p == ProgramCe || p == ProgramEe || p == ProgramDev +} diff --git a/cli/search/search.go b/cli/search/search.go index d78313322..eb615a134 100644 --- a/cli/search/search.go +++ b/cli/search/search.go @@ -25,41 +25,38 @@ type SearchCtx struct { // Filter out which builds of tarantool-ee must be included in the result of search. Filter SearchFlags // What package to look for. + // FIXME: It looks like this is not needed here, as it is then auto filled via [GetApiPackage]. Package string // Release version to look for. ReleaseVersion string - // Program name - ProgramName string + // Program type of program to search for. + Program ProgramType // Search for development builds. DevBuilds bool + // TntIoDoer tarantool.io API handler with interface of TntIoDoer. + TntIoDoer TntIoDoer platformInformer PlatformInformer - tntIoDoer TntIoDoer } // NewSearchCtx creates a new SearchCtx with default production values. func NewSearchCtx(informer PlatformInformer, doer TntIoDoer) SearchCtx { return SearchCtx{ Filter: SearchRelease, + TntIoDoer: doer, platformInformer: informer, - tntIoDoer: doer, } } // printVersion prints the version and labels: // * if the package is installed: [installed] // * if the package is installed and in use: [active] -func printVersion(bindir string, program string, versionStr string) { +func printVersion(bindir string, program ProgramType, versionStr string) { if _, err := os.Stat(filepath.Join(bindir, - program+version.FsSeparator+versionStr)); err == nil { - target := "" - if program == ProgramEe { - target, _ = util.ResolveSymlink(filepath.Join(bindir, "tarantool")) - } else { - target, _ = util.ResolveSymlink(filepath.Join(bindir, program)) - } + program.String()+version.FsSeparator+versionStr)); err == nil { + target, _ := util.ResolveSymlink(filepath.Join(bindir, program.Exec())) - if path.Base(target) == program+version.FsSeparator+versionStr { + if path.Base(target) == program.String()+version.FsSeparator+versionStr { fmt.Printf("%s [active]\n", versionStr) } else { fmt.Printf("%s [installed]\n", versionStr) @@ -71,16 +68,16 @@ func printVersion(bindir string, program string, versionStr string) { // SearchVersions outputs available versions of program. func SearchVersions(searchCtx SearchCtx, cliOpts *config.CliOpts) error { - prg := searchCtx.ProgramName - log.Infof("Available versions of " + prg + ":") + prg := searchCtx.Program + log.Infof("Available versions of %s:", prg) var err error var vers version.VersionSlice switch prg { case ProgramCe: - vers, err = searchVersionsGit(cliOpts, prg, GitRepoTarantool) + vers, err = searchVersionsGit(cliOpts, GitRepoTarantool) case ProgramTt: - vers, err = searchVersionsGit(cliOpts, prg, GitRepoTT) + vers, err = searchVersionsGit(cliOpts, GitRepoTT) case ProgramEe, ProgramTcm: // Group of API-based searches vers, err = searchVersionsTntIo(cliOpts, &searchCtx) default: diff --git a/cli/search/search_git.go b/cli/search/search_git.go index b7a085ce3..3742b2c36 100644 --- a/cli/search/search_git.go +++ b/cli/search/search_git.go @@ -186,7 +186,7 @@ func GetVersionsFromGitLocal(repo string) (version.VersionSlice, error) { } // searchVersionsGit handles searching versions from a remote Git repository. -func searchVersionsGit(cliOpts *config.CliOpts, program, repo string) ( +func searchVersionsGit(cliOpts *config.CliOpts, repo string) ( version.VersionSlice, error, ) { versions, err := GetVersionsFromGitRemote(repo) diff --git a/cli/search/search_local.go b/cli/search/search_local.go index 87322f278..e7553d9c0 100644 --- a/cli/search/search_local.go +++ b/cli/search/search_local.go @@ -2,6 +2,7 @@ package search import ( "fmt" + "io/fs" "os" "path/filepath" "sort" @@ -15,7 +16,7 @@ import ( // searchVersionsLocalGit handles searching versions from a local Git repository clone. // It returns a slice of versions found. -func searchVersionsLocalGit(program, repoPath string) ( +func searchVersionsLocalGit(program ProgramType, repoPath string) ( version.VersionSlice, error, ) { if _, err := os.Stat(repoPath); os.IsNotExist(err) { @@ -34,44 +35,10 @@ func searchVersionsLocalGit(program, repoPath string) ( // searchVersionsLocalSDK handles searching versions from locally available SDK bundle files. // It returns a slice of versions found. -func searchVersionsLocalSDK(program, localDir string) ( +func searchVersionsLocalSDK(program ProgramType, dir string) ( version.VersionSlice, error, ) { - localFiles, err := os.ReadDir(localDir) - if err != nil { - if os.IsNotExist(err) { - log.Debugf("Local directory %s not found, cannot search for local SDK files.", localDir) - // The directory doesn't exist, it's not an error for searching. - return nil, nil - } - // Other errors (e.g., permissions) are actual errors. - return nil, fmt.Errorf("failed to read local directory %s: %w", localDir, err) - } - - files := []string{} - var prefix string - switch program { - case ProgramEe: - prefix = "tarantool-enterprise-sdk-" - case ProgramTcm: - prefix = "tcm-" - default: - // Should not happen if called correctly, but good practice to handle. - return nil, fmt.Errorf("local SDK file search not supported for %s", program) - } - - for _, v := range localFiles { - if strings.Contains(v.Name(), prefix) && !v.IsDir() { - files = append(files, v.Name()) - } - } - - if len(files) == 0 { - log.Debugf("No local SDK files found for %s in %s", program, localDir) - return nil, nil - } - - bundles, err := fetchBundlesInfoLocal(files, program) + bundles, err := FindLocalBundles(program, os.DirFS(dir)) if err != nil { return nil, err } @@ -80,13 +47,13 @@ func searchVersionsLocalSDK(program, localDir string) ( for i, bundle := range bundles { versions[i] = bundle.Version } + return versions, nil } // fetchBundlesInfoLocal returns slice of information about all tarantool-ee or tcm // bundles available locally. The result will be sorted in ascending order. -// Needs 'program' parameter to select correct regex. -func fetchBundlesInfoLocal(files []string, program string) (BundleInfoSlice, error) { +func fetchBundlesInfoLocal(files []string, program ProgramType) (BundleInfoSlice, error) { re, err := compileVersionRegexp(program) if err != nil { return nil, fmt.Errorf("failed to compile regex for %s: %w", program, err) @@ -104,17 +71,57 @@ func fetchBundlesInfoLocal(files []string, program string) (BundleInfoSlice, err if err != nil { return nil, fmt.Errorf("failed to parse version from file %s: %w", file, err) } - ver.Tarball = file + ver.Tarball = file versions = append(versions, BundleInfo{Version: ver}) } sort.Sort(versions) - return versions, nil } -// getBaseDirectory determines the base directory for local search (usually 'distfiles'). +// FindLocalBundles finds and parses local SDK bundle files for a given program. +func FindLocalBundles(program ProgramType, fsys fs.FS) (BundleInfoSlice, error) { + localFiles, err := fs.ReadDir(fsys, ".") + if err != nil { + if os.IsNotExist(err) { + log.Debugf("Directory not found, cannot search for local SDK files") + // The directory doesn't exist, it's not an error for searching. + return nil, nil + } + return nil, fmt.Errorf("failed to read directory: %w", err) + } + + files := []string{} + var prefix string + switch program { + case ProgramEe: + prefix = "tarantool-enterprise-sdk-" + case ProgramTcm: + prefix = "tcm-" + default: + return nil, fmt.Errorf("local SDK file search not supported for %q", program) + } + + for _, v := range localFiles { + if strings.Contains(v.Name(), prefix) && !v.IsDir() { + files = append(files, v.Name()) + } + } + + if len(files) == 0 { + log.Debugf("No local SDK files found for %q", program) + return nil, nil + } + + bundles, err := fetchBundlesInfoLocal(files, program) + if err != nil { + return nil, err + } + return bundles, nil +} + +// getBaseDirectory determines the base directory for local search. func getBaseDirectory(cfgPath string, repo *config.RepoOpts) string { localDir := "" if repo != nil && repo.Install != "" { @@ -123,13 +130,14 @@ func getBaseDirectory(cfgPath string, repo *config.RepoOpts) string { configDir := filepath.Dir(cfgPath) localDir = filepath.Join(configDir, "distfiles") } + log.Debugf("Using local search directory: %s", localDir) return localDir } // SearchVersionsLocal outputs available versions of program found locally. func SearchVersionsLocal(searchCtx SearchCtx, cliOpts *config.CliOpts, cfgPath string) error { - prg := searchCtx.ProgramName + prg := searchCtx.Program log.Infof("Available local versions of %s:", prg) localDir := getBaseDirectory(cfgPath, cliOpts.Repo) @@ -165,19 +173,3 @@ func SearchVersionsLocal(searchCtx SearchCtx, cliOpts *config.CliOpts, cfgPath s return nil } - -func getSdkBundleInfoLocal(files []string, expectedVersion string) (BundleInfo, error) { - bundles, err := fetchBundlesInfoLocal(files, ProgramEe) - if err != nil { - return BundleInfo{}, err - } - if expectedVersion == "" { - return bundles[bundles.Len()-1], nil - } - for _, bundle := range bundles { - if bundle.Version.Str == expectedVersion { - return bundle, nil - } - } - return BundleInfo{}, fmt.Errorf("%s version doesn't exist locally", expectedVersion) -} diff --git a/cli/search/search_local_test.go b/cli/search/search_local_test.go new file mode 100644 index 000000000..47704505b --- /dev/null +++ b/cli/search/search_local_test.go @@ -0,0 +1,260 @@ +package search_test + +import ( + "errors" + "fmt" + "io/fs" + "strings" + "testing" + + "github.com/apex/log" + "github.com/apex/log/handlers/memory" + "github.com/stretchr/testify/require" + "github.com/tarantool/tt/cli/search" + "github.com/tarantool/tt/cli/version" +) + +// mockDirEntry is a mock implementation of fs.DirEntry for testing. +type mockDirEntry struct { + name string + isDir bool +} + +func (m mockDirEntry) Name() string { return m.name } +func (m mockDirEntry) IsDir() bool { return m.isDir } +func (m mockDirEntry) Type() fs.FileMode { return 0 } +func (m mockDirEntry) Info() (fs.FileInfo, error) { return nil, nil } + +// mockFS is a mock implementation of fs.FS for testing. +type mockFS struct { + entries []fs.DirEntry +} + +// ReadDir implements interface [fs.ReadDirFS] and returns the entries in the directory. +// If the entries is empty, it returns an error [fs.ErrNotExist]. +// If the entries is not readable, it returns an error [fs.ErrPermission]. +func (m mockFS) ReadDir(name string) ([]fs.DirEntry, error) { + if m.entries == nil { + return nil, fs.ErrPermission + } + if len(m.entries) == 0 { + return nil, fs.ErrNotExist + } + return m.entries, nil +} + +// Open returns a dummy file for compatibility with interface [fs.FS]. +func (m mockFS) Open(name string) (fs.File, error) { + return nil, errors.New("not implemented") +} + +func TestFindLocalBundles(t *testing.T) { + tests := map[string]struct { + program search.ProgramType + files []fs.DirEntry + expectedVersion version.VersionSlice + errMsg string + logMsg string + }{ + "No matching files": { + program: search.ProgramEe, + files: []fs.DirEntry{ + mockDirEntry{name: "random-file.txt"}, + mockDirEntry{name: "another-file.log"}, + }, + // TODO: Проверять запись в логе: + logMsg: fmt.Sprintf("No local SDK files found for %q", search.ProgramEe), + }, + "No permission to read directory": { + program: search.ProgramEe, + errMsg: "failed to read directory", + }, + "Not exists directory": { + program: search.ProgramEe, + files: []fs.DirEntry{}, + // TODO: Проверять запись в логе: + logMsg: "Directory not found, cannot search for local SDK files", + }, + "Matching files for " + search.ProgramEe.String(): { + program: search.ProgramEe, + files: []fs.DirEntry{ + mockDirEntry{ + name: "tarantool-enterprise-sdk-gc64-3.3.2-0-r59.linux.x86_64.tar.gz", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-debug-gc64-3.3.2-0-r5.linux.x86_64.tar.gz", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-gc64-3.3.2-0-r59.linux.x86_64.tar.gz.sha256", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-debug-gc64-3.3.2-0-r5.linux.x86_64" + + ".tar.gz.sha256", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-2.8.4-0-r680.tar.gz", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-debug-2.8.4-0-r680.tar.gz", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-2.8.4-0-r680.tar.gz.sha256", + }, + mockDirEntry{ + name: "tarantool-enterprise-sdk-debug-2.8.4-0-r680.tar.gz.sha256", + }, + }, + expectedVersion: version.VersionSlice{ + { + Tarball: "tarantool-enterprise-sdk-2.8.4-0-r680.tar.gz", + Major: 2, + Minor: 8, + Patch: 4, + Revision: 680, + Release: version.Release{Type: version.TypeRelease}, + Str: "2.8.4-0-r680", + }, + { + Tarball: "tarantool-enterprise-sdk-debug-2.8.4-0-r680.tar.gz", + Major: 2, + Minor: 8, + Patch: 4, + Revision: 680, + Release: version.Release{Type: version.TypeRelease}, + Str: "debug-2.8.4-0-r680", + BuildName: "debug", + }, + { + Tarball: "tarantool-enterprise-sdk-debug-gc64-3.3.2-0-r5.linux.x86_64.tar.gz", + Major: 3, + Minor: 3, + Patch: 2, + Revision: 5, + Release: version.Release{Type: version.TypeRelease}, + Str: "debug-gc64-3.3.2-0-r5", + BuildName: "debug-gc64", + }, + { + Tarball: "tarantool-enterprise-sdk-gc64-3.3.2-0-r59.linux.x86_64.tar.gz", + Major: 3, + Minor: 3, + Patch: 2, + Revision: 59, + Release: version.Release{Type: version.TypeRelease}, + Str: "gc64-3.3.2-0-r59", + BuildName: "gc64", + }, + }, + }, + "Directory with match name": { + program: search.ProgramEe, + files: []fs.DirEntry{ + mockDirEntry{name: "tarantool-enterprise-sdk-2.8.4-0-r680.tar.gz", isDir: true}, + mockDirEntry{name: "tarantool-enterprise-sdk-gc64-3.3.2-0-r59.linux.x86_64.tar.gz"}, + mockDirEntry{ + name: "tarantool-enterprise-sdk-debug-2.8.4-0-r680.tar.gz", + isDir: true, + }, + }, + expectedVersion: version.VersionSlice{ + { + Tarball: "tarantool-enterprise-sdk-gc64-3.3.2-0-r59.linux.x86_64.tar.gz", + Major: 3, + Minor: 3, + Patch: 2, + Revision: 59, + Release: version.Release{Type: version.TypeRelease}, + Str: "gc64-3.3.2-0-r59", + BuildName: "gc64", + }, + }, + }, + "Matching files for " + search.ProgramTcm.String(): { + program: search.ProgramTcm, + files: []fs.DirEntry{ + mockDirEntry{name: "tcm-1.3.1-0-g074b5ffa.linux.amd64.tar.gz"}, + mockDirEntry{name: "tcm-1.3.1-0-g074b5ffa.linux.amd64.tar.gz.sha256"}, + mockDirEntry{name: "tcm-1.3.0-0-g3857712a.linux.amd64.tar.gz"}, + mockDirEntry{name: "tcm-1.3.0-0-g3857712a.linux.amd64.tar.gz.sha256"}, + }, + expectedVersion: version.VersionSlice{ + { + Tarball: "tcm-1.3.0-0-g3857712a.linux.amd64.tar.gz", + Major: 1, + Minor: 3, + Patch: 0, + Release: version.Release{Type: version.TypeRelease}, + Hash: "g3857712a", + Str: "1.3.0-0-g3857712a", + }, + { + Tarball: "tcm-1.3.1-0-g074b5ffa.linux.amd64.tar.gz", + Major: 1, + Minor: 3, + Patch: 1, + Release: version.Release{Type: version.TypeRelease}, + Hash: "g074b5ffa", + Str: "1.3.1-0-g074b5ffa", + }, + }, + }, + "Unsupported program": { + program: search.ProgramUnknown, + files: []fs.DirEntry{ + mockDirEntry{name: "unsupported-program-1.0.0.tar.gz"}, + }, + errMsg: `local SDK file search not supported for "unknown(0)"`, + }, + "Invalid version format for " + search.ProgramEe.String(): { + program: search.ProgramEe, + files: []fs.DirEntry{ + mockDirEntry{name: "tarantool-enterprise-sdk-gc64-Ver3.1.linux.x86_64.tar.gz"}, + }, + errMsg: "failed to parse version from file", + }, + "Invalid version format for " + search.ProgramTcm.String(): { + program: search.ProgramTcm, + files: []fs.DirEntry{ + mockDirEntry{name: "tcm-1.3.1-X-g074b5ffa.linux.amd64.tar.gz"}, + }, + errMsg: "failed to parse version from file", + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + handler := memory.New() + log.SetHandler(handler) + log.SetLevel(log.DebugLevel) + + mockFS := mockFS{entries: tt.files} + bundles, err := search.FindLocalBundles(tt.program, &mockFS) + + if tt.errMsg != "" { + if err == nil { + t.Fatalf("expected an error but got none") + } + require.ErrorContains(t, err, tt.errMsg) + return + } + require.NoError(t, err) + + require.Equal(t, len(tt.expectedVersion), len(bundles)) + + for i, bundle := range bundles { + require.Equal(t, tt.expectedVersion[i], bundle.Version, "index %d", i) + } + + if tt.logMsg != "" { + found := false + for _, entry := range handler.Entries { + if strings.Contains(entry.Message, tt.logMsg) { + found = true + break + } + } + require.True(t, found, "expected %q not found in log entries", tt.logMsg) + } + }) + } +} diff --git a/cli/search/search_sdk.go b/cli/search/search_sdk.go index 0ebc0e8f0..b3280990d 100644 --- a/cli/search/search_sdk.go +++ b/cli/search/search_sdk.go @@ -7,8 +7,8 @@ import ( "github.com/tarantool/tt/cli/version" ) -// getPackageName returns the package name at tarantool.io for the given program. -func getPackageName(program string) string { +// GetApiPackage returns the package name at tarantool.io for the given program. +func GetApiPackage(program ProgramType) string { switch program { case ProgramEe: return "enterprise" @@ -22,21 +22,15 @@ func getPackageName(program string) string { func searchVersionsTntIo(cliOpts *config.CliOpts, searchCtx *SearchCtx) ( version.VersionSlice, error, ) { - searchCtx.Package = getPackageName(searchCtx.ProgramName) - if searchCtx.Package == "" { - return nil, fmt.Errorf("there is no tarantool.io package for program: %s", - searchCtx.ProgramName) - } - - bundles, err := fetchBundlesInfo(searchCtx, cliOpts) + bundles, err := FetchBundlesInfo(searchCtx, cliOpts) if err != nil { return nil, fmt.Errorf("failed to fetch bundle info for %s: %w", - searchCtx.ProgramName, err) + searchCtx.Program, err) } if len(bundles) == 0 { return nil, fmt.Errorf("no versions found for %s matching the criteria", - searchCtx.ProgramName) + searchCtx.Program) } vers := make(version.VersionSlice, bundles.Len()) @@ -45,38 +39,3 @@ func searchVersionsTntIo(cliOpts *config.CliOpts, searchCtx *SearchCtx) ( } return vers, nil } - -func getSdkBundleInfoRemote(cliOpts *config.CliOpts, devBuild bool, expectedVersion string) ( - BundleInfo, error, -) { - // FIXME: Use correct search context. https://jira.vk.team/browse/TNTP-1095 - searchCtx := SearchCtx{ - ProgramName: ProgramEe, - Filter: SearchAll, - Package: "enterprise", - DevBuilds: devBuild, - platformInformer: NewPlatformInformer(), - tntIoDoer: NewTntIoDoer(), - } - bundles, err := fetchBundlesInfo(&searchCtx, cliOpts) - if err != nil { - return BundleInfo{}, err - } - for _, bundle := range bundles { - if bundle.Version.Str == expectedVersion { - return bundle, nil - } - } - return BundleInfo{}, fmt.Errorf("expected version %s not found", expectedVersion) -} - -// GetEeBundleInfo returns the available EE SDK bundle for user's OS, -// corresponding to the passed expected version argument. -func GetEeBundleInfo(cliOpts *config.CliOpts, local bool, devBuild bool, - files []string, expectedVersion string, -) (BundleInfo, error) { - if local { - return getSdkBundleInfoLocal(files, expectedVersion) - } - return getSdkBundleInfoRemote(cliOpts, devBuild, expectedVersion) -} diff --git a/cli/search/search_sdk_test.go b/cli/search/search_sdk_test.go new file mode 100644 index 000000000..12d2323f8 --- /dev/null +++ b/cli/search/search_sdk_test.go @@ -0,0 +1,52 @@ +package search_test + +import ( + "testing" + + "github.com/stretchr/testify/require" + "github.com/tarantool/tt/cli/search" +) + +func TestGetApiPackage(t *testing.T) { + tests := map[string]struct { + input search.ProgramType + expected string + }{ + "tarantool enterprise edition": { + input: search.ProgramEe, + expected: "enterprise", + }, + + "tcm": { + input: search.ProgramTcm, + expected: "tarantool-cluster-manager", + }, + + "tarantool development": { + input: search.ProgramDev, + expected: "", + }, + + "tarantool community edition": { + input: search.ProgramCe, + expected: "", + }, + + "tt cli": { + input: search.ProgramTt, + expected: "", + }, + + "unknown program": { + input: search.NewProgramType("unknown"), + expected: "", + }, + } + + for name, tc := range tests { + t.Run(name, func(t *testing.T) { + result := search.GetApiPackage(tc.input) + require.Equal(t, tc.expected, result) + }) + } +} diff --git a/cli/search/search_test.go b/cli/search/search_test.go index d2edba4b3..b3595eefa 100644 --- a/cli/search/search_test.go +++ b/cli/search/search_test.go @@ -108,8 +108,9 @@ func Test_getBundles(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { sCtx := SearchCtx{ - ProgramName: ProgramEe, - Filter: tt.args.flags, + Program: ProgramEe, + Package: "enterprise", + Filter: tt.args.flags, } got, err := getBundles(tt.args.rawBundleInfoList, &sCtx) if (err != nil) != tt.wantErr { @@ -212,11 +213,11 @@ func TestNewSearchCtx(t *testing.T) { got := NewSearchCtx(NewPlatformInformer(), NewTntIoDoer()) require.NotNil(t, got) assert.Equal(t, got.Filter, SearchRelease) - assert.Equal(t, got.ProgramName, "") + assert.Equal(t, got.Program, ProgramUnknown) assert.Equal(t, got.Package, "") assert.Equal(t, got.ReleaseVersion, "") assert.Equal(t, got.DevBuilds, false) assert.Implements(t, (*PlatformInformer)(nil), got.platformInformer) - assert.Implements(t, (*TntIoDoer)(nil), got.tntIoDoer) + assert.Implements(t, (*TntIoDoer)(nil), got.TntIoDoer) }) } diff --git a/cli/search/tntio_api.go b/cli/search/tntio_api.go index 3cc2743df..798e78313 100644 --- a/cli/search/tntio_api.go +++ b/cli/search/tntio_api.go @@ -7,10 +7,11 @@ import ( "fmt" "io" "net/http" + "reflect" "strings" - "github.com/tarantool/tt/cli/install_ee" "github.com/tarantool/tt/cli/util" + "github.com/tarantool/tt/lib/connect" ) const ( @@ -118,7 +119,7 @@ func getOsForApi(informer PlatformInformer) (string, error) { } // getArchForApi determines the architecture type string required by the tarantool.io API. -func getArchForApi(informer PlatformInformer, program string) (string, error) { +func getArchForApi(informer PlatformInformer, program ProgramType) (string, error) { arch, err := informer.GetArch() if err != nil { return "", fmt.Errorf("failed to get architecture: %w", err) @@ -144,45 +145,51 @@ func getArchForApi(informer PlatformInformer, program string) (string, error) { return "", fmt.Errorf("unsupported architecture: %s", arch) } +func getBuildType(isDev bool) string { + if isDev { + return "dev" + } + return "release" +} + // TntIoMakePkgURI generates a URI for downloading a package. -func TntIoMakePkgURI(Package string, Release string, - Tarball string, DevBuilds bool, -) (string, error) { +func TntIoMakePkgURI(searchCtx *SearchCtx, Tarball string) (string, error) { var uri string - buildType := "release" - if DevBuilds { - buildType = "dev" + if searchCtx.platformInformer == nil || reflect.ValueOf(searchCtx.platformInformer).IsNil() { + return "", fmt.Errorf("no platform informer was applied") } - // FIXME: use platformInformer from Search Context. - informer := &realInfo{} - arch, err := getArchForApi(informer, ProgramEe) + arch, err := getArchForApi(searchCtx.platformInformer, searchCtx.Program) if err != nil { return "", err } - osType, err := getOsForApi(informer) + osType, err := getOsForApi(searchCtx.platformInformer) if err != nil { return "", err } uri = fmt.Sprintf("%s/%s/%s/%s/%s/%s/%s", - PkgURI, Package, buildType, osType, arch, Release, Tarball) + PkgURI, + GetApiPackage(searchCtx.Program), + getBuildType(searchCtx.DevBuilds), + osType, + arch, + searchCtx.ReleaseVersion, + Tarball, + ) return uri, nil } // buildApiQuery constructs the query string for the tarantool.io API. -func buildApiQuery(searchCtx *SearchCtx, credentials install_ee.UserCredentials) ( +func buildApiQuery(searchCtx *SearchCtx, credentials connect.UserCredentials) ( apiRequest, error, ) { - buildType := "release" - if searchCtx.DevBuilds { - buildType = "dev" - } + buildType := getBuildType(searchCtx.DevBuilds) - arch, err := getArchForApi(searchCtx.platformInformer, searchCtx.ProgramName) + arch, err := getArchForApi(searchCtx.platformInformer, searchCtx.Program) if err != nil { return apiRequest{}, fmt.Errorf("failed to get architecture: %w", err) } @@ -265,10 +272,10 @@ func parseApiResponse(respBody []byte) (map[string][]string, error) { } // tntIoGetPkgVersions returns a list of versions of the requested package for the given host. -func tntIoGetPkgVersions(credentials install_ee.UserCredentials, searchCtx *SearchCtx) ( +func tntIoGetPkgVersions(credentials connect.UserCredentials, searchCtx *SearchCtx) ( map[string][]string, error, ) { - if searchCtx.tntIoDoer == nil { + if searchCtx.TntIoDoer == nil { return nil, fmt.Errorf("no tarantool.io doer was applied") } if searchCtx.platformInformer == nil { @@ -280,7 +287,7 @@ func tntIoGetPkgVersions(credentials install_ee.UserCredentials, searchCtx *Sear return nil, err } - resp, err := sendApiRequest(request, searchCtx.tntIoDoer) + resp, err := sendApiRequest(request, searchCtx.TntIoDoer) if err != nil { return nil, err } diff --git a/cli/search/tntio_api_test.go b/cli/search/tntio_api_test.go index 045f86b23..ed8e20812 100644 --- a/cli/search/tntio_api_test.go +++ b/cli/search/tntio_api_test.go @@ -6,13 +6,14 @@ import ( "errors" "fmt" "io" - "log" "net/http" "os" "strconv" "strings" "testing" + "github.com/apex/log" + "github.com/apex/log/handlers/memory" "github.com/stretchr/testify/require" "github.com/tarantool/tt/cli/config" "github.com/tarantool/tt/cli/search" @@ -149,7 +150,7 @@ func TestSearchVersions_TntIo(t *testing.T) { defer os.Unsetenv("TT_CLI_EE_PASSWORD") tests := map[string]struct { - program string + program search.ProgramType platform platformInfo specificVersion string devBuilds bool @@ -342,7 +343,7 @@ func TestSearchVersions_TntIo(t *testing.T) { "unknown_os": { program: search.ProgramTcm, platform: platformInfo{arch: "x86_64", os: util.OsUnknown}, - errMsg: "failed to fetch bundle info for " + search.ProgramTcm + + errMsg: "failed to fetch bundle info for " + search.ProgramTcm.String() + ": failed to get OS type for API: unsupported OS: " + strconv.Itoa(int(util.OsUnknown)), }, @@ -394,13 +395,14 @@ func TestSearchVersions_TntIo(t *testing.T) { for name, tt := range tests { t.Run(name, func(t *testing.T) { originalStdout := os.Stdout - var logBuf bytes.Buffer - log.SetOutput(&logBuf) + handler := memory.New() + log.SetHandler(handler) + log.SetLevel(log.DebugLevel) + r, w, _ := os.Pipe() os.Stdout = w defer func() { os.Stdout = originalStdout - log.SetOutput(os.Stderr) }() // Configure the mockDoer for this specific test case. @@ -412,7 +414,7 @@ func TestSearchVersions_TntIo(t *testing.T) { // Create SearchCtx with the configured mock. sCtx := search.NewSearchCtx(&tt.platform, &mockDoer) - sCtx.ProgramName = tt.program + sCtx.Program = tt.program sCtx.ReleaseVersion = tt.specificVersion sCtx.DevBuilds = tt.devBuilds if tt.searchDebug { @@ -426,24 +428,148 @@ func TestSearchVersions_TntIo(t *testing.T) { _, readErr := outBuf.ReadFrom(r) require.NoError(t, readErr, "Failed to read from stdout pipe") gotOutput := outBuf.String() - gotLog := logBuf.String() + var logBuilder strings.Builder + for _, entry := range handler.Entries { + logBuilder.WriteString(fmt.Sprintf("%s %s\n", entry.Level, entry.Message)) + } + gotLog := logBuilder.String() t.Logf("Log:\n%s", gotLog) if tt.errMsg != "" { require.Error(t, err, "Expected an error, but got nil") require.Contains(t, err.Error(), tt.errMsg, "Expected error message does not match") - } else { - require.NoError(t, err, "Expected no error, but got: %v", err) - require.Contains(t, - gotLog, - "info Available versions of "+tt.program+":", - "No info log found") - - t.Logf("Output:\n%s", gotOutput) - checkOutputVersionOrder(t, gotOutput, tt.expectedVersions) + return + } + require.NoError(t, err, "Expected no error, but got: %v", err) + require.Contains(t, + gotLog, + "info Available versions of "+tt.program.String()+":", + "No info log found") + + t.Logf("Output:\n%s", gotOutput) + checkOutputVersionOrder(t, gotOutput, tt.expectedVersions) + }) + } +} + +func TestTntIoMakePkgURI(t *testing.T) { + type args struct { + platform *platformInfo + program search.ProgramType + version string + devBuilds bool + tarball string + } + tests := map[string]struct { + args args + expected string + errMsg string + }{ + "tcm x86 linux": { + args: args{ + platform: &platformInfo{arch: "x86_64", os: util.OsLinux}, + program: search.ProgramTcm, + version: "1.3", + tarball: "tcm.tar.gz", + }, + // nolint: lll + expected: "https://www.tarantool.io/en/accounts/customer_zone/packages/tarantool-cluster-manager/release/linux/amd64/1.3/tcm.tar.gz", + }, + + "tcm arm macos": { + args: args{ + platform: &platformInfo{arch: "aarch64", os: util.OsMacos}, + program: search.ProgramTcm, + version: "1.1", + devBuilds: true, + tarball: "tcm.tar.gz", + }, + // nolint: lll + expected: "https://www.tarantool.io/en/accounts/customer_zone/packages/tarantool-cluster-manager/dev/macos/arm64/1.1/tcm.tar.gz", + }, + + "tarantool x86 linux": { + args: args{ + platform: &platformInfo{arch: "x86_64", os: util.OsLinux}, + program: search.ProgramEe, + version: "3.0", + tarball: "tarantool.tar.gz", + }, + // nolint: lll + expected: "https://www.tarantool.io/en/accounts/customer_zone/packages/enterprise/release/linux/x86_64/3.0/tarantool.tar.gz", + }, + + "tarantool arm macos": { + args: args{ + platform: &platformInfo{arch: "aarch64", os: util.OsMacos}, + program: search.ProgramEe, + version: "3.3", + devBuilds: true, + tarball: "tarantool.tar.gz", + }, + // nolint: lll + expected: "https://www.tarantool.io/en/accounts/customer_zone/packages/enterprise/dev/macos/aarch64/3.3/tarantool.tar.gz", + }, + + "no platform informer": { + args: args{ + platform: nil, + program: search.ProgramEe, + version: "3.0", + tarball: "tarantool.tar.gz", + }, + errMsg: "no platform informer was applied", + }, + + "wrong arch": { + args: args{ + platform: &platformInfo{arch: "arm", os: util.OsLinux}, + program: search.ProgramEe, + version: "3.0", + tarball: "tarantool.tar.gz", + }, + errMsg: "unsupported architecture: arm", + }, + + "empty arch": { + args: args{ + platform: &platformInfo{arch: "", os: util.OsLinux}, + program: search.ProgramEe, + version: "3.0", + tarball: "tarantool.tar.gz", + }, + errMsg: "failed to get architecture: mock architecture not applied", + }, + + "wrong os": { + args: args{ + platform: &platformInfo{arch: "x86_64", os: util.OsUnknown}, + program: search.ProgramEe, + version: "3.0", + tarball: "tarantool.tar.gz", + }, + errMsg: "unsupported OS: " + strconv.Itoa(int(util.OsUnknown)), + }, + } + + for name, tt := range tests { + t.Run(name, func(t *testing.T) { + sCtx := search.NewSearchCtx(tt.args.platform, nil) + sCtx.Program = tt.args.program + sCtx.ReleaseVersion = tt.args.version + sCtx.DevBuilds = tt.args.devBuilds + + got, err := search.TntIoMakePkgURI(&sCtx, tt.args.tarball) + if tt.errMsg != "" { + require.Error(t, err, "Expected an error, but got nil") + require.Contains(t, err.Error(), tt.errMsg, + "Expected error message does not match") + return } + require.NoError(t, err, "Expected no error, but got: %v", err) + require.Equal(t, tt.expected, got) }) } } diff --git a/cli/uninstall/uninstall.go b/cli/uninstall/uninstall.go index 501570526..5851c6d87 100644 --- a/cli/uninstall/uninstall.go +++ b/cli/uninstall/uninstall.go @@ -18,11 +18,12 @@ import ( "github.com/tarantool/tt/cli/version" ) +var progRegexp = "(?P" + + search.ProgramTt.String() + "|" + + search.ProgramCe.String() + "|" + + search.ProgramEe.String() + ")" + const ( - progRegexp = "(?P" + - search.ProgramTt + "|" + - search.ProgramCe + "|" + - search.ProgramEe + ")" verRegexp = "(?P.*)" MajorMinorPatchRegexp = `^[0-9]+\.[0-9]+\.[0-9]+` @@ -32,19 +33,13 @@ var errNotInstalled = errors.New("program is not installed") // remove removes binary/directory and symlinks from directory. // It returns true if symlink was removed, error. -func remove(program string, programVersion string, directory string, +func remove(program search.ProgramType, programVersion string, directory string, cmdCtx *cmdcontext.CmdCtx) (bool, error) { var linkPath string var err error - if program == search.ProgramCe || program == search.ProgramEe { - if linkPath, err = util.JoinAbspath(directory, "tarantool"); err != nil { - return false, err - } - } else { - if linkPath, err = util.JoinAbspath(directory, program); err != nil { - return false, err - } + if linkPath, err = util.JoinAbspath(directory, program.Exec()); err != nil { + return false, err } if _, err := os.Stat(directory); os.IsNotExist(err) { @@ -53,7 +48,7 @@ func remove(program string, programVersion string, directory string, return false, fmt.Errorf("there was some problem with %s directory", directory) } - fileName := program + version.FsSeparator + programVersion + fileName := program.String() + version.FsSeparator + programVersion path := filepath.Join(directory, fileName) if _, err := os.Stat(path); os.IsNotExist(err) { @@ -94,7 +89,11 @@ func remove(program string, programVersion string, directory string, } // UninstallProgram uninstalls program and symlinks. -func UninstallProgram(program string, programVersion string, binDst string, headerDst string, +func UninstallProgram( + program search.ProgramType, + programVersion string, + binDst string, + headerDst string, cmdCtx *cmdcontext.CmdCtx) error { log.Infof("Removing binary...") var err error @@ -146,7 +145,7 @@ func UninstallProgram(program string, programVersion string, binDst string, head return err } - if strings.Contains(program, "tarantool") { + if program.IsTarantool() { log.Infof("Removing headers...") _, err = remove(program, programVersion, headerDst, cmdCtx) if err != nil { @@ -163,10 +162,10 @@ func UninstallProgram(program string, programVersion string, binDst string, head // getAllTtVersionFormats returns all version formats with 'v' prefix and // without it before x.y.z version. -func getAllTtVersionFormats(programName, ttVersion string) ([]string, error) { +func getAllTtVersionFormats(program search.ProgramType, ttVersion string) ([]string, error) { versionsToDelete := []string{ttVersion} - if programName == search.ProgramTt { + if program == search.ProgramTt { // Need to determine if we have x.y.z format in tt uninstall argument // to make sure we add version prefix. versionMatches, err := regexp.Match(MajorMinorPatchRegexp, []byte(ttVersion)) @@ -182,11 +181,11 @@ func getAllTtVersionFormats(programName, ttVersion string) ([]string, error) { } // getDefault returns a default version of an installed program. -func getDefault(program, dir string) (string, error) { +func getDefault(program search.ProgramType, dir string) (string, error) { var ver string re := regexp.MustCompile( - "^" + program + version.FsSeparator + verRegexp + "$", + "^" + program.String() + version.FsSeparator + verRegexp + "$", ) installedPrograms, err := os.ReadDir(dir) @@ -240,7 +239,7 @@ func GetList(cliOpts *config.CliOpts, program string) []string { func searchLatestVersion(linkName, binDst, headerDst string) (string, error) { var programsToSearch []string if linkName == "tarantool" { - programsToSearch = []string{search.ProgramCe, search.ProgramEe} + programsToSearch = []string{search.ProgramCe.String(), search.ProgramEe.String()} } else { programsToSearch = []string{linkName} } @@ -316,11 +315,8 @@ func searchLatestVersion(linkName, binDst, headerDst string) (string, error) { } // switchProgramToLatestVersion switches the active version of the program to the latest installed. -func switchProgramToLatestVersion(program, binDst, headerDst string) error { - linkName := program - if program == search.ProgramCe || program == search.ProgramEe || program == search.ProgramDev { - linkName = "tarantool" - } +func switchProgramToLatestVersion(program search.ProgramType, binDst, headerDst string) error { + linkName := program.Exec() progToSwitch, err := searchLatestVersion(linkName, binDst, headerDst) if err != nil { diff --git a/cli/uninstall/uninstall_test.go b/cli/uninstall/uninstall_test.go index 5fa09a2b7..f926477ab 100644 --- a/cli/uninstall/uninstall_test.go +++ b/cli/uninstall/uninstall_test.go @@ -148,7 +148,7 @@ func TestSearchLatestVersion(t *testing.T) { func TestGetAllVersionFormats(t *testing.T) { type testCase struct { name string - programName string + program search.ProgramType ttVersion string expectedVersions []string expectedError error @@ -157,28 +157,28 @@ func TestGetAllVersionFormats(t *testing.T) { cases := []testCase{ { name: "without prefix", - programName: search.ProgramTt, + program: search.ProgramTt, ttVersion: "1.2.3", expectedVersions: []string{"1.2.3", "v1.2.3"}, expectedError: nil, }, { name: "with prefix", - programName: search.ProgramTt, + program: search.ProgramTt, ttVersion: "v1.2.3", expectedVersions: []string{"v1.2.3"}, expectedError: nil, }, { name: "not tt program", - programName: search.ProgramCe, + program: search.ProgramCe, ttVersion: "1.2.3", expectedVersions: []string{"1.2.3"}, expectedError: nil, }, { name: "not format", - programName: search.ProgramCe, + program: search.ProgramCe, ttVersion: "e902206", expectedVersions: []string{"e902206"}, expectedError: nil, @@ -187,7 +187,7 @@ func TestGetAllVersionFormats(t *testing.T) { for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { - ttVersions, err := getAllTtVersionFormats(tc.programName, tc.ttVersion) + ttVersions, err := getAllTtVersionFormats(tc.program, tc.ttVersion) assert.NoError(t, err) assert.Equal(t, tc.expectedVersions, ttVersions) }) diff --git a/go.mod b/go.mod index ce012f3df..5f07ddae1 100644 --- a/go.mod +++ b/go.mod @@ -26,12 +26,12 @@ require ( github.com/tarantool/cartridge-cli v0.0.0-20220605082730-53e6a5be9a61 github.com/tarantool/go-prompt v1.0.1 github.com/tarantool/go-tarantool v1.12.2 - github.com/tarantool/go-tarantool/v2 v2.2.1 + github.com/tarantool/go-tarantool/v2 v2.3.2 github.com/tarantool/go-tlsdialer v1.0.0 github.com/tarantool/tt/lib/cluster v0.0.0 github.com/tarantool/tt/lib/connect v0.0.0-0 github.com/tarantool/tt/lib/integrity v0.0.0 - github.com/vmihailenco/msgpack/v5 v5.3.5 + github.com/vmihailenco/msgpack/v5 v5.4.1 github.com/yuin/gopher-lua v1.1.1-0.20230219103905-71163b697a8f go.etcd.io/etcd/api/v3 v3.5.12 go.etcd.io/etcd/client/pkg/v3 v3.5.12 diff --git a/go.sum b/go.sum index 0cb52fca3..258b617e8 100644 --- a/go.sum +++ b/go.sum @@ -438,8 +438,8 @@ github.com/tarantool/go-prompt v1.0.1 h1:88Yer6gCFylqGRrdWwikNFVbklRQsqKF7mycvGd github.com/tarantool/go-prompt v1.0.1/go.mod h1:9Vuvi60Bk+3yaXqgYaXNTpLbwPPaaEOeaUgpFW1jqTU= github.com/tarantool/go-tarantool v1.12.2 h1:u4g+gTOHNxbUDJv0EIUFkRurU/lTQSzWrz8o7bHVAqI= github.com/tarantool/go-tarantool v1.12.2/go.mod h1:QRiXv0jnxwgxHtr9ZmifSr/eRba76gTUBgp69pDMX1U= -github.com/tarantool/go-tarantool/v2 v2.2.1 h1:ldzMVfkmTuJl4ie3ByMIr+mmPSKDVTcSkN8XlVZEows= -github.com/tarantool/go-tarantool/v2 v2.2.1/go.mod h1:hKKeZeCP8Y8+U6ZFS32ot1jHV/n4WKVP4fjRAvQznMY= +github.com/tarantool/go-tarantool/v2 v2.3.2 h1:egs3Cdmg4RdIyLHdG4XkkOw0k4ySmmiLxjy1fC/HN1w= +github.com/tarantool/go-tarantool/v2 v2.3.2/go.mod h1:MTbhdjFc3Jl63Lgi/UJr5D+QbT+QegqOzsNJGmaw7VM= github.com/tarantool/go-tlsdialer v1.0.0 h1:UZ67erz/QiThttIvcHtUnUWo1ZJ/BlQoN088NvkVmBQ= github.com/tarantool/go-tlsdialer v1.0.0/go.mod h1:IZuFRmnasGSBtPGZi3LMrpaSUgHYR+hDZ4ec7gR9k/0= github.com/tj/assert v0.0.0-20171129193455-018094318fb0/go.mod h1:mZ9/Rh9oLWpLLDRpvE+3b7gP/C2YyLFYxNmcLnPTMe0= @@ -456,8 +456,9 @@ github.com/tklauser/numcpus v0.2.1/go.mod h1:9aU+wOc6WjUIZEwWMP62PL/41d65P+iks1g github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/vmihailenco/msgpack/v5 v5.1.0/go.mod h1:C5gboKD0TJPqWDTVTtrQNfRbiBwHZGo8UTqP/9/XvLI= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser v0.1.2/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= diff --git a/lib/cluster/go.mod b/lib/cluster/go.mod index 38e14a390..bc9c14c92 100644 --- a/lib/cluster/go.mod +++ b/lib/cluster/go.mod @@ -5,7 +5,7 @@ go 1.23.8 require ( github.com/mitchellh/mapstructure v1.5.0 github.com/stretchr/testify v1.10.0 - github.com/tarantool/go-tarantool/v2 v2.2.1 + github.com/tarantool/go-tarantool/v2 v2.3.2 github.com/tarantool/tt/lib/connect v0.0.0-0 go.etcd.io/etcd/api/v3 v3.5.12 go.etcd.io/etcd/client/pkg/v3 v3.5.12 @@ -46,6 +46,7 @@ require ( github.com/spf13/pflag v1.0.5 // indirect github.com/tarantool/go-iproto v1.1.0 // indirect github.com/tarantool/go-openssl v1.0.0 // indirect + github.com/tarantool/tt v1.3.1 // indirect github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect go.etcd.io/bbolt v1.3.8 // indirect @@ -62,6 +63,7 @@ require ( go.opentelemetry.io/otel/trace v1.20.0 // indirect go.opentelemetry.io/proto/otlp v1.0.0 // indirect golang.org/x/crypto v0.35.0 // indirect + golang.org/x/term v0.29.0 // indirect golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -76,7 +78,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/tarantool/go-tlsdialer v1.0.0 - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect go.etcd.io/etcd/tests/v3 v3.5.12 go.uber.org/atomic v1.7.0 // indirect diff --git a/lib/cluster/go.sum b/lib/cluster/go.sum index 4f66e417a..d6322e299 100644 --- a/lib/cluster/go.sum +++ b/lib/cluster/go.sum @@ -186,7 +186,6 @@ github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= @@ -194,14 +193,16 @@ github.com/tarantool/go-iproto v1.1.0 h1:HULVOIHsiehI+FnHfM7wMDntuzUddO09DKqu2Wn github.com/tarantool/go-iproto v1.1.0/go.mod h1:LNCtdyZxojUed8SbOiYHoc3v9NvaZTB7p96hUySMlIo= github.com/tarantool/go-openssl v1.0.0 h1:kE0XhXi8b1qWOLwmcasAS67OyQdGXn42t0Qv3NwEjWY= github.com/tarantool/go-openssl v1.0.0/go.mod h1:M7H4xYSbzqpW/ZRBMyH0eyqQBsnhAMfsYk5mv0yid7A= -github.com/tarantool/go-tarantool/v2 v2.2.1 h1:ldzMVfkmTuJl4ie3ByMIr+mmPSKDVTcSkN8XlVZEows= -github.com/tarantool/go-tarantool/v2 v2.2.1/go.mod h1:hKKeZeCP8Y8+U6ZFS32ot1jHV/n4WKVP4fjRAvQznMY= +github.com/tarantool/go-tarantool/v2 v2.3.2 h1:egs3Cdmg4RdIyLHdG4XkkOw0k4ySmmiLxjy1fC/HN1w= +github.com/tarantool/go-tarantool/v2 v2.3.2/go.mod h1:MTbhdjFc3Jl63Lgi/UJr5D+QbT+QegqOzsNJGmaw7VM= github.com/tarantool/go-tlsdialer v1.0.0 h1:UZ67erz/QiThttIvcHtUnUWo1ZJ/BlQoN088NvkVmBQ= github.com/tarantool/go-tlsdialer v1.0.0/go.mod h1:IZuFRmnasGSBtPGZi3LMrpaSUgHYR+hDZ4ec7gR9k/0= +github.com/tarantool/tt v1.3.1 h1:bczu649MCQaX6k9HFA71MJ5V70nY4E0grBxGLJvYSD8= +github.com/tarantool/tt v1.3.1/go.mod h1:Z+YuHc9FNtWDcKyrOVjZ7B3fw7s0imW3QKSMAV7Qjpk= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4dN7GR16kFc5fp3d1RIYzJW5onx8Ybykw2YQFA= github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5Qo6v2eYzo7kUS51QINcR5jNpbZS8= @@ -312,6 +313,8 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.29.0 h1:L6pJp37ocefwRRtYPKSWOWzOtWSxVajvz2ldH/xi3iU= +golang.org/x/term v0.29.0/go.mod h1:6bl4lRlvVuDgSf3179VpIxBF0o10JUpXWOnI7nErv7s= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= diff --git a/lib/cluster/integration_test.go b/lib/cluster/integration_test.go index 2fa808e5c..19537ddeb 100644 --- a/lib/cluster/integration_test.go +++ b/lib/cluster/integration_test.go @@ -6,10 +6,8 @@ import ( "context" "fmt" "os" - "os/exec" "path/filepath" "reflect" - "strings" "testing" "time" @@ -20,6 +18,8 @@ import ( "go.etcd.io/etcd/tests/v3/integration" "github.com/tarantool/go-tarantool/v2" + "github.com/tarantool/go-tarantool/v2/test_helpers" + tcs_helper "github.com/tarantool/go-tarantool/v2/test_helpers/tcs" "github.com/tarantool/tt/lib/cluster" ) @@ -27,43 +27,24 @@ import ( const timeout = 5 * time.Second func tcsIsSupported(t *testing.T) bool { - cmd := exec.Command("tarantool", "--version") - - out, err := cmd.Output() - require.NoError(t, err) - - expected := "Tarantool Enterprise 3" - - return strings.HasPrefix(string(out), expected) -} - -func startTcs(t *testing.T) *exec.Cmd { - cmd := exec.Command("tarantool", "--name", "master", - "--config", "testdata/config.yml", - "testdata/init.lua") - err := cmd.Start() - require.NoError(t, err) - - var conn tarantool.Connector - // Wait for Tarantool to start. - for i := 0; i < 10; i++ { - conn, err = tarantool.Connect(context.Background(), tarantool.NetDialer{ - Address: "127.0.0.1:3301", - }, tarantool.Opts{}) - if err == nil { - defer conn.Close() - break - } - time.Sleep(time.Second) + ok, err := test_helpers.IsTcsSupported() + if err != nil { + t.Fatalf("Failed to check if TCS is supported: %s", err) } - require.NoError(t, err) + return ok +} - return cmd +func startTcs(t *testing.T) *tcs_helper.TCS { + tcs := tcs_helper.StartTesting(t, 3301) + return &tcs } -func stopTcs(t *testing.T, cmd *exec.Cmd) { - err := cmd.Process.Kill() - require.NoError(t, err) +func stopTcs(t *testing.T, inst any) { + tcs, ok := inst.(*tcs_helper.TCS) + if !ok { + t.Fatalf("Shutdown expected *tcs_helper.TCS, got %T", inst) + } + tcs.Stop() } type etcdOpts struct { @@ -605,11 +586,11 @@ var testsIntegrity = []struct { Name: "tarantool", Applicable: tcsIsSupported, Setup: func(t *testing.T) interface{} { - command := startTcs(t) - return command + inst := startTcs(t) + return inst }, Shutdown: func(t *testing.T, inst interface{}) { - stopTcs(t, inst.(*exec.Cmd)) + stopTcs(t, inst) }, NewPublisher: func( t *testing.T, @@ -618,6 +599,11 @@ var testsIntegrity = []struct { key string, inst interface{}, ) (cluster.DataPublisher, func()) { + tcs, ok := inst.(*tcs_helper.TCS) + if !ok { + t.Fatalf("NewPublisher expected *tcs_helper.TCS, got %T", inst) + } + publisherFactory := cluster.NewIntegrityDataPublisherFactory(signFunc) opts := tarantool.Opts{ @@ -626,11 +612,7 @@ var testsIntegrity = []struct { MaxReconnects: 10, } - conn, err := tarantool.Connect(context.Background(), tarantool.NetDialer{ - Address: "127.0.0.1:3301", - User: "client", - Password: "secret", - }, opts) + conn, err := tarantool.Connect(context.Background(), tcs.Dialer(), opts) require.NoError(t, err) pub, err := publisherFactory.NewTarantool(conn, prefix, key, 1*time.Second) @@ -645,6 +627,10 @@ var testsIntegrity = []struct { key string, inst interface{}, ) (cluster.DataCollector, func()) { + tcs, ok := inst.(*tcs_helper.TCS) + if !ok { + t.Fatalf("NewCollector expected *tcs_helper.TCS, got %T", inst) + } collectorFactory := cluster.NewIntegrityDataCollectorFactory(checkFunc, nil) opts := tarantool.Opts{ @@ -653,11 +639,7 @@ var testsIntegrity = []struct { MaxReconnects: 10, } - conn, err := tarantool.Connect(context.Background(), tarantool.NetDialer{ - Address: "127.0.0.1:3301", - User: "client", - Password: "secret", - }, opts) + conn, err := tarantool.Connect(context.Background(), tcs.Dialer(), opts) require.NoError(t, err) coll, err := collectorFactory.NewTarantool(conn, prefix, key, 1*time.Second) @@ -761,8 +743,7 @@ func TestIntegrityDataPublisherKey_CollectorAll_valid(t *testing.T) { } for _, entry := range data { - publisher, closeConn := - test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) + publisher, closeConn := test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) defer closeConn() err := publisher.Publish(entry.Revision, entry.Value) @@ -801,8 +782,8 @@ func TestIntegrityDataPublisherKey_CollectorKey_valid(t *testing.T) { } for _, entry := range data { - publisher, closeConn := - test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) + publisher, closeConn := test.NewPublisher( + t, validSignFunc, testPrefix, entry.Source, inst) defer closeConn() err := publisher.Publish(entry.Revision, entry.Value) @@ -810,8 +791,8 @@ func TestIntegrityDataPublisherKey_CollectorKey_valid(t *testing.T) { } for _, entry := range data { - collector, closeConn := - test.NewCollector(t, validCheckFunc, testPrefix, entry.Source, inst) + collector, closeConn := test.NewCollector( + t, validCheckFunc, testPrefix, entry.Source, inst) defer closeConn() result, err := collector.Collect() @@ -845,8 +826,7 @@ func TestIntegrityDataCollectorAllPublisherAll_valid(t *testing.T) { } for _, entry := range data { - publisher, closeConn := - test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) + publisher, closeConn := test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) defer closeConn() err := publisher.Publish(entry.Revision, entry.Value) @@ -886,8 +866,7 @@ func TestIntegrityDataCollectorKeyPublisherAll_valid(t *testing.T) { } for _, entry := range data { - publisher, closeConn := - test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) + publisher, closeConn := test.NewPublisher(t, validSignFunc, testPrefix, entry.Source, inst) defer closeConn() err := publisher.Publish(entry.Revision, entry.Value) diff --git a/cli/install_ee/credentials.go b/lib/connect/credentials.go similarity index 99% rename from cli/install_ee/credentials.go rename to lib/connect/credentials.go index 4955f4581..810f192fe 100644 --- a/cli/install_ee/credentials.go +++ b/lib/connect/credentials.go @@ -1,4 +1,4 @@ -package install_ee +package connect import ( "bufio" diff --git a/lib/connect/credentials_test.go b/lib/connect/credentials_test.go new file mode 100644 index 000000000..37570a865 --- /dev/null +++ b/lib/connect/credentials_test.go @@ -0,0 +1,121 @@ +package connect + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" +) + +type getCredsFromFileInputValue struct { + path string +} + +type getCredsFromFileOutputValue struct { + result UserCredentials + err error +} + +func TestGetCredsFromFile(t *testing.T) { + assert := assert.New(t) + + testCases := make(map[getCredsFromFileInputValue]getCredsFromFileOutputValue) + + testCases[getCredsFromFileInputValue{path: "./testdata/nonexisting"}] = + getCredsFromFileOutputValue{ + result: UserCredentials{}, + err: fmt.Errorf("open ./testdata/nonexisting: no such file or directory"), + } + + file, err := os.CreateTemp("/tmp", "tt-unittest-*.bat") + assert.Nil(err) + file.WriteString("user\npass") + defer os.Remove(file.Name()) + + testCases[getCredsFromFileInputValue{path: file.Name()}] = + getCredsFromFileOutputValue{ + result: UserCredentials{ + Username: "user", + Password: "pass", + }, + err: nil, + } + + file, err = os.CreateTemp("/tmp", "tt-unittest-*.bat") + assert.Nil(err) + file.WriteString("") + defer os.Remove(file.Name()) + + testCases[getCredsFromFileInputValue{path: file.Name()}] = + getCredsFromFileOutputValue{ + result: UserCredentials{}, + err: fmt.Errorf("login not set"), + } + + file, err = os.CreateTemp("/tmp", "tt-unittest-*.bat") + assert.Nil(err) + file.WriteString("user") + defer os.Remove(file.Name()) + + testCases[getCredsFromFileInputValue{path: file.Name()}] = + getCredsFromFileOutputValue{ + result: UserCredentials{}, + err: fmt.Errorf("password not set"), + } + + for input, output := range testCases { + creds, err := getCredsFromFile(input.path) + + if output.err == nil { + assert.Nil(err) + assert.Equal(output.result, creds) + } else { + assert.Equal(output.err.Error(), err.Error()) + } + } +} + +func Test_getCredsFromEnvVars(t *testing.T) { + tests := []struct { + name string + prepare func() + want UserCredentials + wantErr assert.ErrorAssertionFunc + }{ + { + name: "Environment variables are not passed", + prepare: func() {}, + want: UserCredentials{Username: "", Password: ""}, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + if err.Error() == "no credentials in environment variables were found" { + return true + } + return false + }, + }, + { + name: "Environment variables are passed", + prepare: func() { + t.Setenv(EnvSdkUsername, "tt_test") + t.Setenv(EnvSdkPassword, "tt_test") + }, + want: UserCredentials{Username: "tt_test", Password: "tt_test"}, + wantErr: func(t assert.TestingT, err error, i ...interface{}) bool { + return true + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Setenv(EnvSdkUsername, "") + t.Setenv(EnvSdkPassword, "") + tt.prepare() + got, err := getCredsFromEnvVars() + if !tt.wantErr(t, err, fmt.Sprintf("getCredsFromEnvVars()")) { + return + } + assert.Equalf(t, tt.want, got, "getCredsFromEnvVars()") + }) + } +} diff --git a/lib/connect/go.mod b/lib/connect/go.mod index f467c466e..0f453be1a 100644 --- a/lib/connect/go.mod +++ b/lib/connect/go.mod @@ -5,11 +5,13 @@ go 1.23.8 require ( github.com/pmezard/go-difflib v1.0.0 github.com/stretchr/testify v1.10.0 + github.com/tarantool/tt v1.3.1 + golang.org/x/term v0.13.0 ) require ( github.com/davecgh/go-spew v1.1.1 // indirect - github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect - gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect + github.com/kr/pretty v0.2.1 // indirect + golang.org/x/sys v0.13.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/lib/connect/go.sum b/lib/connect/go.sum index 2db3f8147..e91a51be3 100644 --- a/lib/connect/go.sum +++ b/lib/connect/go.sum @@ -1,16 +1,22 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/kr/pretty v0.2.1 h1:Fmg33tUaq4/8ym9TJN1x7sLJnHVwhP33CNkpYV/7rwI= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= -github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tarantool/tt v1.3.1 h1:bczu649MCQaX6k9HFA71MJ5V70nY4E0grBxGLJvYSD8= +github.com/tarantool/tt v1.3.1/go.mod h1:Z+YuHc9FNtWDcKyrOVjZ7B3fw7s0imW3QKSMAV7Qjpk= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.13.0 h1:bb+I9cTfFazGW51MZqBVmZy7+JEJMouUHTUSKVQLBek= +golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU= -gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/test/integration/ee/test_ee_install.py b/test/integration/ee/test_ee_install.py index eb86ff12d..259294154 100644 --- a/test/integration/ee/test_ee_install.py +++ b/test/integration/ee/test_ee_install.py @@ -1,5 +1,7 @@ import os import re +from pathlib import Path +from typing import Optional import pytest @@ -11,54 +13,80 @@ @pytest.mark.slow_ee -def test_install_ee(tt_cmd, tmp_path): +@pytest.mark.parametrize( + "prog,exec,incl", + [ + ("tarantool-ee", "tarantool", "include/tarantool"), + ("tcm", "tcm", None), + ], +) +def test_install_ee( + tt_cmd: Path, tmp_path: Path, prog: str, exec: str, incl: Optional[str] +) -> None: rc, output = run_command_and_get_output( - [tt_cmd, "init"], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "init"], cwd=tmp_path, env=dict(os.environ, PWD=tmp_path) + ) assert rc == 0 rc, output = run_command_and_get_output( - [tt_cmd, "search", "tarantool-ee"], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "search", prog], + cwd=tmp_path, + env=dict(os.environ, PWD=tmp_path), + ) - version = output.split('\n')[1] - assert re.search(r"(\d+.\d+.\d+|)", - version) + version = output.split("\n")[1] + assert re.search(r"(\d+.\d+.\d+|)", version) rc, output = run_command_and_get_output( - [tt_cmd, "install", "-f", "tarantool-ee", version], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "install", "-f", prog, version], + cwd=tmp_path, + env=dict(os.environ, PWD=tmp_path), + ) assert rc == 0 - assert re.search("Installing tarantool-ee="+version, output) - assert re.search("Downloading tarantool-ee...", output) + assert re.search(f"Installing {prog}={version}", output) + assert re.search(f"Downloading {prog}...", output) assert re.search("Done.", output) - assert os.path.exists(os.path.join(tmp_path, 'bin', 'tarantool')) - assert os.path.exists(os.path.join(tmp_path, 'include', 'include', 'tarantool')) + assert (tmp_path / "bin" / exec).exists() + if incl: + assert (tmp_path / "include" / incl).exists() @pytest.mark.slow_ee -def test_install_ee_dev(tt_cmd, tmp_path): +@pytest.mark.parametrize( + "prog,exec,incl", + [ + ("tarantool-ee", "tarantool", "include/tarantool"), + ("tcm", "tcm", None), + ], +) +def test_install_ee_dev( + tt_cmd: Path, tmp_path: Path, prog: str, exec: str, incl: Optional[str] +) -> None: rc, output = run_command_and_get_output( - [tt_cmd, "init"], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "init"], cwd=tmp_path, env=dict(os.environ, PWD=tmp_path) + ) assert rc == 0 rc, output = run_command_and_get_output( - [tt_cmd, "search", "tarantool-ee", "--dev"], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "search", prog, "--dev"], + cwd=tmp_path, + env=dict(os.environ, PWD=tmp_path), + ) - version = output.split('\n')[1] - assert re.search(r"(\d+.\d+.\d+|)", - version) + version = output.split("\n")[1] + assert re.search(r"(\d+.\d+.\d+|)", version) rc, output = run_command_and_get_output( - [tt_cmd, "install", "-f", "tarantool-ee", version, "--dev"], - cwd=tmp_path, env=dict(os.environ, PWD=tmp_path)) + [tt_cmd, "install", "-f", prog, version, "--dev"], + cwd=tmp_path, + env=dict(os.environ, PWD=tmp_path), + ) assert rc == 0 - assert re.search("Installing tarantool-ee="+version, output) - assert re.search("Downloading tarantool-ee...", output) + assert re.search(f"Installing {prog}={version}", output) + assert re.search(f"Downloading {prog}...", output) assert re.search("Done.", output) - assert os.path.exists(os.path.join(tmp_path, 'bin', 'tarantool')) - assert os.path.exists(os.path.join(tmp_path, 'include', 'include', 'tarantool')) + assert (tmp_path / "bin" / exec).exists() + if incl: + assert (tmp_path / "include" / incl).exists()