From 788da9a898cf6eafa058e84f023bf744eba484a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Iv=C3=A1n=20SZKIBA?= Date: Fri, 28 Jun 2024 17:45:58 +0200 Subject: [PATCH] feat: Add spinner to indicate build in progress. --- cmd/cmd.go | 179 ++++++++++++--------------------------------- cmd/k6exec/main.go | 76 ++++++++++++++++++- cmd/state.go | 137 ++++++++++++++++++++++++++++++++++ go.mod | 6 +- go.sum | 15 +++- 5 files changed, 274 insertions(+), 139 deletions(-) create mode 100644 cmd/state.go diff --git a/cmd/cmd.go b/cmd/cmd.go index 043d9bd..66a2bdc 100644 --- a/cmd/cmd.go +++ b/cmd/cmd.go @@ -2,70 +2,18 @@ package cmd import ( - "context" _ "embed" "log/slog" - "net/url" - "os" - "github.com/grafana/k6deps" - "github.com/grafana/k6exec" "github.com/spf13/cobra" ) //go:embed help.md var help string -type options struct { - k6exec.Options - buildServiceURL string - extensionCatalogURL string - verbose bool - levelVar *slog.LevelVar -} - -func (o *options) postProcess() error { - if len(o.buildServiceURL) > 0 { - val, err := url.Parse(o.buildServiceURL) - if err != nil { - return err - } - - o.BuildServiceURL = val - } - - if len(o.extensionCatalogURL) > 0 { - val, err := url.Parse(o.extensionCatalogURL) - if err != nil { - return err - } - - o.ExtensionCatalogURL = val - } - - if o.verbose && o.levelVar != nil { - o.levelVar.Set(slog.LevelDebug) - } - - return nil -} - -//nolint:forbidigo -func (o *options) init() { - if value, found := os.LookupEnv("K6_BUILD_SERVICE_URL"); found { - o.buildServiceURL = value - } - - if value, found := os.LookupEnv("K6_EXTENSION_CATALOG_URL"); found { - o.extensionCatalogURL = value - } -} - // New creates new cobra command for exec command. func New(levelVar *slog.LevelVar) *cobra.Command { - opts := &options{levelVar: levelVar} - - opts.init() + state := newState(levelVar) root := &cobra.Command{ Use: "exec [flags] [command]", @@ -75,86 +23,58 @@ func New(levelVar *slog.LevelVar) *cobra.Command { SilenceErrors: true, DisableAutoGenTag: true, CompletionOptions: cobra.CompletionOptions{DisableDefaultCmd: true}, - RunE: func(cmd *cobra.Command, _ []string) error { return cmd.Help() }, - PersistentPreRunE: func(_ *cobra.Command, _ []string) error { return opts.postProcess() }, + PreRunE: func(cmd *cobra.Command, args []string) error { + if state.usage { + return nil + } + + state.AppName = cmd.Name() + + return state.preRunE(cmd, args) + }, + RunE: func(cmd *cobra.Command, args []string) error { + if state.usage { + return cmd.Help() + } + + return state.runE(cmd, args) + }, + PersistentPreRunE: state.persistentPreRunE, } root.SetVersionTemplate(`{{with .Name}}{{printf "%s " .}}{{end}}{{printf "%s\n" .Version}}`) - root.AddCommand(subcommands(opts)...) + for _, name := range commands { + root.AddCommand(newSubcommand(name, state)) + } flags := root.PersistentFlags() flags.StringVar( - &opts.extensionCatalogURL, + &state.extensionCatalogURL, "extension-catalog-url", - opts.extensionCatalogURL, + state.extensionCatalogURL, "URL of the k6 extension catalog to be used", ) flags.StringVar( - &opts.buildServiceURL, + &state.buildServiceURL, "build-service-url", - opts.buildServiceURL, + state.buildServiceURL, "URL of the k6 build service to be used", ) - flags.BoolVarP( - &opts.verbose, - "verbose", - "v", - false, - "enable verbose logging", - ) + flags.BoolVarP(&state.verbose, "verbose", "v", false, "enable verbose logging") + flags.BoolVarP(&state.quiet, "quiet", "q", false, "disable progress updates") + flags.BoolVar(&state.quiet, "no-color", false, "disable colored output") + flags.BoolVar(&state.usage, "usage", false, "print launcher usage") + + root.InitDefaultHelpFlag() + root.Flags().Lookup("help").Usage = "help for k6" root.MarkFlagsMutuallyExclusive("extension-catalog-url", "build-service-url") return root } -func usage(cmd *cobra.Command, args []string) { - err := exec(cmd, append(args, "-h"), new(options)) - if err != nil { - cmd.PrintErr(err) - } -} - -func exec(sub *cobra.Command, args []string, opts *options) error { - var ( - deps k6deps.Dependencies - err error - dopts k6deps.Options - ) - - if scriptname, hasScript := scriptArg(sub, args); hasScript { - dopts.Script.Name = scriptname - } - - deps, err = k6deps.Analyze(&dopts) - if err != nil { - return err - } - - cmdargs := []string{sub.Name()} - - if opts.verbose { - cmdargs = append(cmdargs, "-v") - } - - cmdargs = append(cmdargs, args...) - - cmd, err := k6exec.Command(context.Background(), cmdargs, deps, &opts.Options) - if err != nil { - return err - } - - cmd.Stderr = os.Stderr //nolint:forbidigo - cmd.Stdout = os.Stdout //nolint:forbidigo - cmd.Stdin = os.Stdin //nolint:forbidigo - - defer k6exec.CleanupState(&opts.Options) //nolint:errcheck - - return cmd.Run() -} - func scriptArg(cmd *cobra.Command, args []string) (string, bool) { if len(cmd.Annotations) == 0 { return "", false @@ -176,30 +96,23 @@ func scriptArg(cmd *cobra.Command, args []string) (string, bool) { return last, true } -func subcommands(opts *options) []*cobra.Command { - annext := map[string]string{useExtensions: "true"} - - all := make([]*cobra.Command, 0, len(commands)) +func newSubcommand(name string, state *state) *cobra.Command { + cmd := &cobra.Command{ + Use: name, + PreRunE: state.preRunE, + RunE: state.runE, + SilenceErrors: true, + SilenceUsage: true, + FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, + Hidden: true, + } + cmd.SetHelpFunc(state.helpFunc) - for _, name := range commands { - cmd := &cobra.Command{ - Use: name, - RunE: func(cmd *cobra.Command, args []string) error { return exec(cmd, args, opts) }, - SilenceErrors: true, - SilenceUsage: true, - FParseErrWhitelist: cobra.FParseErrWhitelist{UnknownFlags: true}, - Hidden: true, - } - cmd.SetHelpFunc(usage) - - if name == "run" || name == "archive" { - cmd.Annotations = annext - } - - all = append(all, cmd) + if name == "run" || name == "archive" { + cmd.Annotations = map[string]string{useExtensions: "true"} } - return all + return cmd } const useExtensions = "useExtensions" diff --git a/cmd/k6exec/main.go b/cmd/k6exec/main.go index f55ba25..3e938e7 100644 --- a/cmd/k6exec/main.go +++ b/cmd/k6exec/main.go @@ -2,10 +2,15 @@ package main import ( + "context" "log/slog" "os" + "os/signal" "strings" + "time" + "github.com/briandowns/spinner" + "github.com/fatih/color" "github.com/grafana/k6exec/cmd" sloglogrus "github.com/samber/slog-logrus/v2" "github.com/sirupsen/logrus" @@ -19,7 +24,7 @@ var ( ) func initLogging() *slog.LevelVar { - var levelVar = new(slog.LevelVar) + levelVar := new(slog.LevelVar) logrus.SetLevel(logrus.DebugLevel) @@ -32,14 +37,24 @@ func initLogging() *slog.LevelVar { } func main() { - levelVar := initLogging() - runCmd(newCmd(os.Args[1:], levelVar)) //nolint:forbidigo + runCmd(newCmd(os.Args[1:], initLogging())) //nolint:forbidigo } func newCmd(args []string, levelVar *slog.LevelVar) *cobra.Command { cmd := cmd.New(levelVar) cmd.Use = strings.Replace(cmd.Use, cmd.Name(), appname, 1) cmd.Version = version + + if len(args) == 1 && (args[0] == "-h" || args[0] == "--help") { + args[0] = "help" + } + + sp := addSpinner(cmd) + + if len(args) == 1 && args[0] == "--usage" { + sp.Disable() + } + cmd.SetArgs(args) return cmd @@ -51,3 +66,58 @@ func runCmd(cmd *cobra.Command) { os.Exit(1) //nolint:forbidigo } } + +func addSpinner(root *cobra.Command) *spinner.Spinner { + sp := spinner.New( + spinner.CharSets[11], + 200*time.Millisecond, + spinner.WithColor("cyan"), + spinner.WithWriterFile(os.Stderr), //nolint:forbidigo + ) + + red := color.New(color.FgRed) + green := color.New(color.FgGreen) + + sp.Prefix = "Preparing k6 " + sp.FinalMSG = sp.Prefix + green.Sprint("✓") + "\n" + + c := make(chan os.Signal, 1) + signal.Notify(c, os.Interrupt) + go func() { + <-c + sp.FinalMSG = sp.Prefix + red.Sprint("✗") + "\n" + + sp.Stop() + }() + + if slog.Default().Enabled(context.Background(), slog.LevelDebug) { + sp.Disable() + } + + prerun := root.PersistentPreRunE + root.PersistentPreRunE = func(cmd *cobra.Command, args []string) error { + if cmd == root && len(args) != 0 { + return nil + } + + sp.Start() + cmdpre := cmd.PreRunE + cmd.PreRunE = func(cmd *cobra.Command, args []string) error { + var err error + + if cmdpre != nil { + err = cmdpre(cmd, args) + if err != nil { + sp.FinalMSG = sp.Prefix + red.Sprint("✗") + "\n" + } + } + + sp.Stop() + return err + } + + return prerun(cmd, args) + } + + return sp +} diff --git a/cmd/state.go b/cmd/state.go new file mode 100644 index 0000000..08db724 --- /dev/null +++ b/cmd/state.go @@ -0,0 +1,137 @@ +package cmd + +import ( + "context" + "log/slog" + "net/url" + "os" + "os/exec" + + "github.com/grafana/k6deps" + "github.com/grafana/k6exec" + "github.com/spf13/cobra" +) + +type state struct { + k6exec.Options + buildServiceURL string + extensionCatalogURL string + verbose bool + quiet bool + nocolor bool + usage bool + levelVar *slog.LevelVar + + cmd *exec.Cmd +} + +//nolint:forbidigo +func newState(levelVar *slog.LevelVar) *state { + s := new(state) + + s.levelVar = levelVar + + if value, found := os.LookupEnv("K6_BUILD_SERVICE_URL"); found { + s.buildServiceURL = value + } + + if value, found := os.LookupEnv("K6_EXTENSION_CATALOG_URL"); found { + s.extensionCatalogURL = value + } + + return s +} + +func (s *state) persistentPreRunE(_ *cobra.Command, _ []string) error { + if len(s.buildServiceURL) > 0 { + val, err := url.Parse(s.buildServiceURL) + if err != nil { + return err + } + + s.Options.BuildServiceURL = val + } + + if len(s.extensionCatalogURL) > 0 { + val, err := url.Parse(s.extensionCatalogURL) + if err != nil { + return err + } + + s.Options.ExtensionCatalogURL = val + } + + if s.verbose && s.levelVar != nil { + s.levelVar.Set(slog.LevelDebug) + } + + return nil +} + +func (s *state) preRunE(sub *cobra.Command, args []string) error { + var ( + deps k6deps.Dependencies + err error + dopts k6deps.Options + ) + + if scriptname, hasScript := scriptArg(sub, args); hasScript { + dopts.Script.Name = scriptname + } + + deps, err = k6deps.Analyze(&dopts) + if err != nil { + return err + } + + cmdargs := make([]string, 0, len(args)) + + if sub.Name() != s.Options.AppName { + cmdargs = append(cmdargs, sub.Name()) + } + + if s.verbose { + cmdargs = append(cmdargs, "-v") + } + + if s.quiet { + cmdargs = append(cmdargs, "-q") + } + + if s.nocolor { + cmdargs = append(cmdargs, "--no-color") + } + + cmdargs = append(cmdargs, args...) + + cmd, err := k6exec.Command(context.Background(), cmdargs, deps, &s.Options) + if err != nil { + return err + } + + cmd.Stderr = os.Stderr //nolint:forbidigo + cmd.Stdout = os.Stdout //nolint:forbidigo + cmd.Stdin = os.Stdin //nolint:forbidigo + + s.cmd = cmd + + return nil +} + +func (s *state) runE(_ *cobra.Command, _ []string) error { + defer k6exec.CleanupState(&s.Options) //nolint:errcheck + + return s.cmd.Run() +} + +func (s *state) helpFunc(cmd *cobra.Command, args []string) { + err := s.preRunE(cmd, append(args, "-h")) + if err != nil { + cmd.PrintErr(err) + } + + err = s.runE(cmd, args) + if err != nil { + cmd.PrintErr(err) + } +} diff --git a/go.mod b/go.mod index bca5518..9369fb4 100644 --- a/go.mod +++ b/go.mod @@ -6,9 +6,11 @@ toolchain go1.22.4 require ( github.com/adrg/xdg v0.4.0 + github.com/briandowns/spinner v1.23.1 + github.com/fatih/color v1.17.0 github.com/grafana/clireadme v0.1.0 github.com/grafana/k6build v0.2.0 - github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f + github.com/grafana/k6deps v0.1.2 github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 github.com/samber/slog-logrus/v2 v2.3.0 github.com/sirupsen/logrus v1.9.3 @@ -25,6 +27,8 @@ require ( github.com/grafana/k6foundry v0.1.3 // indirect github.com/grafana/k6pack v0.2.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/peterbourgon/diskv v2.0.1+incompatible // indirect github.com/samber/lo v1.38.1 // indirect github.com/samber/slog-common v0.16.0 // indirect diff --git a/go.sum b/go.sum index 9cbed93..4cd3d64 100644 --- a/go.sum +++ b/go.sum @@ -4,12 +4,16 @@ github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0 github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= +github.com/briandowns/spinner v1.23.1 h1:t5fDPmScwUjozhDj4FA46p5acZWIPXYE30qW2Ptu650= +github.com/briandowns/spinner v1.23.1/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/evanw/esbuild v0.21.5 h1:oShm8TT5QUhf6vM7teg0nmd14eHu64dPmVluC2f4DMg= github.com/evanw/esbuild v0.21.5/go.mod h1:D2vIQZqV/vIf/VRHtViaUtViZmG7o+kKmlBfVQuRi48= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -20,8 +24,8 @@ github.com/grafana/k6build v0.2.0 h1:4IRinD5iuPW7+XR5590UduPwm1hBAwH2bpdkMADifP8 github.com/grafana/k6build v0.2.0/go.mod h1:DXItIZzDI1gnMOC0+oSE2OsjNJtR4ahLHYC8EQ643T8= github.com/grafana/k6catalog v0.1.0 h1:jLmbmB3EUJ+zyQG3hWy6dWbtMjvTkvJNx1d4LX8it6I= github.com/grafana/k6catalog v0.1.0/go.mod h1:8R9eXAh2nb69+drkj0rZ4aemso0jcwCbPP6Q3E5LqCw= -github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f h1:TQE8m+ScEDi4s+vlWpvMoc2U2fiWtEt85PuRflStCiw= -github.com/grafana/k6deps v0.1.2-0.20240617140502-f1b0dfc93f7f/go.mod h1:j8UOs5mZhn5+hpJqDtl5zjYRjMpBKYf+FwaBmHHcfao= +github.com/grafana/k6deps v0.1.2 h1:/AJXUFy5UJhpqHqupr5LZuUudPxEj04QSSKLVtr9F2A= +github.com/grafana/k6deps v0.1.2/go.mod h1:j8UOs5mZhn5+hpJqDtl5zjYRjMpBKYf+FwaBmHHcfao= github.com/grafana/k6foundry v0.1.3 h1:05sRM5ik+MsZr1tdJR/rTjI8trLpWFbG+vzmnpmsC5g= github.com/grafana/k6foundry v0.1.3/go.mod h1:b6n4InFgXl+3yPobmlyJfcJmLozU9CI9IIUuq8YqEiM= github.com/grafana/k6pack v0.2.1 h1:S9EkeFuRMnfwP/lHrKnlgctlNDiUKgKU1bEKbIfOUro= @@ -30,6 +34,11 @@ github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 h1:+ngKgrYPPJr github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/peterbourgon/diskv v2.0.1+incompatible h1:UBdAOUP5p4RWqPBg048CAvpKN+vxiaj6gdUUzhl4XmI= github.com/peterbourgon/diskv v2.0.1+incompatible/go.mod h1:uqqh8zWWbv1HBMNONnaR/tNboyR3/BZd58JJSHlUSCU= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -57,6 +66,8 @@ golang.org/x/mod v0.18.0 h1:5+9lSbEzPSdWkH32vYPBwEpX8KwDbM52Ud9xBUvNlb0= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA=