From 8fcf9881b71924d0b52d9ad8e516589247ce4119 Mon Sep 17 00:00:00 2001 From: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:10:24 -0600 Subject: [PATCH 1/3] update nex CLI command Signed-off-by: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> --- cmd/nex/cli.go | 10 ++- cmd/nex/helpers.go | 23 ++++++ cmd/nex/logger.go | 2 +- cmd/nex/main.go | 5 +- cmd/nex/main_test.go | 20 ++--- cmd/nex/nats.go | 2 +- cmd/nex/node.go | 31 ++++++-- cmd/nex/table.go | 8 ++ cmd/nex/upgrade.go | 171 +++++++++++++++++++++++++++++++++++++++- cmd/nex/upgrade_test.go | 54 +++++++++++++ 10 files changed, 301 insertions(+), 25 deletions(-) create mode 100644 cmd/nex/helpers.go create mode 100644 cmd/nex/upgrade_test.go diff --git a/cmd/nex/cli.go b/cmd/nex/cli.go index 97e73498..dd5ceb40 100644 --- a/cmd/nex/cli.go +++ b/cmd/nex/cli.go @@ -11,10 +11,12 @@ type Globals struct { GlobalLogger `prefix:"logger." group:"Logger Configuration"` GlobalNats `prefix:"nats." group:"NATS Configuration"` - Config kong.ConfigFlag `help:"Configuration file to load" placeholder:"./nex.config.json"` - Version kong.VersionFlag `help:"Print version information"` - Namespace string `env:"NEX_NAMESPACE" placeholder:"default" help:"Specifies namespace when running nex commands"` - Check bool `help:"Print the current configuration"` + Config kong.ConfigFlag `help:"Configuration file to load" placeholder:"./nex.config.json"` + Version kong.VersionFlag `help:"Print version information"` + Namespace string `env:"NEX_NAMESPACE" placeholder:"default" help:"Specifies namespace when running nex commands"` + Check bool `help:"Print the current configuration"` + DisableUpgradeCheck bool `env:"NEX_DISABLE_UPGRADE_CHECK" name:"disable-upgrade-check" help:"Disable the upgrade check"` + AutoUpgrade bool `env:"NEX_AUTO_UPGRADE" name:"auto-upgrade" help:"Automatically upgrade the nex CLI when a new version is available"` } type GlobalLogger struct { diff --git a/cmd/nex/helpers.go b/cmd/nex/helpers.go new file mode 100644 index 00000000..dcad82fb --- /dev/null +++ b/cmd/nex/helpers.go @@ -0,0 +1,23 @@ +package main + +import "context" + +func checkVer(globals *Globals) error { + if globals.Check { + return nil + } + if !globals.DisableUpgradeCheck { + iVer, err := versionCheck() + if err != nil { + return err + } + if globals.AutoUpgrade { + u := Upgrade{ + installVersion: iVer, + } + globals.Check = false + return u.Run(context.Background(), globals) + } + } + return nil +} diff --git a/cmd/nex/logger.go b/cmd/nex/logger.go index 48dd3acf..b39dc6ca 100644 --- a/cmd/nex/logger.go +++ b/cmd/nex/logger.go @@ -12,7 +12,7 @@ import ( "github.com/nats-io/nats.go" ) -func configureLogger(cfg Globals, nc *nats.Conn, serverPublicKey string) *slog.Logger { +func configureLogger(cfg *Globals, nc *nats.Conn, serverPublicKey string) *slog.Logger { var handlerOpts []shandler.HandlerOption switch cfg.LogLevel { diff --git a/cmd/nex/main.go b/cmd/nex/main.go index e872aeed..1b50dc42 100644 --- a/cmd/nex/main.go +++ b/cmd/nex/main.go @@ -45,11 +45,10 @@ func main() { "versionOnly": VERSION, "defaultResourcePath": userResourcePath, }, + kong.BindTo(context.Background(), (*context.Context)(nil)), + kong.Bind(&nex.Globals), ) - ctx.BindTo(context.Background(), (*context.Context)(nil)) - ctx.BindTo(nex.Globals, (*Globals)(nil)) - err = ctx.Run() ctx.FatalIfErrorf(err) } diff --git a/cmd/nex/main_test.go b/cmd/nex/main_test.go index 059313e9..31ddd264 100644 --- a/cmd/nex/main_test.go +++ b/cmd/nex/main_test.go @@ -1,8 +1,6 @@ package main import ( - "encoding/json" - "fmt" "os" "path/filepath" "testing" @@ -12,9 +10,12 @@ import ( ) func TestCLISimple(t *testing.T) { - nex := new(NexCLI) + nex := NexCLI{} - parser := kong.Must(nex, kong.Vars(map[string]string{"versionOnly": "testing", "defaultResourcePath": "."})) + parser := kong.Must(&nex, + kong.Vars(map[string]string{"versionOnly": "testing", "defaultResourcePath": "."}), + kong.Bind(&nex.Globals), + ) kp, err := nkeys.CreatePair(nkeys.PrefixByteServer) if err != nil { t.Fatal(err) @@ -63,8 +64,12 @@ func TestCLIWithConfig(t *testing.T) { f.WriteString(config) defer f.Close() - nex := new(NexCLI) - parser := kong.Must(nex, kong.Vars(map[string]string{"versionOnly": "testing", "defaultResourcePath": "."}), kong.Configuration(kong.JSON, f.Name())) + nex := NexCLI{} + parser := kong.Must(&nex, + kong.Vars(map[string]string{"versionOnly": "testing", "defaultResourcePath": "."}), + kong.Configuration(kong.JSON, f.Name()), + kong.Bind(&nex.Globals), + ) parser.LoadConfig(f.Name()) _, err = parser.Parse([]string{"node", "up", "--config", f.Name()}) @@ -72,9 +77,6 @@ func TestCLIWithConfig(t *testing.T) { t.Fatal(err) } - jsonData, _ := json.MarshalIndent(nex, "", " ") - fmt.Println(string(jsonData)) - if string(nex.Globals.Config) != f.Name() { t.Fatal("Expected config to be loaded") } diff --git a/cmd/nex/nats.go b/cmd/nex/nats.go index 8b8cd67c..841fba32 100644 --- a/cmd/nex/nats.go +++ b/cmd/nex/nats.go @@ -7,7 +7,7 @@ import ( "github.com/nats-io/nats.go" ) -func configureNatsConnection(cfg Globals) (*nats.Conn, error) { +func configureNatsConnection(cfg *Globals) (*nats.Conn, error) { if cfg.Check { return nil, nil } diff --git a/cmd/nex/node.go b/cmd/nex/node.go index 5b17535c..bef8734b 100644 --- a/cmd/nex/node.go +++ b/cmd/nex/node.go @@ -32,6 +32,10 @@ type Preflight struct { GithubPAT string `optional:"" help:"GitHub Personal Access Token. Can be provided if rate limits are hit pulling data from Github" placeholder:"ghp_abc123..."` } +func (Preflight) AfterApply(globals *Globals) error { + return checkVer(globals) +} + func (p Preflight) Validate() error { var errs error if p.InstallVersion != "" { @@ -81,11 +85,10 @@ func (p Preflight) Validate() error { return errs } -func (p Preflight) Run(ctx context.Context, globals Globals) error { +func (p Preflight) Run(ctx context.Context, globals *Globals) error { if globals.Check { return printTable("Node Preflight Configuration", append(globals.Table(), p.Table()...)...) } - fmt.Println("run preflight") return nil } @@ -95,6 +98,10 @@ type LameDuck struct { Label map[string]string `optional:"" help:"Put all nodes with label in lameduck. Only 1 label allowed" placeholder:"nex.nexus=mynexus"` } +func (LameDuck) AfterApply(globals *Globals) error { + return checkVer(globals) +} + func (l LameDuck) Validate() error { if l.NodeID == "" && len(l.Label) == 0 { return errors.New("must provide a node ID or label") @@ -118,7 +125,7 @@ func (l LameDuck) Validate() error { return nil } -func (l LameDuck) Run(ctx context.Context, globals Globals) error { +func (l LameDuck) Run(ctx context.Context, globals *Globals) error { if globals.Check { return printTable("Node Lameduck Configuration", append(globals.Table(), l.Table()...)...) } @@ -132,11 +139,15 @@ type List struct { JSON bool `optional:"" help:"Output in JSON format"` } +func (List) AfterApply(globals *Globals) error { + return checkVer(globals) +} + func (l List) Validate() error { return nil } -func (l List) Run(ctx context.Context, globals Globals) error { +func (l List) Run(ctx context.Context, globals *Globals) error { if globals.Check { return printTable("Node List Configuration", append(globals.Table(), l.Table()...)...) } @@ -150,6 +161,10 @@ type Info struct { JSON bool `optional:"" help:"Output in JSON format"` } +func (Info) AfterApply(globals *Globals) error { + return checkVer(globals) +} + func (i Info) Validate() error { var errs error if !nkeys.IsValidPublicServerKey(i.NodeID) { @@ -158,7 +173,7 @@ func (i Info) Validate() error { return errs } -func (i Info) Run(ctx context.Context, globals Globals) error { +func (i Info) Run(ctx context.Context, globals *Globals) error { if globals.Check { return printTable("Node Info Configuration", append(globals.Table(), i.Table()...)...) } @@ -179,6 +194,10 @@ type Up struct { OtelConfig OtelConfig `embed:"" prefix:"otel." group:"OpenTelemetry Configuration"` } +func (u *Up) AfterApply(globals *Globals) error { + return checkVer(globals) +} + func (u Up) Validate() error { var errs error if u.WorkloadTypes == nil || len(u.WorkloadTypes) < 1 { @@ -187,7 +206,7 @@ func (u Up) Validate() error { return errs } -func (u Up) Run(ctx context.Context, globals Globals, n *Node) error { +func (u Up) Run(ctx context.Context, globals *Globals, n *Node) error { if globals.Check { return printTable("Node Up Configuration", append(globals.Table(), u.Table()...)...) } diff --git a/cmd/nex/table.go b/cmd/nex/table.go index b1faeada..f2b87a59 100644 --- a/cmd/nex/table.go +++ b/cmd/nex/table.go @@ -25,6 +25,8 @@ func printTable(title string, in ...table.Row) error { func (g Globals) Table() []table.Row { return []table.Row{ {"Config File", g.Config, reflect.TypeOf(g.Config).String()}, + {"Disable Upgrade Check", g.DisableUpgradeCheck, reflect.TypeOf(g.DisableUpgradeCheck).String()}, + {"Enable Auto Upgrade ", g.AutoUpgrade, reflect.TypeOf(g.AutoUpgrade).String()}, {"Nex Namespace", g.Namespace, reflect.TypeOf(g.Namespace).String()}, {"NATS Server", g.NatsServers, reflect.TypeOf(g.NatsServers).String()}, {"NATS Context", g.NatsContext, reflect.TypeOf(g.NatsContext).String()}, @@ -100,3 +102,9 @@ func (i Info) Table() []table.Row { {"JSON Output", i.JSON, reflect.TypeOf(i.JSON).String()}, } } + +func (u Upgrade) Table() []table.Row { + return []table.Row{ + {"Git Tag", u.GitTag, reflect.TypeOf(u.GitTag).String()}, + } +} diff --git a/cmd/nex/upgrade.go b/cmd/nex/upgrade.go index b2f08306..84c2cdbc 100644 --- a/cmd/nex/upgrade.go +++ b/cmd/nex/upgrade.go @@ -1,3 +1,172 @@ package main -type Upgrade struct{} +import ( + "context" + "crypto/sha256" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "log/slog" + "net/http" + "os" + "runtime" +) + +type Upgrade struct { + GitTag string `json:"tag_name" help:"Specific release tag to download"` + + installVersion string `kong:"-"` +} + +func versionCheck() (string, error) { + if COMMIT == "development" { + return "", nil + } + + res, err := http.Get("https://api.github.com/repos/synadia-io/nex/releases/latest") + if err != nil { + return "", err + } + defer res.Body.Close() + + b, err := io.ReadAll(res.Body) + if err != nil { + return "", err + } + + payload := make(map[string]interface{}) + err = json.Unmarshal(b, &payload) + if err != nil { + return "", err + } + + latestTag, ok := payload["tag_name"].(string) + if !ok { + return "", errors.New("error parsing tag_name") + } + + if latestTag != VERSION { + fmt.Printf(`================================================================ +🎉 There is a newer version [v%s] of the NEX CLI available 🎉 +To update, run: + nex upgrade +================================================================ + +`, + latestTag) + } + + return latestTag, nil +} + +func (u Upgrade) Run(ctx context.Context, globals *Globals) error { + if globals.Check { + return printTable("Node Upgrade Configuration", append(globals.Table(), u.Table()...)...) + } + + var err error + if u.GitTag == "" { + u.installVersion, err = versionCheck() + } else { + u.installVersion = u.GitTag + } + + if u.installVersion == VERSION { + // no upgrade needed + return nil + } + + if u.installVersion == "" { + return errors.New("upgrade disabled when using a developmental build of nex cli") + } + + logger := slog.New(slog.NewTextHandler(os.Stdout, nil)) + nexPath, err := os.Executable() + if err != nil { + return err + } + + f, err := os.Open(nexPath) + if err != nil { + return err + } + + // copy binary backup + f_bak, err := os.Create(nexPath + ".bak") + if err != nil { + return err + } + defer os.Remove(nexPath + ".bak") + _, err = io.Copy(f_bak, f) + if err != nil { + return err + } + f_bak.Close() + f.Close() + + restoreBackup := func() { + logger.Info("Restoring backup binary") + if err := os.Rename(nexPath+".bak", nexPath); err != nil { + logger.Error("Failed to restore backup binary", slog.Any("err", err)) + } + err = os.Chmod(nexPath, 0755) + if err != nil { + logger.Error("Failed to restore backup binary permissions", slog.Any("err", err)) + } + } + + _os := runtime.GOOS + arch := runtime.GOARCH + + url := fmt.Sprintf("https://github.com/synadia-io/nex/releases/download/%s/nex_%s_%s_%s", u.installVersion, u.installVersion, _os, arch) + resp, err := http.Get(url) + if err != nil { + return err + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("failed to download nex: %s | %s", resp.Status, url) + } + + nexBinary, err := io.ReadAll(resp.Body) + if err != nil { + return err + } + + nex, err := os.Create(nexPath + ".new") + if err != nil { + return err + } + defer os.Remove(nexPath + ".new") + + _, err = nex.Write(nexBinary) + if err != nil { + return err + } + + h := sha256.New() + if _, err := io.Copy(h, nex); err != nil { + return err + } + + shasum := hex.EncodeToString(h.Sum(nil)) + + err = os.Rename(nexPath+".new", nexPath) + if err != nil { + restoreBackup() + return err + } + + err = os.Chmod(nexPath, 0755) + if err != nil { + logger.Error("Failed to update nex binary permissions", slog.Any("err", err)) + } + + logger.Debug("New binary downloaded", slog.String("sha256", shasum)) + logger.Info("nex upgrade complete!", slog.String("new_version", u.installVersion)) + + return nil +} diff --git a/cmd/nex/upgrade_test.go b/cmd/nex/upgrade_test.go new file mode 100644 index 00000000..3daec3b1 --- /dev/null +++ b/cmd/nex/upgrade_test.go @@ -0,0 +1,54 @@ +package main + +import ( + "context" + "crypto/sha256" + "fmt" + "io" + "os" + "strings" + "testing" +) + +func setEnvironment(t *testing.T) { + t.Helper() + + VERSION = "9.9.9" + COMMIT = "abcdefg12345678test" + BUILDDATE = "2021-01-01T00:00:00Z" +} + +func TestUpdateNex(t *testing.T) { + setEnvironment(t) + nexPath, _ := os.Executable() + if !strings.HasPrefix(nexPath, os.TempDir()) { + t.Log("bailing on update nex test so real env isnt affected") + t.SkipNow() + } + + globals := new(Globals) + u := Upgrade{GitTag: "0.2.7"} + err := u.Run(context.Background(), globals) + if err != nil { + t.Fatal(err) + } + + f, err := os.Open(nexPath) + if err != nil { + t.Fatal(err) + } + defer f.Close() + + buf, err := io.ReadAll(f) + if err != nil { + t.Fatal(err) + } + + // https://github.com/synadia-io/nex/releases/download/0.2.7/nex_0.2.7_linux_amd64.sha256 + expectedSha := "143753466f83f744ccdc5dbe7e76e9539a2a5db2130cd8cb44607b55e414ee58" + s256 := sha256.Sum256(buf) + sSum := fmt.Sprintf("%x", s256) + if sSum != expectedSha { + t.Fatalf("Expected sha256 to be %s; Got %s", expectedSha, sSum) + } +} From 8bd483e71b5bf0655125ffd86588eba8815ca52f Mon Sep 17 00:00:00 2001 From: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:28:47 -0600 Subject: [PATCH 2/3] pr feedback Signed-off-by: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> --- cmd/nex/upgrade.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nex/upgrade.go b/cmd/nex/upgrade.go index 84c2cdbc..718f777a 100644 --- a/cmd/nex/upgrade.go +++ b/cmd/nex/upgrade.go @@ -49,7 +49,7 @@ func versionCheck() (string, error) { if latestTag != VERSION { fmt.Printf(`================================================================ -🎉 There is a newer version [v%s] of the NEX CLI available 🎉 +🎉 There is a newer version [v%s] of the Nex available 🎉 To update, run: nex upgrade ================================================================ From bba55096fe4a07caafba6e0f899b233ead561819 Mon Sep 17 00:00:00 2001 From: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> Date: Thu, 26 Sep 2024 14:45:55 -0600 Subject: [PATCH 3/3] better help for check Signed-off-by: Jordan Rash <15827604+jordan-rash@users.noreply.github.com> --- cmd/nex/cli.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/nex/cli.go b/cmd/nex/cli.go index dd5ceb40..925625fb 100644 --- a/cmd/nex/cli.go +++ b/cmd/nex/cli.go @@ -14,7 +14,7 @@ type Globals struct { Config kong.ConfigFlag `help:"Configuration file to load" placeholder:"./nex.config.json"` Version kong.VersionFlag `help:"Print version information"` Namespace string `env:"NEX_NAMESPACE" placeholder:"default" help:"Specifies namespace when running nex commands"` - Check bool `help:"Print the current configuration"` + Check bool `help:"Print the current values of all options without running a command"` DisableUpgradeCheck bool `env:"NEX_DISABLE_UPGRADE_CHECK" name:"disable-upgrade-check" help:"Disable the upgrade check"` AutoUpgrade bool `env:"NEX_AUTO_UPGRADE" name:"auto-upgrade" help:"Automatically upgrade the nex CLI when a new version is available"` }