diff --git a/README.md b/README.md index 327b1113..e11c9ce7 100644 --- a/README.md +++ b/README.md @@ -644,6 +644,8 @@ Apache License, Version 2.0 - https://github.com/c-bata/go-prompt - https://github.com/vultr/govultr - https://github.com/Azure/azure-sdk-for-go +- https://github.com/blang/semver +- https://github.com/tcnksm/go-latest ### Legal Disclaimer 👮 diff --git a/cmd/minectl/create.go b/cmd/minectl/create.go index 5d52dd36..436c84f5 100644 --- a/cmd/minectl/create.go +++ b/cmd/minectl/create.go @@ -2,7 +2,6 @@ package minectl import ( "fmt" - "log" "os" "github.com/minectl/pkg/common" @@ -24,7 +23,7 @@ var createCmd = &cobra.Command{ Short: "Create an Minecraft Server.", Example: `mincetl create \ --filename server-do.yaml`, - RunE: runCreate, + RunE: RunFunc(runCreate), SilenceUsage: true, SilenceErrors: true, } @@ -39,7 +38,7 @@ func runCreate(cmd *cobra.Command, _ []string) error { } p, err := provisioner.NewProvisioner(filename) if err != nil { - log.Fatal(err) + fmt.Println(err) } wait := true if cmd.Flags().Changed("wait") { @@ -47,7 +46,8 @@ func runCreate(cmd *cobra.Command, _ []string) error { } res, err := p.CreateServer(wait) if err != nil { - log.Fatal(err) + fmt.Println(err) + return nil } table := tablewriter.NewWriter(os.Stdout) table.SetHeader([]string{"ID", "NAME", "REGION", "TAGS", "IP"}) @@ -64,5 +64,5 @@ func runCreate(cmd *cobra.Command, _ []string) error { common.PrintMixedGreen("⤴️ To upload a plugin type:\n\n %s", fmt.Sprintf("minectl plugins -f %s --id %s --plugin /x.jar --destination /minecraft/plugins\n", filename, res.ID)) common.PrintMixedGreen("\n🔌 Connected to RCON type:\n\n %s", fmt.Sprintf("minectl rcon -f %s --id %s\n", filename, res.ID)) - return err + return nil } diff --git a/cmd/minectl/delete.go b/cmd/minectl/delete.go index 577d8d63..584dee07 100644 --- a/cmd/minectl/delete.go +++ b/cmd/minectl/delete.go @@ -22,7 +22,7 @@ var deleteCmd = &cobra.Command{ --filename server-do.yaml --id xxx-xxx-xxx-xxx `, - RunE: runDelete, + RunE: RunFunc(runDelete), SilenceUsage: true, SilenceErrors: true, } @@ -48,5 +48,8 @@ func runDelete(cmd *cobra.Command, _ []string) error { return err } err = newProvisioner.DeleteServer() - return err + if err != nil { + return err + } + return nil } diff --git a/cmd/minectl/list.go b/cmd/minectl/list.go index a06d5462..761b9bf3 100644 --- a/cmd/minectl/list.go +++ b/cmd/minectl/list.go @@ -23,7 +23,7 @@ var listCmd = &cobra.Command{ Example: `mincetl list \ --provider civo \ --region LON1`, - RunE: runList, + RunE: RunFunc(runList), SilenceUsage: true, SilenceErrors: true, } @@ -62,7 +62,7 @@ func runList(cmd *cobra.Command, _ []string) error { table.SetBorder(false) table.Render() } else { - fmt.Println("🤷 No server found") + return errors.New("🤷 No server found") } return nil } diff --git a/cmd/minectl/minectl.go b/cmd/minectl/minectl.go index 5985f32f..a2af52f2 100644 --- a/cmd/minectl/minectl.go +++ b/cmd/minectl/minectl.go @@ -1,11 +1,22 @@ package minectl import ( + "bytes" "fmt" "os" + "os/exec" + "path/filepath" + "regexp" + "runtime" + "strings" + "github.com/Azure/go-autorest/autorest/to" + + "github.com/blang/semver/v4" "github.com/morikuni/aec" + "github.com/pkg/errors" "github.com/spf13/cobra" + "github.com/tcnksm/go-latest" ) var ( @@ -18,10 +29,196 @@ func init() { minectlCmd.AddCommand(versionCmd) } +var updateCheckResult chan *string + var minectlCmd = &cobra.Command{ Use: "minectl", Short: "Create Minecraft Server on different cloud provider.", Run: runMineCtl, + CompletionOptions: cobra.CompletionOptions{ + DisableDefaultCmd: false, + }, + PersistentPostRun: func(cmd *cobra.Command, args []string) { + fmt.Println("PersistentPostRun") + }, + PersistentPreRunE: func(cmd *cobra.Command, args []string) error { + var waitForUpdateCheck bool + defer func() { + if !waitForUpdateCheck { + close(updateCheckResult) + } + }() + + updateCheckResult = make(chan *string) + waitForUpdateCheck = true + go func() { + updateCheckResult <- checkForUpdate() + close(updateCheckResult) + }() + return nil + }, + PersistentPostRunE: func(cmd *cobra.Command, args []string) error { + checkVersionMsg, ok := <-updateCheckResult + if ok && checkVersionMsg != nil { + fmt.Println() + fmt.Println(to.String(checkVersionMsg)) + } + return nil + }, +} + +func isDevVersion(s semver.Version) bool { + if len(s.Pre) == 0 { + return false + } + + devStrings := regexp.MustCompile(`alpha|beta|dev|rc`) + return !s.Pre[0].IsNum && devStrings.MatchString(s.Pre[0].VersionStr) +} + +func isBrewInstall(exe string) (bool, error) { + if runtime.GOOS != "darwin" { + return false, nil + } + + exePath, err := filepath.EvalSymlinks(exe) + if err != nil { + return false, err + } + + brewBin, err := exec.LookPath("brew") + if err != nil { + return false, err + } + + brewPrefixCmd := exec.Command(brewBin, "--prefix", "minectl") + + var stdout bytes.Buffer + var stderr bytes.Buffer + brewPrefixCmd.Stdout = &stdout + brewPrefixCmd.Stderr = &stderr + if err = brewPrefixCmd.Run(); err != nil { + if ee, ok := err.(*exec.ExitError); ok { + ee.Stderr = stderr.Bytes() + } + return false, errors.Wrapf(err, "'brew --prefix minectl' failed") + } + + brewPrefixCmdOutput := strings.TrimSpace(stdout.String()) + if brewPrefixCmdOutput == "" { + return false, errors.New("trimmed output from 'brew --prefix minectl' is empty") + } + + brewPrefixPath, err := filepath.EvalSymlinks(brewPrefixCmdOutput) + if err != nil { + return false, err + } + + brewPrefixExePath := filepath.Join(brewPrefixPath, "minectl") + return exePath == brewPrefixExePath, nil +} + +func runPostCommandHooks(c *cobra.Command, args []string) error { + if c.PostRunE != nil { + if err := c.PostRunE(c, args); err != nil { + return err + } + } else if c.PostRun != nil { + c.PostRun(c, args) + } + for p := c; p != nil; p = p.Parent() { + if p.PersistentPostRunE != nil { + if err := p.PersistentPostRunE(c, args); err != nil { + return err + } + break + } else if p.PersistentPostRun != nil { + p.PersistentPostRun(c, args) + break + } + } + return nil +} + +func RunFunc(run func(cmd *cobra.Command, args []string) error) func(*cobra.Command, []string) error { + return func(cmd *cobra.Command, args []string) error { + if res := run(cmd, args); res != nil { + fmt.Println(res) + if postRunErr := runPostCommandHooks(cmd, args); postRunErr != nil { + fmt.Println(res) + } + os.Exit(1) + } + os.Exit(0) + return nil + } +} + +func getUpgradeCommand() string { + exe, err := os.Executable() + if err != nil { + return "" + } + + isBrew, err := isBrewInstall(exe) + if err != nil { + fmt.Printf("error determining if the running executable was installed with brew: %s", err) + } + if isBrew { + return "$ brew upgrade minectl" + } + + if runtime.GOOS != "windows" { + return "$ curl -sSL https://get.minectl.dev | sh" + } + return "" +} + +func getUpgradeMessage(latest *semver.Version, current *semver.Version) *string { + cmd := getUpgradeCommand() + msg := fmt.Sprintf("A new version of minectl is available. To upgrade from version '%s' to '%s', ", current, latest) + if cmd != "" { + msg += "run \n " + cmd + "\n\nor " + } + + msg += "visit https://github.com/dirien/minectl#installing-minectl- for manual instructions." + return &msg +} + +func getCLIVersionInfo(current *semver.Version) (*semver.Version, error) { + githubTag := &latest.GithubTag{ + Owner: "dirien", + Repository: "minectl", + } + + res, err := latest.Check(githubTag, current.String()) + if err != nil { + return nil, err + } + version, err := semver.New(res.Current) + if err != nil { + return nil, err + } + return version, nil +} + +func checkForUpdate() *string { + curVer, err := semver.ParseTolerant(getVersion()) + if err != nil { + fmt.Printf("error parsing current version: %s", err) + } + if isDevVersion(curVer) { + return nil + } + latestVer, err := getCLIVersionInfo(&curVer) + if err != nil { + return nil + } + if latestVer.GT(curVer) { + return getUpgradeMessage(latestVer, &curVer) + } + + return nil } var versionCmd = &cobra.Command{ @@ -34,16 +231,14 @@ func getVersion() string { if len(Version) != 0 { return Version } - return "dev" + return "0.1.0-dev" } func parseBaseCommand(_ *cobra.Command, _ []string) { printLogo() - fmt.Println("Version:", getVersion()) fmt.Println("Git Commit:", GitCommit) fmt.Println("Build date:", Date) - os.Exit(0) } func Execute(version, gitCommit, date string) error { diff --git a/cmd/minectl/plugins.go b/cmd/minectl/plugins.go index ad02cb93..5426c97a 100644 --- a/cmd/minectl/plugins.go +++ b/cmd/minectl/plugins.go @@ -1,8 +1,6 @@ package minectl import ( - "log" - "github.com/minectl/pkg/provisioner" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -39,7 +37,7 @@ var pluginCmd = &cobra.Command{ --id xxx-xxx-xxx-xxx --plugin plugin.jar --destination /minecraft/mods`, - RunE: runPlugin, + RunE: RunFunc(runPlugin), SilenceUsage: true, SilenceErrors: true, } @@ -71,7 +69,7 @@ func runPlugin(cmd *cobra.Command, _ []string) error { } p, err := provisioner.NewProvisioner(filename, id) if err != nil { - log.Fatal(err) + return err } plugin, _ := cmd.Flags().GetString("plugin") destination, _ := cmd.Flags().GetString("destination") diff --git a/cmd/minectl/rcon.go b/cmd/minectl/rcon.go index 074ecfdd..3d90d385 100644 --- a/cmd/minectl/rcon.go +++ b/cmd/minectl/rcon.go @@ -1,8 +1,6 @@ package minectl import ( - "log" - "github.com/minectl/pkg/provisioner" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -20,7 +18,7 @@ var rconCmd = &cobra.Command{ Example: `mincetl rcon \ --filename server-do.yaml \ --id xxxx`, - RunE: runRCON, + RunE: RunFunc(runRCON), SilenceUsage: true, SilenceErrors: true, } @@ -42,11 +40,11 @@ func runRCON(cmd *cobra.Command, _ []string) error { } p, err := provisioner.NewProvisioner(filename, id) if err != nil { - log.Fatal(err) + return err } err = p.DoRCON() if err != nil { - log.Fatal(err) + return err } return nil } diff --git a/cmd/minectl/update.go b/cmd/minectl/update.go index 62d1eecb..297f10e9 100644 --- a/cmd/minectl/update.go +++ b/cmd/minectl/update.go @@ -1,8 +1,6 @@ package minectl import ( - "log" - "github.com/minectl/pkg/provisioner" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -20,7 +18,7 @@ var updateCmd = &cobra.Command{ Example: `mincetl update \ --filename server-do.yaml --id xxx-xxx-xxx-xxx`, - RunE: runUpdate, + RunE: RunFunc(runUpdate), SilenceUsage: true, SilenceErrors: true, } @@ -42,7 +40,7 @@ func runUpdate(cmd *cobra.Command, _ []string) error { } p, err := provisioner.NewProvisioner(filename, id) if err != nil { - log.Fatal(err) + return err } err = p.UpdateServer() if err != nil { diff --git a/go.mod b/go.mod index c1d699fd..0aa083b2 100644 --- a/go.mod +++ b/go.mod @@ -12,6 +12,7 @@ require ( github.com/Masterminds/semver v1.5.0 // indirect github.com/Masterminds/sprig v2.22.0+incompatible github.com/Tnze/go-mc v1.17.0 + github.com/blang/semver/v4 v4.0.0 // indirect github.com/briandowns/spinner v1.16.0 github.com/c-bata/go-prompt v0.2.6 github.com/civo/civogo v0.2.49 @@ -19,8 +20,10 @@ require ( github.com/dirien/ovh-go-sdk v0.1.1 github.com/fatih/color v1.12.0 github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect + github.com/google/go-github v17.0.0+incompatible // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.0 + github.com/hashicorp/go-version v1.3.0 // indirect github.com/hetznercloud/hcloud-go v1.29.1 github.com/huandu/xstrings v1.3.2 // indirect github.com/imdario/mergo v0.3.11 // indirect @@ -39,6 +42,7 @@ require ( github.com/sethvargo/go-password v0.2.0 github.com/spf13/cobra v1.2.1 github.com/stretchr/testify v1.7.0 + github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e // indirect github.com/vultr/govultr/v2 v2.7.1 github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect github.com/xeipuuv/gojsonschema v1.2.0 diff --git a/go.sum b/go.sum index 1ebbf624..b5d1fe8c 100644 --- a/go.sum +++ b/go.sum @@ -98,6 +98,8 @@ github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmV github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= +github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= +github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/briandowns/spinner v1.16.0 h1:DFmp6hEaIx2QXXuqSJmtfSBSAjRmpGiKG6ip2Wm/yOs= github.com/briandowns/spinner v1.16.0/go.mod h1:QOuQk7x+EaDASo80FEXwlwiA+j/PPIcX3FScO+3/ZPQ= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= @@ -216,6 +218,8 @@ github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= +github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= @@ -274,6 +278,8 @@ github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerX github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.3.0 h1:McDWVJIU/y+u1BRV06dPaLfLCaT7fUTJLp5r04x7iNw= +github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= @@ -425,6 +431,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e h1:IWllFTiDjjLIf2oeKxpIUmtiDV5sn71VgeQgg6vcE7k= +github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e/go.mod h1:d7u6HkTYKSv5m6MCKkOQlHwaShTMl3HjqSGW3XtVhXM= github.com/vultr/govultr/v2 v2.7.1 h1:uF9ERet++Gb+7Cqs3p1P6b6yebeaZqVd7t5P2uZCaJU= github.com/vultr/govultr/v2 v2.7.1/go.mod h1:BvOhVe6/ZpjwcoL6/unkdQshmbS9VGbowI4QT+3DGVU= github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= diff --git a/main.go b/main.go index 719c09c1..86d8830e 100644 --- a/main.go +++ b/main.go @@ -16,6 +16,5 @@ var ( func main() { if err := minectl.Execute(version, commit, date); err != nil { fmt.Fprintf(os.Stderr, "%s\n", err.Error()) - os.Exit(1) } } diff --git a/pkg/cloud/azure/azure.go b/pkg/cloud/azure/azure.go index cab594ea..7e255551 100644 --- a/pkg/cloud/azure/azure.go +++ b/pkg/cloud/azure/azure.go @@ -6,10 +6,11 @@ import ( "encoding/json" "fmt" "io/ioutil" - "log" "path/filepath" "strings" + "github.com/pkg/errors" + "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2021-04-01/compute" "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2021-02-01/network" "github.com/Azure/azure-sdk-for-go/services/resources/mgmt/2020-10-01/resources" @@ -36,7 +37,7 @@ func NewAzure(authFile string) (*Azure, error) { } authInfo, err := readJSON(authFile) if err != nil { - log.Fatalf("Failed to read JSON: %+v", err) + return nil, errors.Wrap(err, "Failed to read JSON") } tmpl, err := minctlTemplate.NewTemplateCloudConfig() if err != nil { @@ -52,7 +53,7 @@ func NewAzure(authFile string) (*Azure, error) { func readJSON(path string) (*map[string]interface{}, error) { data, err := ioutil.ReadFile(path) if err != nil { - log.Fatalf("failed to read file: %v", err) + return nil, errors.Wrap(err, "Failed to read file") } contents := make(map[string]interface{}) _ = json.Unmarshal(data, &contents)