Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 5 additions & 4 deletions cmd/scw/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,13 +4,14 @@ import (
"os"
"runtime"

"github.com/hashicorp/go-version"
"github.com/scaleway/scaleway-cli/internal/core"
autocompleteNamespace "github.com/scaleway/scaleway-cli/internal/namespaces/autocomplete"
configNamespace "github.com/scaleway/scaleway-cli/internal/namespaces/config"
initNamespace "github.com/scaleway/scaleway-cli/internal/namespaces/init"
"github.com/scaleway/scaleway-cli/internal/namespaces/instance/v1"
"github.com/scaleway/scaleway-cli/internal/namespaces/marketplace/v1"
"github.com/scaleway/scaleway-cli/internal/namespaces/version"
versionNamespace "github.com/scaleway/scaleway-cli/internal/namespaces/version"
"github.com/scaleway/scaleway-cli/internal/sentry"
)

Expand All @@ -26,7 +27,7 @@ var (

func main() {
buildInfo := &core.BuildInfo{
Version: Version,
Version: version.Must(version.NewSemver(Version)), // panic when version does not respect semantic versionning
BuildDate: BuildDate,
GoVersion: GoVersion,
GitBranch: GitBranch,
Expand All @@ -35,7 +36,7 @@ func main() {
GoArch: GoArch,
}

// catch every panic after this line
// Catch every panic after this line. This will send an anonymous report on Scaleway's sentry.
defer sentry.RecoverPanicAndSendReport(buildInfo)

// Import all commands available in CLI from various packages.
Expand All @@ -45,7 +46,7 @@ func main() {
commands.Merge(configNamespace.GetCommands())
commands.Merge(marketplace.GetCommands())
commands.Merge(autocompleteNamespace.GetCommands())
commands.Merge(version.GetCommands())
commands.Merge(versionNamespace.GetCommands())

exitCode, _, _ := core.Bootstrap(&core.BootstrapConfig{
Args: os.Args,
Expand Down
3 changes: 2 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ require (
github.com/fatih/color v1.7.0
github.com/getsentry/raven-go v0.2.0
github.com/hashicorp/go-multierror v1.0.0
github.com/hashicorp/go-version v1.2.0
github.com/kr/pretty v0.1.0 // indirect
github.com/mattn/go-colorable v0.1.2 // indirect
github.com/mattn/go-isatty v0.0.9
github.com/pkg/errors v0.9.1 // indirect
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200129095835-23f29e96b36e
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200130170711-05d27d10a3b8
github.com/sergi/go-diff v1.0.0 // indirect
github.com/spf13/cobra v0.0.5
github.com/spf13/pflag v1.0.5
Expand Down
6 changes: 4 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/U
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-multierror v1.0.0 h1:iVjPR7a6H0tWELX5NxNe7bYopibicUzc7uPribsnS6o=
github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk=
github.com/hashicorp/go-version v1.2.0 h1:3vNe/fWF5CBgRIguda1meWhsZHy3m8gCJ5wx+dIzX/E=
github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=
Expand Down Expand Up @@ -70,8 +72,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/russross/blackfriday v1.5.2 h1:HyvC0ARfnZBqnXwABFeSZHpKvJHJJfPz81GNueLj0oo=
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200129095835-23f29e96b36e h1:UAYWUW1xKDW6k/KzLWVBcklc66nQlW6lAcM4pXunFqg=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200129095835-23f29e96b36e/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200130170711-05d27d10a3b8 h1:+y+U0MHG110iL4TkTKEb3Wh07OK7ALY2XbNFxeAEb80=
github.com/scaleway/scaleway-sdk-go v1.0.0-beta.5.0.20200130170711-05d27d10a3b8/go.mod h1:CJJ5VAbozOl0yEw7nHB9+7BXTJbIn6h7W+f6Gau5IP8=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
Expand Down
47 changes: 28 additions & 19 deletions internal/core/bootstrap.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,25 +59,34 @@ func Bootstrap(config *BootstrapConfig) (exitCode int, result interface{}, err e
}

// Send Matomo telemetry when exiting the bootstrap
if (matomo.ForceTelemetry || config.BuildInfo.IsRelease()) && matomo.IsTelemetryEnabled() {
start := time.Now()
defer func() {
if meta.command == nil || meta.command.DisableTelemetry {
logger.Debugf("skipping telemetry report")
return
}
matomoErr := matomo.SendCommandTelemetry(&matomo.SendCommandTelemetryRequest{
Command: meta.command.getPath(),
Version: config.BuildInfo.Version,
ExecutionTime: time.Since(start),
})
if matomoErr != nil {
logger.Debugf("error during telemetry reporting: %s", matomoErr)
} else {
logger.Debugf("telemetry successfully sent")
}
}()
}
start := time.Now()
defer func() {
// skip telemetry report when at least one of the following criteria matches:
// - version is not a release
// - telemetry is disabled on the current command
// - telemetry is disabled from the config (user must consent)
if (!matomo.ForceTelemetry && !config.BuildInfo.IsRelease()) ||
(meta.command == nil || meta.command.DisableTelemetry) ||
!matomo.IsTelemetryEnabled() {
logger.Debugf("skipping telemetry report")
return
}
matomoErr := matomo.SendCommandTelemetry(&matomo.SendCommandTelemetryRequest{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: add a newline

Command: meta.command.getPath(),
Version: config.BuildInfo.Version.String(),
ExecutionTime: time.Since(start),
})
if matomoErr != nil {
logger.Debugf("error during telemetry reporting: %s", matomoErr)
} else {
logger.Debugf("telemetry successfully sent")
}
}()

// Check CLI new version when exiting the bootstrap
defer func() { // if we plan to remove defer, do not forget logger is not set until cobra pre init func
config.BuildInfo.checkVersion()
}()

// cobraBuilder will build a Cobra root command from a list of Command
builder := cobraBuilder{
Expand Down
106 changes: 103 additions & 3 deletions internal/core/build_info.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,20 @@
package core

import (
"io/ioutil"
"net/http"
"os"
"path/filepath"
"strings"
"time"

"github.com/hashicorp/go-version"
"github.com/scaleway/scaleway-sdk-go/logger"
"github.com/scaleway/scaleway-sdk-go/scw"
)

type BuildInfo struct {
Version string
Version *version.Version
BuildDate string
GoVersion string
GitBranch string
Expand All @@ -14,9 +23,100 @@ type BuildInfo struct {
GoOS string
}

const (
scwDisableCheckVersionEnv = "SCW_DISABLE_CHECK_VERSION"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to document it somewhere

latestVersionFileURL = "https://scw-devtools.s3.nl-ams.scw.cloud/scw-cli-v2-version"
latestVersionUpdateFileLocalName = "latest-cli-version"
latestVersionRequestTimeout = 1 * time.Second
)

// IsRelease returns true when the version of the CLI is an official release:
// - version must be non-empty (exclude tests)
// - version must not contain label (e.g. '+dev')
// - version must not contain metadata (e.g. '+dev')
func (b *BuildInfo) IsRelease() bool {
return b.Version != "" && !strings.Contains(b.Version, "+")
return b.Version != nil && b.Version.Metadata() == ""
}

func (b *BuildInfo) checkVersion() {
if !b.IsRelease() || os.Getenv(scwDisableCheckVersionEnv) == "true" {
logger.Debugf("skipping check version")
return
}

latestVersionUpdateFilePath := getLatestVersionUpdateFilePath()

// do nothing if last refresh at during the last 24h
if wasFileModifiedLast24h(latestVersionUpdateFilePath) {
logger.Debugf("version was already checked during past 24 hours")
return
}

// do nothing if we cannot create the file
if !createAndCloseFile(latestVersionUpdateFilePath) {
return
}

// pull latest version
latestVersion, err := getLatestVersion()
if err != nil {
logger.Debugf("failed to retrieve latest version: %s", err)
return
}

if b.Version.LessThan(latestVersion) {
logger.Infof("a new version of scw is available (%s), beware that you are currently running %v", latestVersion, b.Version)
} else {
logger.Debugf("version is up to date (%s)", b.Version)
}
}

func getLatestVersionUpdateFilePath() string {
return filepath.Join(scw.GetCacheDirectory(), latestVersionUpdateFileLocalName)
}

// getLatestVersion attempt to read the latest version of the remote file at latestVersionFileURL.
func getLatestVersion() (*version.Version, error) {
resp, err := (&http.Client{
Timeout: latestVersionRequestTimeout,
}).Get(latestVersionFileURL)
if resp != nil {
defer resp.Body.Close()
}
if err != nil {
return nil, err
}
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}

return version.NewSemver(strings.Trim(string(body), "\n"))
}

// wasFileModifiedLast24h checks whether the file has been updated during last 24 hours.
func wasFileModifiedLast24h(path string) bool {
stat, err := os.Stat(path)
if err != nil {
return false
}

yesterday := time.Now().AddDate(0, 0, -1)
lastUpdate := stat.ModTime()
return lastUpdate.After(yesterday)
}

// createAndCloseFile creates a file and closes it. It returns true on succeed, false on failure.
func createAndCloseFile(path string) bool {
err := os.MkdirAll(filepath.Dir(path), 0700)
if err != nil {
logger.Debugf("failed creating path %s: %s", path, err)
}
newFile, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_RDWR, 0600)
if err != nil {
logger.Debugf("failed creating file %s: %s", path, err)
return false
}

newFile.Close()
return true
}
69 changes: 69 additions & 0 deletions internal/core/build_info_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package core

import (
"context"
"fmt"
"os"
"reflect"
"testing"

"github.com/hashicorp/go-version"
"github.com/scaleway/scaleway-cli/internal/args"
)

var fakeCommand = &Command{
Namespace: "plop",
DisableTelemetry: true,
ArgsType: reflect.TypeOf(args.RawArgs{}),
Run: func(ctx context.Context, argsI interface{}) (i interface{}, e error) {
return &SuccessResult{}, nil
},
}

func deleteLatestVersionUpdateFile(*BeforeFuncCtx) error {
os.Remove(getLatestVersionUpdateFilePath())
return nil
}

func Test_CheckVersion(t *testing.T) {
t.Run("Outdated version", Test(&TestConfig{
Commands: NewCommands(fakeCommand),
BuildInfo: BuildInfo{
Version: version.Must(version.NewSemver("v1.20")),
},
BeforeFunc: deleteLatestVersionUpdateFile,
Cmd: "scw plop",
Check: TestCheckCombine(
TestCheckStderrGolden(),
),
}))

t.Run("Up to date version", Test(&TestConfig{
Commands: NewCommands(fakeCommand),
BuildInfo: BuildInfo{
Version: version.Must(version.NewSemver("v99.99")),
},
BeforeFunc: deleteLatestVersionUpdateFile,
Cmd: "scw plop -D",
Check: TestCheckCombine(
TestCheckStderrGolden(),
),
}))

t.Run("Already checked", Test(&TestConfig{
Commands: NewCommands(fakeCommand),
BuildInfo: BuildInfo{
Version: version.Must(version.NewSemver("v1.0")),
},
BeforeFunc: func(ctx *BeforeFuncCtx) error {
if createAndCloseFile(getLatestVersionUpdateFilePath()) {
return nil
}
return fmt.Errorf("failed to create latestVersionUpdateFile")
},
Cmd: "scw plop -D",
Check: TestCheckCombine(
TestCheckStderrGolden(),
),
}))
}
2 changes: 1 addition & 1 deletion internal/core/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ func createClient(meta *meta) (*scw.Client, error) {
}

opts := []scw.ClientOption{
scw.WithUserAgent("scaleway-cli/" + meta.BuildInfo.Version),
scw.WithUserAgent("scaleway-cli/" + meta.BuildInfo.Version.String()),
scw.WithProfile(profile),
}

Expand Down
5 changes: 3 additions & 2 deletions internal/core/cobra_utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,10 +115,11 @@ func cobraPreRunInitMeta(ctx context.Context, cmd *Command) func(cmd *cobra.Comm
var err error
meta := extractMeta(ctx)

// enable debug mode
logLevel := logger.LogLevelInfo
if meta.DebugModeFlag {
logger.EnableDebugMode()
logLevel = logger.LogLevelDebug // enable debug mode
}
logger.DefaultLogger.Init(meta.stderr, logLevel)

meta.Printer, err = printer.New(meta.PrinterTypeFlag, meta.stdout, meta.stderr)
if err != nil {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: 2019/12/09 16:04:07 marshalling type '*core.SuccessResult'
DEBUG: 2019/12/09 16:04:07 version was already checked during past 24 hours
DEBUG: 2019/12/09 16:04:07 skipping telemetry report
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
INFO: 2019/12/09 16:04:07 a new version of scw is available (2.0.0-alpha1), beware that you are currently running 1.20.0
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
DEBUG: 2019/12/09 16:04:07 marshalling type '*core.SuccessResult'
DEBUG: 2019/12/09 16:04:07 version is up to date (99.99.0)
DEBUG: 2019/12/09 16:04:07 skipping telemetry report
Loading