Skip to content

Commit

Permalink
Merge 3408c65 into 326c450
Browse files Browse the repository at this point in the history
  • Loading branch information
ycngadeu-coveo committed Jul 17, 2019
2 parents 326c450 + 3408c65 commit b493054
Show file tree
Hide file tree
Showing 11 changed files with 883 additions and 9 deletions.
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ Key | Description | Default value
| run-before | Script that is executed before the actual command | *no default*
| run-after | Script that is executed after the actual command | *no default*
| alias | Allows to set short aliases for long commands<br>`my_command: "--ri --with-docker-mount --image=my-image --image-version=my-tag -E my-script.py"` | *no default*
| auto-update | Toggles the auto update check. Will only perform the update after the delay | true
| auto-update-delay | Delay before running auto-update again | 2h (2 hours)
| update-version | The version to update to when running auto update | Latest fetched from Github's API

Note: *The key names are not case sensitive*

Expand Down Expand Up @@ -150,7 +153,7 @@ arguments conflicts with an argument of the desired entry point, you must place
tgf and are passed to the entry point. Any non conflicting argument will be passed to the entry point wherever it is located on the invocation
arguments.
tgf ls -- -D # Avoid -D to be interpreted by tgf as --debug-docker
tgf ls -- -D # Avoid -D to be interpreted by tgf as --debug
It is also possible to specify additional arguments through environment variable TGF_ARGS or enable debugging mode through TGF_DEBUG.
Expand All @@ -162,7 +165,7 @@ Flags:
-H, --tgf-help Show context-sensitive help (also try --help-man).
--all-versions Get versions of TGF & all others underlying utilities (alias --av)
--current-version Get current version information (alias --cv)
-D, --debug-docker Print the docker command issued
-D, --debug Print debug messages and docker commands issued
-F, --flush-cache Invoke terragrunt with --terragrunt-update-source to flush the cache
--get-image-name Just return the resulting image name (alias --gi)
--interactive On by default, use --no-interactive or --no-it to disable launching Docker in interactive mode or set
Expand All @@ -188,6 +191,7 @@ Flags:
-P, --profile=PROFILE Set the AWS profile configuration to use
--ps-path=<path> Parameter Store path used to find AWS common configuration shared by a team or set TGF_SSM_PATH
-T, --tag=latest Use a different tag of docker image instead of the default one
--update Run auto update script
```

Example:
Expand Down
84 changes: 84 additions & 0 deletions auto_update.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"fmt"
"os"
"runtime"
"time"

"github.com/blang/semver"
)

const locallyBuilt = "(Locally Built)"
const autoUpdateFile = "TGFAutoUpdate"

//go:generate moq -out runner_updater_moq_test.go . RunnerUpdater

// RunnerUpdater allows flexibility for testing
type RunnerUpdater interface {
LogDebug(format string, args ...interface{})
GetUpdateVersion() (string, error)
GetLastRefresh(file string) time.Duration
SetLastRefresh(file string)
ShouldUpdate() bool
DoUpdate(url string) (err error)
Run() int
Restart() int
}

// RunWithUpdateCheck checks if an update is due, checks if current version is outdated and performs update if needed
func RunWithUpdateCheck(c RunnerUpdater) int {
if !c.ShouldUpdate() {
return c.Run()
}

c.LogDebug("Comparing local and latest versions...")
c.SetLastRefresh(autoUpdateFile)
updateVersion, err := c.GetUpdateVersion()
if err != nil {
printError("Error fetching update version: %v", err)
return c.Run()
}
latestVersion, err := semver.Make(updateVersion)
if err != nil {
printError(`Semver error on retrieved version "%s" : %v`, updateVersion, err)
return c.Run()
}

currentVersion, err := semver.Make(version)
if err != nil {
printWarning(`Semver error on current version "%s": %v`, version, err)
return c.Run()
}

if currentVersion.GTE(latestVersion) {
c.LogDebug("Your current version (%v) is up to date.", currentVersion)
return c.Run()
}

url := PlatformZipURL(latestVersion.String())

executablePath, err := os.Executable()
if err != nil {
printError("Executable path error: %v", err)
}

printWarning("Updating %s from %s ==> %v", executablePath, version, latestVersion)
if err := c.DoUpdate(url); err != nil {
printError("Failed update for %s: %v", url, err)
return c.Run()
}

c.LogDebug("TGF updated to %v", latestVersion)
printWarning("TGF is restarting...")
return c.Restart()
}

// PlatformZipURL compute the uri pointing at the given version of tgf zip
func PlatformZipURL(version string) string {
name := runtime.GOOS
if name == "darwin" {
name = "macOS"
}
return fmt.Sprintf("https://github.com/coveo/tgf/releases/download/v%[1]s/tgf_%[1]s_%[2]s_64-bits.zip", version, name)
}
162 changes: 162 additions & 0 deletions auto_update_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package main

import (
"fmt"
"testing"
"time"

"github.com/stretchr/testify/assert"
)

func setupUpdaterMock(localVersion string, latestVersion string) *RunnerUpdaterMock {
version = localVersion
mockUpdater := &RunnerUpdaterMock{
GetUpdateVersionFunc: func() (string, error) { return latestVersion, nil }, // Remote version
LogDebugFunc: func(format string, args ...interface{}) {},
GetLastRefreshFunc: func(string) time.Duration { return 0 * time.Hour }, // Force update
SetLastRefreshFunc: func(string) {},
ShouldUpdateFunc: func() bool { return true },
RunFunc: func() int { return 0 },
RestartFunc: func() int { return 0 },
DoUpdateFunc: func(url string) (err error) { return nil },
}

return mockUpdater
}

func (mockUpdater *RunnerUpdaterMock) LogDebugCalledWith(arg string) bool {
for _, call := range mockUpdater.LogDebugCalls() {
if call.Format == arg {
return true
}
}
return false
}

func TestAutoUpdateLower(t *testing.T) {
mockUpdater := setupUpdaterMock("1.20.0", "1.21.0")
RunWithUpdateCheck(mockUpdater)
assert.True(t, mockUpdater.LogDebugCalledWith("TGF updated to %v"), `"TGF updated" never logged`)
assert.Equal(t, len(mockUpdater.RunCalls()), 0, "Auto update bypassed")
assert.NotEqual(t, len(mockUpdater.RestartCalls()), 0, "Application did not restart")
}

func TestAutoUpdateEqual(t *testing.T) {
mockUpdater := setupUpdaterMock("1.21.0", "1.21.0")
RunWithUpdateCheck(mockUpdater)
assert.True(t, mockUpdater.LogDebugCalledWith("Your current version (%v) is up to date."), `"TGF updated" never logged`)
assert.NotEqual(t, len(mockUpdater.RunCalls()), 0, "Auto update was not bypassed")
assert.Equal(t, len(mockUpdater.RestartCalls()), 0, "Application was restarted")
}

func TestAutoUpdateHigher(t *testing.T) {
mockUpdater := setupUpdaterMock("1.21.0", "1.20.0")
RunWithUpdateCheck(mockUpdater)
assert.True(t, mockUpdater.LogDebugCalledWith("Your current version (%v) is up to date."), `"TGF updated" never logged`)
assert.NotEqual(t, len(mockUpdater.RunCalls()), 0, "Auto update was not bypassed")
assert.Equal(t, len(mockUpdater.RestartCalls()), 0, "Application was restarted")
}

func ExampleRunWithUpdateCheck_githubApiError() {
mockUpdater := setupUpdaterMock("1.20.0", "1.21.0")
mockUpdater.GetUpdateVersionFunc = func() (string, error) { return "", fmt.Errorf("API error") }
ErrPrintln = fmt.Println
RunWithUpdateCheck(mockUpdater)
// Output:
// Error fetching update version: API error
}

func ExampleRunWithUpdateCheck_githubApiBadVersionString() {
mockUpdater := setupUpdaterMock("1.20.0", "not a number")
ErrPrintln = fmt.Println
RunWithUpdateCheck(mockUpdater)
// Output:
// Semver error on retrieved version "not a number" : No Major.Minor.Patch elements found
}

func ExampleRunWithUpdateCheck_badVersionStringLocal() {
mockUpdater := setupUpdaterMock("not a number", "1.21.0")
ErrPrintln = fmt.Println
RunWithUpdateCheck(mockUpdater)
// Output:
// Semver error on current version "not a number": No Major.Minor.Patch elements found

}

func ExampleTGFConfig_ShouldUpdate_forceConfiglocal() {
version = locallyBuilt
cfg := &TGFConfig{
tgf: &TGFApplication{
DebugMode: true,
},
AutoUpdate: true,
}

ErrPrintf = fmt.Printf
cfg.ShouldUpdate()
// Output:
// Running locally. Bypassing update version check.
}

func ExampleTGFConfig_ShouldUpdate_forceCliLocal() {
version = locallyBuilt
cfg := &TGFConfig{
tgf: &TGFApplication{
AutoUpdateSet: true,
AutoUpdate: true,
DebugMode: true,
},
}

ErrPrintf = fmt.Printf
cfg.ShouldUpdate()
// Output:
// Auto update is forced locally. Checking version...
}

func ExampleTGFConfig_ShouldUpdate_forceOffCli() {
cfg := &TGFConfig{
tgf: &TGFApplication{
AutoUpdateSet: true,
AutoUpdate: false,
DebugMode: true,
},
}

ErrPrintf = fmt.Printf
cfg.ShouldUpdate()
// Output:
// Auto update is force disabled. Bypassing update version check.
}

func ExampleTGFConfig_ShouldUpdate_forceConfig() {
version = "1.1.1"
cfg := &TGFConfig{
tgf: &TGFApplication{
AutoUpdateSet: false,
DebugMode: true,
},
AutoUpdate: true,
AutoUpdateDelay: 0 * time.Hour,
}

ErrPrintf = fmt.Printf
cfg.ShouldUpdate()
// Output:
// An update is due. Checking version...
}

func ExampleTGFConfig_ShouldUpdate_forceOffConfig() {
cfg := &TGFConfig{
tgf: &TGFApplication{
AutoUpdateSet: false,
DebugMode: true,
},
AutoUpdate: false,
}

ErrPrintf = fmt.Printf
cfg.ShouldUpdate()
// Output:
// Auto update is disabled in the config. Bypassing update version check.
}
16 changes: 12 additions & 4 deletions cli.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ If any of the tgf arguments conflicts with an argument of the desired entry poin
after -- to ensure that they are not interpreted by tgf and are passed to the entry point. Any non conflicting
argument will be passed to the entry point wherever it is located on the invocation arguments.
tgf ls -- -D # Avoid -D to be interpreted by tgf as --debug-docker
tgf ls -- -D # Avoid -D to be interpreted by tgf as --debug
It is also possible to specify additional arguments through environment variable @(envArgs).
Expand Down Expand Up @@ -98,6 +98,8 @@ type TGFApplication struct {
UseLocalImage bool
WithCurrentUser bool
WithDockerMount bool
AutoUpdate bool
AutoUpdateSet bool
}

// NewTGFApplication returns an initialized copy of TGFApplication along with the parsed CLI arguments
Expand All @@ -121,7 +123,7 @@ func NewTGFApplication(args []string) *TGFApplication {
app.Flag("current-version", "Get current version information").BoolVar(&app.GetCurrentVersion)
app.Flag("all-versions", "Get versions of TGF & all others underlying utilities").BoolVar(&app.GetAllVersions)
app.Flag("logging-level", "Set the logging level (critical=0, error=1, warning=2, notice=3, info=4, debug=5, full=6)").Short('L').PlaceHolder("<level>").StringVar(&app.LoggingLevel)
app.Flag("debug-docker", "Print the docker command issued").Short('D').BoolVar(&app.DebugMode)
app.Flag("debug", "Print debug messages and docker commands issued").Short('D').Default(String(os.Getenv(envDebug)).ParseBool()).BoolVar(&app.DebugMode)
app.Flag("flush-cache", "Invoke terragrunt with --terragrunt-update-source to flush the cache").Short('F').BoolVar(&app.FlushCache)
swFlagON("interactive", "Launch Docker in interactive mode").Alias("it").BoolVar(&app.DockerInteractive)
swFlagON("docker-build", "Enable docker build instructions configured in the config files").BoolVar(&app.DockerBuild)
Expand All @@ -138,6 +140,7 @@ func NewTGFApplication(args []string) *TGFApplication {
app.Flag("ssm-path", "Parameter Store path used to find AWS common configuration shared by a team").PlaceHolder("<path>").Default(defaultSSMParameterFolder).StringVar(&app.PsPath)
app.Flag("config-files", "Set the files to look for (default: "+remoteDefaultConfigPath+")").PlaceHolder("<files>").StringVar(&app.ConfigFiles)
app.Flag("config-location", "Set the configuration location").PlaceHolder("<path>").StringVar(&app.ConfigLocation)
app.Flag("update", "Run auto update script").IsSetByUser(&app.AutoUpdateSet).BoolVar(&app.AutoUpdate)

kingpin.CommandLine = app.Application
kingpin.HelpFlag = app.GetFlag("help-tgf")
Expand Down Expand Up @@ -205,8 +208,13 @@ func (app *TGFApplication) ShowHelp(c *kingpin.ParseContext) error {
// Run execute the application
func (app *TGFApplication) Run() int {
if app.GetCurrentVersion {
Printf("tgf v%s\n", version)
if version == locallyBuilt {
Printf("tgf (built from source)\n")
} else {
Printf("tgf v%s\n", version)
}
return 0
}
return InitConfig(app).Run()

return RunWithUpdateCheck(InitConfig(app))
}
Loading

0 comments on commit b493054

Please sign in to comment.