Skip to content

Commit

Permalink
feat: install CLI version command
Browse files Browse the repository at this point in the history
  • Loading branch information
Integralist committed Dec 11, 2023
1 parent 954008c commit 9720a14
Show file tree
Hide file tree
Showing 3 changed files with 123 additions and 0 deletions.
3 changes: 3 additions & 0 deletions pkg/commands/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/fastly/cli/pkg/commands/dictionaryentry"
"github.com/fastly/cli/pkg/commands/domain"
"github.com/fastly/cli/pkg/commands/healthcheck"
"github.com/fastly/cli/pkg/commands/install"
"github.com/fastly/cli/pkg/commands/ip"
"github.com/fastly/cli/pkg/commands/kvstore"
"github.com/fastly/cli/pkg/commands/kvstoreentry"
Expand Down Expand Up @@ -169,6 +170,7 @@ func Define(
healthcheckDescribe := healthcheck.NewDescribeCommand(healthcheckCmdRoot.CmdClause, data)
healthcheckList := healthcheck.NewListCommand(healthcheckCmdRoot.CmdClause, data)
healthcheckUpdate := healthcheck.NewUpdateCommand(healthcheckCmdRoot.CmdClause, data)
installRoot := install.NewRootCommand(app, data)
ipCmdRoot := ip.NewRootCommand(app, data)
kvstoreCmdRoot := kvstore.NewRootCommand(app, data)
kvstoreCreate := kvstore.NewCreateCommand(kvstoreCmdRoot.CmdClause, data)
Expand Down Expand Up @@ -535,6 +537,7 @@ func Define(
healthcheckDescribe,
healthcheckList,
healthcheckUpdate,
installRoot,
ipCmdRoot,
kvstoreCreate,
kvstoreDelete,
Expand Down
2 changes: 2 additions & 0 deletions pkg/commands/install/doc.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
// Package install contains functions for installing a specific CLI version.
package install
118 changes: 118 additions & 0 deletions pkg/commands/install/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package install

import (
"fmt"
"io"
"os"
"path/filepath"

"github.com/fastly/cli/pkg/cmd"
"github.com/fastly/cli/pkg/filesystem"
"github.com/fastly/cli/pkg/global"
"github.com/fastly/cli/pkg/text"
)

// RootCommand is the parent command for all subcommands in this package.
// It should be installed under the primary root command.
type RootCommand struct {
cmd.Base

versionToInstall string
}

// NewRootCommand returns a new command registered in the parent.
func NewRootCommand(parent cmd.Registerer, g *global.Data) *RootCommand {
var c RootCommand
c.Globals = g
c.CmdClause = parent.Command("install", "Install the specified version of the CLI")
c.CmdClause.Arg("version", "CLI release version to install (e.g. 10.8.0)").Required().StringVar(&c.versionToInstall)
return &c
}

// Exec implements the command interface.
func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error {
spinner, err := text.NewSpinner(out)
if err != nil {
return err
}

var downloadedBin string
err = spinner.Process(fmt.Sprintf("Fetching release %s", c.versionToInstall), func(_ *text.SpinnerWrapper) error {
downloadedBin, err = c.Globals.Versioners.CLI.DownloadVersion(c.versionToInstall)
if err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"CLI version to install": c.versionToInstall,
})
return fmt.Errorf("error downloading release version %s: %w", c.versionToInstall, err)
}
return nil
})
if err != nil {
return err
}
defer os.RemoveAll(downloadedBin)

var currentBin string
err = spinner.Process("Replacing binary", func(_ *text.SpinnerWrapper) error {
execPath, err := os.Executable()
if err != nil {
c.Globals.ErrLog.Add(err)
return fmt.Errorf("error determining executable path: %w", err)
}

currentBin, err = filepath.Abs(execPath)
if err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Executable path": execPath,
})
return fmt.Errorf("error determining absolute target path: %w", err)
}

// Windows does not permit replacing a running executable, however it will
// permit it if you first move the original executable. So we first move the
// running executable to a new location, then we move the executable that we
// downloaded to the same location as the original.
// I've also tested this approach on nix systems and it works fine.
//
// Reference:
// https://github.com/golang/go/issues/21997#issuecomment-331744930

backup := currentBin + ".bak"
if err := os.Rename(currentBin, backup); err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Executable (source)": downloadedBin,
"Executable (destination)": currentBin,
})
return fmt.Errorf("error moving the current executable: %w", err)
}

if err = os.Remove(backup); err != nil {
c.Globals.ErrLog.Add(err)
}

// Move the downloaded binary to the same location as the current executable.
if err := os.Rename(downloadedBin, currentBin); err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Executable (source)": downloadedBin,
"Executable (destination)": currentBin,
})
renameErr := err

// Failing that we'll try to io.Copy downloaded binary to the current binary.
if err := filesystem.CopyFile(downloadedBin, currentBin); err != nil {
c.Globals.ErrLog.AddWithContext(err, map[string]any{
"Executable (source)": downloadedBin,
"Executable (destination)": currentBin,
})
return fmt.Errorf("error 'copying' latest binary in place: %w (following an error 'moving': %w)", err, renameErr)
}
}
return nil
})
if err != nil {
return err
}

text.Success(out, "\nInstalled version %s.", c.versionToInstall)
return nil
}

0 comments on commit 9720a14

Please sign in to comment.