diff --git a/go.mod b/go.mod index 762c0f5e0..49fd3421b 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,7 @@ require ( github.com/fsnotify/fsnotify v1.6.0 github.com/getsentry/sentry-go v0.18.0 github.com/google/go-cmp v0.5.9 - github.com/mattn/go-isatty v0.0.17 + github.com/mattn/go-isatty v0.0.17 // indirect github.com/mholt/archiver/v3 v3.5.1 github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/mapstructure v1.5.0 diff --git a/pkg/app/run.go b/pkg/app/run.go index d24d2b4cf..dbaa65a04 100644 --- a/pkg/app/run.go +++ b/pkg/app/run.go @@ -154,11 +154,11 @@ func Run(opts RunOpts) error { if g.Verbose() { switch source { case lookup.SourceEnvironment: - fmt.Fprintf(opts.Stdout, "Fastly API endpoint (via %s): %s\n", env.Endpoint, endpoint) + fmt.Fprintf(opts.Stdout, "Fastly API endpoint (via %s): %s\n\n", env.Endpoint, endpoint) case lookup.SourceFile: - fmt.Fprintf(opts.Stdout, "Fastly API endpoint (via config file): %s\n", endpoint) + fmt.Fprintf(opts.Stdout, "Fastly API endpoint (via config file): %s\n\n", endpoint) default: - fmt.Fprintf(opts.Stdout, "Fastly API endpoint: %s\n", endpoint) + fmt.Fprintf(opts.Stdout, "Fastly API endpoint: %s\n\n", endpoint) } } diff --git a/pkg/commands/acl/acl_test.go b/pkg/commands/acl/acl_test.go index 1dcb2e49d..ec2aaa4bd 100644 --- a/pkg/commands/acl/acl_test.go +++ b/pkg/commands/acl/acl_test.go @@ -281,7 +281,7 @@ func TestACLList(t *testing.T) { ListACLsFn: listACLs, }, Args: args("acl list --service-id 123 --verbose --version 1"), - WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nID: 456\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nID: 789\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", + WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\n\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nID: 456\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nID: 789\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\n", }, } diff --git a/pkg/commands/aclentry/aclentry_test.go b/pkg/commands/aclentry/aclentry_test.go index 2f5c7425f..53b787250 100644 --- a/pkg/commands/aclentry/aclentry_test.go +++ b/pkg/commands/aclentry/aclentry_test.go @@ -349,6 +349,7 @@ var listACLEntriesOutputPageTwo = `SERVICE ID ID IP SUBNET NEGATED var listACLEntriesOutputVerbose = `Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 ACL ID: 123 @@ -372,6 +373,7 @@ Comment: bar Created at: 2021-06-15 23:00:00 +0000 UTC Updated at: 2021-06-15 23:00:00 +0000 UTC Deleted at: 2021-06-15 23:00:00 +0000 UTC + ` func TestACLEntryUpdate(t *testing.T) { diff --git a/pkg/commands/authtoken/authtoken_test.go b/pkg/commands/authtoken/authtoken_test.go index 830724a61..e146cc667 100644 --- a/pkg/commands/authtoken/authtoken_test.go +++ b/pkg/commands/authtoken/authtoken_test.go @@ -393,6 +393,7 @@ func listTokenOutputVerbose() string { return `Fastly API token provided via --token Fastly API endpoint: https://api.fastly.com + ID: 123 Name: Foo User ID: 456 @@ -413,7 +414,9 @@ IP: 127.0.0.2 Created at: 2021-06-15 23:00:00 +0000 UTC Last used at: 2021-06-15 23:00:00 +0000 UTC -Expires at: 2021-06-15 23:00:00 +0000 UTC` +Expires at: 2021-06-15 23:00:00 +0000 UTC + +` } func listTokenOutputSummary(env bool) string { diff --git a/pkg/commands/backend/backend_test.go b/pkg/commands/backend/backend_test.go index f6b7ce836..eadcb4cd9 100644 --- a/pkg/commands/backend/backend_test.go +++ b/pkg/commands/backend/backend_test.go @@ -388,6 +388,7 @@ SERVICE VERSION NAME ADDRESS PORT COMMENT var listBackendsVerboseOutput = strings.Join([]string{ "Fastly API token not provided", "Fastly API endpoint: https://api.fastly.com", + "", "Service ID (via --service-id): 123", "", "Version: 1", diff --git a/pkg/commands/compute/build.go b/pkg/commands/compute/build.go index 1ae590e9d..96aa02479 100644 --- a/pkg/commands/compute/build.go +++ b/pkg/commands/compute/build.go @@ -70,16 +70,24 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { if c.Globals.Flags.Quiet { out = io.Discard } - progress := text.NewProgress(out, c.Globals.Verbose()) + + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } defer func(errLog fsterr.LogInterface) { if err != nil { errLog.Add(err) - progress.Fail() // progress.Done is handled inline } }(c.Globals.ErrLog) - progress.Step("Verifying package manifest...") + err = spinner.Start() + if err != nil { + return err + } + msg := "Verifying package manifest..." + spinner.Message(msg) err = c.Manifest.File.ReadError() if err != nil { @@ -87,33 +95,77 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { err = fsterr.ErrReadingManifest } c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + + return err + } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { return err } + err = spinner.Start() + if err != nil { + return err + } + msg = "Identifying package name..." + spinner.Message(msg) + packageName, err := packageName(c) + if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err + } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + + err = spinner.Start() if err != nil { return err } + msg = "Identifying toolchain..." + spinner.Message(msg) toolchain, err := toolchain(c) if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return err } - language, err := language(toolchain, c, progress) + spinner.StopMessage(msg) + err = spinner.Stop() if err != nil { return err } - err = binDir(c) + language, err := language(toolchain, c, out) if err != nil { return err } - // NOTE: We set the progress indicator to Done() so that any output we now - // print doesn't get hidden by the progress status. - progress.Done() - progress = text.ResetProgress(out, c.Globals.Verbose()) + err = binDir(c) + if err != nil { + return err + } postBuildCallback := func() error { if !c.Globals.Flags.AutoYes && !c.Globals.Flags.NonInteractive { @@ -125,19 +177,19 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { return nil } - if err := language.Build(out, progress, c.Globals.Flags.Verbose, postBuildCallback); err != nil { + if err := language.Build(out, spinner, c.Globals.Flags.Verbose, postBuildCallback); err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Language": language.Name, }) return err } - if c.Globals.Verbose() { - text.Break(out) + err = spinner.Start() + if err != nil { + return err } - - progress = text.ResetProgress(out, c.Globals.Verbose()) - progress.Step("Creating package archive...") + msg = "Creating package archive..." + spinner.Message(msg) dest := filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", packageName)) @@ -149,6 +201,11 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { files, err = c.includeSourceCode(files, language.SourceDirectory) if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return err } @@ -158,10 +215,21 @@ func (c *BuildCommand) Exec(in io.Reader, out io.Writer) (err error) { "Files": files, "Destination": dest, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error creating package archive: %w", err) } - progress.Done() + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } out = originalOut text.Success(out, "Built package (%s)", dest) @@ -250,7 +318,7 @@ func toolchain(c *BuildCommand) (string, error) { } // language returns a pointer to a supported language. -func language(toolchain string, c *BuildCommand, progress text.Progress) (*Language, error) { +func language(toolchain string, c *BuildCommand, out io.Writer) (*Language, error) { var language *Language switch toolchain { case "assemblyscript": @@ -261,7 +329,7 @@ func language(toolchain string, c *BuildCommand, progress text.Progress) (*Langu &c.Manifest.File, c.Globals.ErrLog, c.Flags.Timeout, - progress, + out, c.Globals.Verbose(), ), }) @@ -274,7 +342,7 @@ func language(toolchain string, c *BuildCommand, progress text.Progress) (*Langu c.Globals.ErrLog, c.Flags.Timeout, c.Globals.Config.Language.Go, - progress, + out, c.Globals.Verbose(), ), }) @@ -286,7 +354,7 @@ func language(toolchain string, c *BuildCommand, progress text.Progress) (*Langu &c.Manifest.File, c.Globals.ErrLog, c.Flags.Timeout, - progress, + out, c.Globals.Verbose(), ), }) @@ -299,7 +367,7 @@ func language(toolchain string, c *BuildCommand, progress text.Progress) (*Langu c.Globals.ErrLog, c.Flags.Timeout, c.Globals.Config.Language.Rust, - progress, + out, c.Globals.Verbose(), ), }) diff --git a/pkg/commands/compute/build_test.go b/pkg/commands/compute/build_test.go index d890c956c..59fa8e2e6 100644 --- a/pkg/commands/compute/build_test.go +++ b/pkg/commands/compute/build_test.go @@ -14,6 +14,7 @@ import ( "github.com/fastly/cli/pkg/config" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/testutil" + "github.com/fastly/cli/pkg/threadsafe" ) func TestBuildRust(t *testing.T) { @@ -788,7 +789,7 @@ func TestBuildOther(t *testing.T) { } } - var stdout bytes.Buffer + var stdout threadsafe.Buffer opts := testutil.NewRunOpts(testcase.args, &stdout) opts.Stdin = strings.NewReader(testcase.stdin) // NOTE: build only has one prompt when dealing with a custom build err = app.Run(opts) diff --git a/pkg/commands/compute/init.go b/pkg/commands/compute/init.go index 84378dedb..226f5dcfb 100644 --- a/pkg/commands/compute/init.go +++ b/pkg/commands/compute/init.go @@ -25,6 +25,7 @@ import ( "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" cp "github.com/otiai10/copy" + "github.com/theckman/yacspin" ) var ( @@ -94,15 +95,9 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { } } - // NOTE: Will be a NullProgress unless --verbose is set. - // - // This is because we don't want any progress output until later. - progress := instantiateProgress(c.Globals.Verbose(), out) - defer func(errLog fsterr.LogInterface) { if err != nil { errLog.Add(err) - progress.Fail() // progress.Done is handled inline } }(c.Globals.ErrLog) @@ -116,12 +111,18 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { if c.Globals.Flags.Quiet { mf.SetQuiet(true) } - if c.dir == "" && !mf.Exists() { - fmt.Fprintf(progress, "--directory not specified, using current directory\n\n") + if c.dir == "" && !mf.Exists() && c.Globals.Verbose() { + text.Info(out, "--directory not specified, using current directory") + text.Break(out) c.dir = wd } - dst, err := verifyDestination(c.dir, progress) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } + + dst, err := verifyDestination(c.dir, spinner, out) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Directory": c.dir, @@ -186,12 +187,6 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { c.cloneFrom = from } - text.Break(out) - - // NOTE: From this point onwards we need a non-null progress regardless of - // whether --verbose was set or not. - progress = text.NewProgress(out, c.Globals.Verbose()) - // We only want to fetch a remote package if c.cloneFrom has been set. // This can happen in two ways: // @@ -202,7 +197,7 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { // "other" because this means they intend on handling the compilation of code // that isn't natively supported by the platform. if c.cloneFrom != "" { - err = fetchPackageTemplate(c, branch, tag, file.Archives, progress, out) + err = fetchPackageTemplate(c, branch, tag, file.Archives, spinner, out) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "From": from, @@ -214,7 +209,7 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { } } - mf, err = updateManifest(mf, progress, c.dir, name, desc, authors, language) + mf, err = updateManifest(mf, spinner, c.dir, name, desc, authors, language) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Directory": c.dir, @@ -224,13 +219,12 @@ func (c *InitCommand) Exec(in io.Reader, out io.Writer) (err error) { return err } - language, err = initializeLanguage(progress, language, languages, mf.Language, wd, c.dir) + language, err = initializeLanguage(spinner, language, languages, mf.Language, wd, c.dir) if err != nil { c.Globals.ErrLog.Add(err) return fmt.Errorf("error initializing package: %w", err) } - progress.Done() displayOutput(mf.Name, dst, language.Name, out) return nil } @@ -263,29 +257,18 @@ func verifyDirectory(flags global.Flags, dir string, out io.Writer, in io.Reader if err != nil { return false, err } - if result { - text.Break(out) - } return result, nil } return true, nil } -// instantiateProgress returns an instance of a text.Progress bar. -func instantiateProgress(verbose bool, out io.Writer) text.Progress { - if verbose { - return text.NewVerboseProgress(out) - } - return text.NewNullProgress() -} - // verifyDestination checks the provided path exists and is a directory. // // NOTE: For validating user permissions it will create a temporary file within // the directory and then remove it before returning the absolute path to the // directory itself. -func verifyDestination(path string, progress text.Progress) (dst string, err error) { +func verifyDestination(path string, spinner *yacspin.Spinner, out io.Writer) (dst string, err error) { dst, err = filepath.Abs(path) if err != nil { return "", err @@ -299,18 +282,55 @@ func verifyDestination(path string, progress text.Progress) (dst string, err err return dst, fmt.Errorf("package destination is not a directory") // specific problem } if err != nil && errors.Is(err, fs.ErrNotExist) { // normal-ish case - fmt.Fprintf(progress, "Creating %s...\n", dst) + text.Break(out) + + err := spinner.Start() + if err != nil { + return "", err + } + msg := fmt.Sprintf("Creating %s...", dst) + spinner.Message(msg) + if err := os.MkdirAll(dst, 0o700); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("error creating package destination: %w", err) } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return "", err + } + } + + text.Break(out) + err = spinner.Start() + if err != nil { + return "", err } + msg := "Validating directory permissions..." + spinner.Message(msg) tmpname := make([]byte, 16) n, err := rand.Read(tmpname) if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("error generating random filename: %w", err) } if n != 16 { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("failed to generate enough entropy (%d/%d)", n, 16) } @@ -321,17 +341,37 @@ func verifyDestination(path string, progress text.Progress) (dst string, err err /* #nosec */ f, err := os.Create(filepath.Join(dst, fmt.Sprintf("tmp_%x", tmpname))) if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("error creating file in package destination: %w", err) } if err := f.Close(); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("error closing file in package destination: %w", err) } if err := os.Remove(f.Name()); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return "", spinErr + } return dst, fmt.Errorf("error removing file in package destination: %w", err) } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return "", err + } return dst, nil } @@ -346,18 +386,28 @@ func promptOrReturn( out io.Writer, ) (name, description string, authors []string, err error) { name, _ = m.Name() + description, _ = m.Description() + authors, _ = m.Authors() + + if name == "" || description == "" || len(authors) == 0 { + text.Break(out) + } + name, err = promptPackageName(flags, name, path, in, out) if err != nil { return "", description, authors, err } - description, _ = m.Description() description, err = promptPackageDescription(flags, description, in, out) if err != nil { return name, "", authors, err } - authors, _ = m.Authors() + // This catches scenarios where someone runs `compute init` multiple times. + if name != "" && len(authors) > 0 { + text.Break(out) + } + authors, err = promptPackageAuthors(flags, authors, email, in, out) if err != nil { return name, description, []string{}, err @@ -418,13 +468,15 @@ func promptPackageDescription(flags global.Flags, desc string, in io.Reader, out // // It will use a default of the user's email found within the manifest, if set // there, otherwise the value will be an empty slice. +// +// FIXME: Handle prompting for multiple authors. func promptPackageAuthors(flags global.Flags, authors []string, manifestEmail string, in io.Reader, out io.Writer) ([]string, error) { defaultValue := []string{manifestEmail} if len(authors) == 0 && (flags.AcceptDefaults || flags.NonInteractive) { return defaultValue, nil } if len(authors) == 0 { - label := "Author: " + label := "Author (email): " if manifestEmail != "" { label = fmt.Sprintf("%s[%s] ", label, manifestEmail) @@ -559,10 +611,17 @@ func fetchPackageTemplate( c *InitCommand, branch, tag string, archives []file.Archive, - progress text.Progress, + spinner *yacspin.Spinner, out io.Writer, ) error { - progress.Step("Fetching package template...") + text.Break(out) + + err := spinner.Start() + if err != nil { + return err + } + msg := "Fetching package template..." + spinner.Message(msg) // If the user has provided a local file path, we'll recursively copy the // directory to c.dir. @@ -570,15 +629,50 @@ func fetchPackageTemplate( if err != nil { c.Globals.ErrLog.Add(err) } else if fi.IsDir() { - return cp.Copy(c.cloneFrom, c.dir) + if err := cp.Copy(c.cloneFrom, c.dir); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err + } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + return nil } req, err := http.NewRequest("GET", c.cloneFrom, nil) if err != nil { c.Globals.ErrLog.Add(err) if gitRepositoryRegEx.MatchString(c.cloneFrom) { - return clonePackageFromEndpoint(c.cloneFrom, branch, tag, c.dir) + if err := clonePackageFromEndpoint(c.cloneFrom, branch, tag, c.dir); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err + } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + return nil } + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("failed to construct package request URL: %w", err) } @@ -591,6 +685,13 @@ func fetchPackageTemplate( res, err := c.Globals.HTTPClient.Do(req) if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("failed to get package: %w", err) } defer res.Body.Close() // #nosec G307 @@ -598,6 +699,13 @@ func fetchPackageTemplate( if res.StatusCode != http.StatusOK { err := fmt.Errorf("failed to get package: %s", res.Status) c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err } @@ -612,6 +720,13 @@ func fetchPackageTemplate( f, err := os.Create(filename) if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("failed to create local %s archive: %w", filename, err) } defer func() { @@ -629,6 +744,13 @@ func fetchPackageTemplate( _, err = io.Copy(f, res.Body) if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("failed to write %s archive to disk: %w", filename, err) } @@ -673,6 +795,13 @@ mimes: err := os.Rename(filename, filenameWithExt) if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err } filename = filenameWithExt @@ -684,18 +813,49 @@ mimes: err = archive.Extract() if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("failed to extract %s archive content: %w", filename, err) } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } return nil } - return clonePackageFromEndpoint(c.cloneFrom, branch, tag, c.dir) + if err := clonePackageFromEndpoint(c.cloneFrom, branch, tag, c.dir); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err + } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + return nil } // clonePackageFromEndpoint clones the given repo (from) into a temp directory, // then copies specific files to the destination directory (path). -func clonePackageFromEndpoint(from string, branch string, tag string, dst string) error { +func clonePackageFromEndpoint( + from string, + branch string, + tag string, + dst string, +) error { _, err := exec.LookPath("git") if err != nil { return fsterr.RemediationError{ @@ -777,6 +937,7 @@ func clonePackageFromEndpoint(from string, branch string, tag string, dst string if err != nil { return fmt.Errorf("error copying files from package template: %w", err) } + return nil } @@ -802,12 +963,17 @@ func tempDir(prefix string) (abspath string, err error) { // NOTE: The language argument might be nil (if the user passes --from flag). func updateManifest( m manifest.File, - progress text.Progress, + spinner *yacspin.Spinner, path, name, desc string, authors []string, language *Language, ) (manifest.File, error) { - progress.Step("Updating package manifest...") + err := spinner.Start() + if err != nil { + return m, err + } + msg := "Reading package manifest..." + spinner.Message(msg) mp := filepath.Join(path, manifest.Filename) @@ -822,48 +988,145 @@ func updateManifest( m.Authors = authors m.Language = language.Name if err := m.Write(mp); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return m, spinErr + } return m, fmt.Errorf("error saving package manifest: %w", err) } + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return m, spinErr + } return m, nil } } + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return m, spinErr + } return m, fmt.Errorf("error reading package manifest: %w", err) } - fmt.Fprintf(progress, "Setting package name in manifest to %q...\n", name) + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err + } + + err = spinner.Start() + if err != nil { + return m, err + } + msg = fmt.Sprintf("Setting package name in manifest to %q...", name) + spinner.Message(msg) + m.Name = name + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err + } + // NOTE: We allow an empty description to be set. m.Description = desc if desc != "" { - desc = " to " + desc + desc = " to '" + desc + "'" + } + + err = spinner.Start() + if err != nil { + return m, err + } + msg = fmt.Sprintf("Setting description in manifest%s...", desc) + spinner.Message(msg) + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err } - fmt.Fprintf(progress, "Setting description in manifest%s...\n", desc) if len(authors) > 0 { - fmt.Fprintf(progress, "Setting authors in manifest to %s...\n", strings.Join(authors, ", ")) + err := spinner.Start() + if err != nil { + return m, err + } + msg := fmt.Sprintf("Setting authors in manifest to '%s'...", strings.Join(authors, ", ")) + spinner.Message(msg) + m.Authors = authors + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err + } } if language != nil { - fmt.Fprintf(progress, "Setting language in manifest to %s...\n", language.Name) + err := spinner.Start() + if err != nil { + return m, err + } + msg := fmt.Sprintf("Setting language in manifest to '%s'...", language.Name) + spinner.Message(msg) + m.Language = language.Name + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err + } + } + + err = spinner.Start() + if err != nil { + return m, err } + msg = "Saving manifest changes..." + spinner.Message(msg) if err := m.Write(mp); err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return m, spinErr + } return m, fmt.Errorf("error saving package manifest: %w", err) } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return m, err + } return m, nil } // initializeLanguage for newly cloned package. -func initializeLanguage(progress text.Progress, language *Language, languages []*Language, name, wd, path string) (*Language, error) { - progress.Step("Initializing package...") +func initializeLanguage(spinner *yacspin.Spinner, language *Language, languages []*Language, name, wd, path string) (*Language, error) { + err := spinner.Start() + if err != nil { + return nil, err + } + msg := "Initializing package..." + spinner.Message(msg) if wd != path { err := os.Chdir(path) if err != nil { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } return nil, fmt.Errorf("error changing to your project directory: %w", err) } } @@ -881,10 +1144,20 @@ func initializeLanguage(progress text.Progress, language *Language, languages [] } } if !match { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } return nil, fmt.Errorf("unrecognised package language") } } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return nil, err + } return language, nil } diff --git a/pkg/commands/compute/init_test.go b/pkg/commands/compute/init_test.go index ac6f3ee61..bf9e7bdc0 100644 --- a/pkg/commands/compute/init_test.go +++ b/pkg/commands/compute/init_test.go @@ -71,9 +71,8 @@ func TestInit(t *testing.T) { }, stdin: "foobar", // expect the first prompt to be for the package name. wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", }, manifestIncludes: `name = "foobar"`, }, @@ -86,9 +85,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", }, manifestIncludes: `description = ""`, // expect this to be empty }, @@ -101,9 +99,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", }, manifestIncludes: `authors = ["test@example.com"]`, }, @@ -116,9 +113,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", }, manifestIncludes: `authors = ["test1@example.com", "test2@example.com"]`, }, @@ -136,9 +132,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", "SUCCESS: Initialized package", }, }, @@ -156,9 +151,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", "SUCCESS: Initialized package", }, }, @@ -176,9 +170,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", "SUCCESS: Initialized package", }, }, @@ -196,9 +189,8 @@ func TestInit(t *testing.T) { }, }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", "SUCCESS: Initialized package", }, }, @@ -218,7 +210,7 @@ func TestInit(t *testing.T) { description = "test" authors = ["test@fastly.com"]`, wantOutput: []string{ - "Updating package manifest...", + "Reading package manifest...", "Initializing package...", }, }, @@ -238,7 +230,8 @@ func TestInit(t *testing.T) { description = "test" authors = ["test@fastly.com"]`, wantOutput: []string{ - "Updating package manifest...", + "Reading package manifest...", + "Saving manifest changes...", "Initializing package...", }, }, @@ -259,10 +252,11 @@ func TestInit(t *testing.T) { "SECURITY.md", }, wantOutput: []string{ - "Author: Language:", - "Initializing...", + "Author (email): Language:", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", + "Saving manifest changes...", + "Initializing package...", }, }, { @@ -292,9 +286,10 @@ func TestInit(t *testing.T) { "SECURITY.md", }, wantOutput: []string{ - "Initializing...", "Fetching package template...", - "Updating package manifest...", + "Reading package manifest...", + "Saving manifest changes...", + "Initializing package...", }, }, { diff --git a/pkg/commands/compute/language_assemblyscript.go b/pkg/commands/compute/language_assemblyscript.go index 9b4278ab1..803ef2fee 100644 --- a/pkg/commands/compute/language_assemblyscript.go +++ b/pkg/commands/compute/language_assemblyscript.go @@ -8,6 +8,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" + "github.com/theckman/yacspin" ) // AsDefaultBuildCommand is a build command compiled into the CLI binary so it @@ -73,7 +74,7 @@ type AssemblyScript struct { } // Build compiles the user's source code into a Wasm binary. -func (a *AssemblyScript) Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error { +func (a *AssemblyScript) Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error { var noBuildScript bool if a.build == "" { a.build = AsDefaultBuildCommand @@ -93,8 +94,6 @@ func (a *AssemblyScript) Build(out io.Writer, progress text.Progress, verbose bo text.Break(out) } - progress.Step("Running [scripts.build]...") - bt := BuildToolchain{ buildFn: a.Shell.Build, buildScript: a.build, @@ -103,7 +102,7 @@ func (a *AssemblyScript) Build(out io.Writer, progress text.Progress, verbose bo timeout: a.timeout, out: out, postBuildCallback: callback, - progress: progress, + spinner: spinner, verbose: verbose, } diff --git a/pkg/commands/compute/language_go.go b/pkg/commands/compute/language_go.go index 8b1e5c696..d9aee6e14 100644 --- a/pkg/commands/compute/language_go.go +++ b/pkg/commands/compute/language_go.go @@ -12,6 +12,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" + "github.com/theckman/yacspin" ) // GoDefaultBuildCommand is a build command compiled into the CLI binary so it @@ -76,7 +77,7 @@ type Go struct { } // Build compiles the user's source code into a Wasm binary. -func (g *Go) Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error { +func (g *Go) Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error { var noBuildScript bool if g.build == "" { g.build = GoDefaultBuildCommand @@ -85,7 +86,6 @@ func (g *Go) Build(out io.Writer, progress text.Progress, verbose bool, callback if noBuildScript && g.verbose { text.Info(out, "No [scripts.build] found in fastly.toml. The following default build command for Go will be used: `%s`\n", g.build) - text.Break(out) } g.toolchainConstraint( @@ -95,8 +95,6 @@ func (g *Go) Build(out io.Writer, progress text.Progress, verbose bool, callback "tinygo", `tinygo version (?P\d[^\s]+)`, g.config.TinyGoConstraint, ) - progress.Step("Running [scripts.build]...") - bt := BuildToolchain{ buildFn: g.Shell.Build, buildScript: g.build, @@ -105,7 +103,7 @@ func (g *Go) Build(out io.Writer, progress text.Progress, verbose bool, callback timeout: g.timeout, out: out, postBuildCallback: callback, - progress: progress, + spinner: spinner, verbose: verbose, } @@ -152,7 +150,6 @@ func (g *Go) toolchainConstraint(toolchain, pattern, constraint string) { if g.verbose { text.Info(g.output, "The Fastly CLI requires a %s version '%s'. ", toolchain, constraint) - text.Break(g.output) } if !c.Check(v) { diff --git a/pkg/commands/compute/language_javascript.go b/pkg/commands/compute/language_javascript.go index c3c6abd23..0653abe29 100644 --- a/pkg/commands/compute/language_javascript.go +++ b/pkg/commands/compute/language_javascript.go @@ -10,6 +10,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" + "github.com/theckman/yacspin" ) // JsDefaultBuildCommand is a build command compiled into the CLI binary so it @@ -75,7 +76,7 @@ type JavaScript struct { } // Build compiles the user's source code into a Wasm binary. -func (j *JavaScript) Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error { +func (j *JavaScript) Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error { var noBuildScript bool if j.build == "" { noBuildScript = true @@ -96,8 +97,6 @@ func (j *JavaScript) Build(out io.Writer, progress text.Progress, verbose bool, text.Break(out) } - progress.Step("Running [scripts.build]...") - bt := BuildToolchain{ buildFn: j.Shell.Build, buildScript: j.build, @@ -106,7 +105,7 @@ func (j *JavaScript) Build(out io.Writer, progress text.Progress, verbose bool, timeout: j.timeout, out: out, postBuildCallback: callback, - progress: progress, + spinner: spinner, verbose: verbose, } diff --git a/pkg/commands/compute/language_other.go b/pkg/commands/compute/language_other.go index 34fad112d..4d5de7b18 100644 --- a/pkg/commands/compute/language_other.go +++ b/pkg/commands/compute/language_other.go @@ -5,7 +5,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" "github.com/fastly/cli/pkg/manifest" - "github.com/fastly/cli/pkg/text" + "github.com/theckman/yacspin" ) // NewOther constructs a new unsupported language instance. @@ -37,9 +37,7 @@ type Other struct { // Build implements the Toolchain interface and attempts to compile the package // source to a Wasm binary. -func (o Other) Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error { - progress.Step("Running [scripts.build]...") - +func (o Other) Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error { bt := BuildToolchain{ buildFn: o.Shell.Build, buildScript: o.build, @@ -48,9 +46,8 @@ func (o Other) Build(out io.Writer, progress text.Progress, verbose bool, callba timeout: o.timeout, out: out, postBuildCallback: callback, - progress: progress, + spinner: spinner, verbose: verbose, } - return bt.Build() } diff --git a/pkg/commands/compute/language_rust.go b/pkg/commands/compute/language_rust.go index 7978d44c0..cb2d88e49 100644 --- a/pkg/commands/compute/language_rust.go +++ b/pkg/commands/compute/language_rust.go @@ -18,6 +18,7 @@ import ( "github.com/fastly/cli/pkg/manifest" "github.com/fastly/cli/pkg/text" toml "github.com/pelletier/go-toml" + "github.com/theckman/yacspin" ) // RustDefaultBuildCommand is a build command compiled into the CLI binary so it @@ -85,7 +86,7 @@ type Rust struct { } // Build compiles the user's source code into a Wasm binary. -func (r *Rust) Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error { +func (r *Rust) Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error { var noBuildScript bool if r.build == "" { r.build = fmt.Sprintf(RustDefaultBuildCommand, RustDefaultPackageName) @@ -99,13 +100,10 @@ func (r *Rust) Build(out io.Writer, progress text.Progress, verbose bool, callba if noBuildScript && r.verbose { text.Info(out, "No [scripts.build] found in fastly.toml. The following default build command for Rust will be used: `%s`\n", r.build) - text.Break(out) } r.toolchainConstraint() - progress.Step("Running [scripts.build]...") - bt := BuildToolchain{ buildFn: r.Shell.Build, buildScript: r.build, @@ -115,7 +113,7 @@ func (r *Rust) Build(out io.Writer, progress text.Progress, verbose bool, callba timeout: r.timeout, out: out, postBuildCallback: callback, - progress: progress, + spinner: spinner, verbose: verbose, } @@ -149,6 +147,7 @@ func (r *Rust) modifyCargoPackageName() error { } if r.verbose { + text.Break(r.output) text.Output(r.output, "Command output for '%s': %s", s, stdout.String()) } @@ -212,7 +211,6 @@ func (r *Rust) toolchainConstraint() { if r.verbose { text.Info(r.output, "The Fastly CLI requires a Rust version '%s'. ", r.config.ToolchainConstraint) - text.Break(r.output) } if !c.Check(v) { diff --git a/pkg/commands/compute/language_toolchain.go b/pkg/commands/compute/language_toolchain.go index 7da00701a..1f5aed76a 100644 --- a/pkg/commands/compute/language_toolchain.go +++ b/pkg/commands/compute/language_toolchain.go @@ -9,6 +9,7 @@ import ( fsterr "github.com/fastly/cli/pkg/errors" fstexec "github.com/fastly/cli/pkg/exec" "github.com/fastly/cli/pkg/text" + "github.com/theckman/yacspin" ) // DefaultBuildErrorRemediation is the message returned to a user when there is @@ -30,7 +31,7 @@ For more information on fastly.toml configuration settings, refer to https://dev // Toolchain abstracts a Compute@Edge source language toolchain. type Toolchain interface { // Build compiles the user's source code into a Wasm binary. - Build(out io.Writer, progress text.Progress, verbose bool, callback func() error) error + Build(out io.Writer, spinner *yacspin.Spinner, verbose bool, callback func() error) error } // BuildToolchain enables a language toolchain to compile their build script. @@ -42,18 +43,47 @@ type BuildToolchain struct { out io.Writer postBuild string postBuildCallback func() error - progress text.Progress + spinner *yacspin.Spinner timeout int verbose bool } // Build compiles the user's source code into a Wasm binary. func (bt BuildToolchain) Build() error { - err := bt.execCommand(bt.buildScript) + var ( + err error + msg string + ) + + if !bt.verbose { + err = bt.spinner.Start() + if err != nil { + return err + } + msg = "Running [scripts.build]..." + bt.spinner.Message(msg) + } + + err = bt.execCommand(bt.buildScript) if err != nil { + if !bt.verbose { + bt.spinner.StopFailMessage(msg) + spinErr := bt.spinner.StopFail() + if spinErr != nil { + return spinErr + } + } return bt.handleError(err) } + if !bt.verbose { + bt.spinner.StopMessage(msg) + err = bt.spinner.Stop() + if err != nil { + return err + } + } + // NOTE: internalPostBuildCallback is only used by Rust currently. // It's not a step that would be configured by a user in their fastly.toml // It enables Rust to move the compiled binary to a different location. @@ -72,20 +102,33 @@ func (bt BuildToolchain) Build() error { return bt.handleError(err) } - // NOTE: We set the progress indicator to Done() so that any output we now - // print via the post_build callback doesn't get hidden by the progress status. - // The progress is 'reset' inside the main build controller `build.go`. - bt.progress.Done() + err = bt.spinner.Start() + if err != nil { + return err + } + msg = "Running post_build callback..." + bt.spinner.Message(msg) if bt.postBuild != "" { if err = bt.postBuildCallback(); err == nil { err := bt.execCommand(bt.postBuild) if err != nil { + bt.spinner.StopFailMessage(msg) + spinErr := bt.spinner.StopFail() + if spinErr != nil { + return spinErr + } return bt.handleError(err) } } } + bt.spinner.StopMessage(msg) + err = bt.spinner.Stop() + if err != nil { + return err + } + return nil } @@ -108,12 +151,11 @@ func (bt BuildToolchain) execCommand(script string) error { cmd, args := bt.buildFn(script) s := fstexec.Streaming{ - Command: cmd, - Args: args, - Env: os.Environ(), - Output: bt.out, - Progress: bt.progress, - Verbose: bt.verbose, + Command: cmd, + Args: args, + Env: os.Environ(), + Output: bt.out, + Verbose: bt.verbose, } if bt.timeout > 0 { s.Timeout = time.Duration(bt.timeout) * time.Second diff --git a/pkg/commands/compute/pack.go b/pkg/commands/compute/pack.go index 2137dc2e9..42ba948a4 100644 --- a/pkg/commands/compute/pack.go +++ b/pkg/commands/compute/pack.go @@ -35,15 +35,18 @@ func NewPackCommand(parent cmd.Registerer, g *global.Data, m manifest.Data) *Pac } // Exec implements the command interface. +// +// NOTE: The bin/manifest is placed in a 'package' folder within the tar.gz. func (c *PackCommand) Exec(_ io.Reader, out io.Writer) (err error) { - progress := text.NewProgress(out, c.Globals.Verbose()) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } defer func(errLog fsterr.LogInterface) { os.RemoveAll("pkg/package") - if err != nil { errLog.Add(err) - progress.Fail() } }(c.Globals.ErrLog) @@ -74,23 +77,55 @@ func (c *PackCommand) Exec(_ io.Reader, out io.Writer) (err error) { }) return err } - progress.Step("Copying wasm binary...") + + err = spinner.Start() + if err != nil { + return err + } + msg := "Copying wasm binary..." + spinner.Message(msg) + if err := filesystem.CopyFile(src, dst); err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Path (absolute)": src, "Wasm destination (absolute)": dst, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error copying wasm binary to '%s': %w", dst, err) } if !filesystem.FileExists(bin) { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fsterr.RemediationError{ Inner: fmt.Errorf("no wasm binary found"), Remediation: "Run `fastly compute pack --path ` to copy your wasm binary to the required location", } } - progress.Step("Copying manifest...") + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + + err = spinner.Start() + if err != nil { + return err + } + msg = "Copying manifest..." + spinner.Message(msg) + src = manifest.Filename dst = fmt.Sprintf("pkg/package/%s", manifest.Filename) if err := filesystem.CopyFile(src, dst); err != nil { @@ -98,10 +133,29 @@ func (c *PackCommand) Exec(_ io.Reader, out io.Writer) (err error) { "Manifest (destination)": dst, "Manifest (source)": src, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error copying manifest to '%s': %w", dst, err) } - progress.Step("Creating .tar.gz file...") + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + + err = spinner.Start() + if err != nil { + return err + } + msg = "Creating package.tar.gz file..." + spinner.Message(msg) + tar := archiver.NewTarGz() tar.OverwriteExisting = true { @@ -113,10 +167,21 @@ func (c *PackCommand) Exec(_ io.Reader, out io.Writer) (err error) { "Tar source": dir, "Tar destination": dst, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return err } } - progress.Done() + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } return nil } diff --git a/pkg/commands/compute/pack_test.go b/pkg/commands/compute/pack_test.go index 6201ab029..3fc8db456 100644 --- a/pkg/commands/compute/pack_test.go +++ b/pkg/commands/compute/pack_test.go @@ -28,10 +28,9 @@ func TestPack(t *testing.T) { manifest_version = 2 name = "mypackagename"`, wantOutput: []string{ - "Initializing...", "Copying wasm binary...", "Copying manifest...", - "Creating .tar.gz file...", + "Creating package.tar.gz file...", }, expectedFiles: [][]string{ {"pkg", "package.tar.gz"}, diff --git a/pkg/commands/compute/serve.go b/pkg/commands/compute/serve.go index caa78f7ab..e86c3df11 100644 --- a/pkg/commands/compute/serve.go +++ b/pkg/commands/compute/serve.go @@ -28,6 +28,7 @@ import ( "github.com/fatih/color" "github.com/fsnotify/fsnotify" ignore "github.com/sabhiram/go-gitignore" + "github.com/theckman/yacspin" ) // ServeCommand produces and runs an artifact from files on the local disk. @@ -104,15 +105,28 @@ func (c *ServeCommand) Exec(in io.Reader, out io.Writer) (err error) { text.Break(out) } - progress := text.ResetProgress(out, c.Globals.Verbose()) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } - bin, err := GetViceroy(progress, out, c.av, c.Globals) + bin, err := GetViceroy(spinner, out, c.av, c.Globals) if err != nil { return err } - progress.Step("Running local server...") - progress.Done() + err = spinner.Start() + if err != nil { + return err + } + msg := "Running local server..." + spinner.Message(msg) + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } for { err = local(bin, c.file, c.addr, c.env.Value, c.debug, c.watch, c.watchDir, c.Globals.Verbose(), out, c.Globals.ErrLog) @@ -182,13 +196,7 @@ func (c *ServeCommand) hasBackendsWithMissingOverrideHost() bool { // // In the case of a network failure we fallback to the latest installed version of the // Viceroy binary as long as one is installed and has the correct permissions. -func GetViceroy(progress text.Progress, out io.Writer, av github.AssetVersioner, g *global.Data) (bin string, err error) { - defer func() { - if err != nil { - progress.Fail() - } - }() - +func GetViceroy(spinner *yacspin.Spinner, out io.Writer, av github.AssetVersioner, g *global.Data) (bin string, err error) { bin = filepath.Join(InstallDir, av.BinaryName()) // NOTE: When checking if Viceroy is installed we don't use @@ -229,7 +237,12 @@ func GetViceroy(progress text.Progress, out io.Writer, av github.AssetVersioner, // The latest_version value 0.0.0 means the property either has not been set // or is now stale and needs to be refreshed. if latest.String() == "0.0.0" { - progress.Step("Checking latest Viceroy release...") + err := spinner.Start() + if err != nil { + return bin, err + } + msg := "Checking latest Viceroy release..." + spinner.Message(msg) v, err := av.Version() if err != nil { @@ -239,14 +252,32 @@ func GetViceroy(progress text.Progress, out io.Writer, av github.AssetVersioner, // and the user doesn't have a pre-existing install of Viceroy, then we're // forced to return the error. if install { + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return bin, spinErr + } + return bin, fsterr.RemediationError{ Inner: fmt.Errorf("error fetching latest version: %w", err), Remediation: fsterr.NetworkRemediation, } } + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return bin, err + } return bin, nil } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return bin, err + } + // WARNING: This variable MUST shadow the parent scoped variable. latest, err = semver.Parse(v) if err != nil { @@ -269,14 +300,14 @@ func GetViceroy(progress text.Progress, out io.Writer, av github.AssetVersioner, } if install { - err := installViceroy(progress, av, bin) + err := installViceroy(spinner, av, bin) if err != nil { g.ErrLog.Add(err) return bin, err } } else if checkUpdate { version := strings.TrimSpace(string(stdoutStderr)) - err := updateViceroy(progress, version, out, av, latest, bin) + err := updateViceroy(spinner, version, out, av, latest, bin) if err != nil { g.ErrLog.Add(err) return bin, err @@ -308,37 +339,60 @@ var InstallDir = func() string { }() // installViceroy downloads the latest release from GitHub. -func installViceroy(progress text.Progress, av github.AssetVersioner, bin string) error { - progress.Step("Fetching latest Viceroy release...") +func installViceroy(spinner *yacspin.Spinner, av github.AssetVersioner, bin string) error { + err := spinner.Start() + if err != nil { + return err + } + msg := "Fetching latest Viceroy release..." + spinner.Message(msg) tmpBin, err := av.Download() if err != nil { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fmt.Errorf("error downloading latest Viceroy release: %w", err) } defer os.RemoveAll(tmpBin) if err := os.Rename(tmpBin, bin); err != nil { if err := filesystem.CopyFile(tmpBin, bin); err != nil { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fmt.Errorf("error moving latest Viceroy binary in place: %w", err) } } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } return nil } // updateViceroy checks if the currently installed version is out-of-date and // downloads the latest release from GitHub. func updateViceroy( - progress text.Progress, + spinner *yacspin.Spinner, version string, out io.Writer, av github.AssetVersioner, latest semver.Version, bin string, ) error { - progress.Step("Checking installed Viceroy version...") + err := spinner.Start() + if err != nil { + return err + } + msg := "Checking installed Viceroy version..." + spinner.Message(msg) viceroyError := fsterr.RemediationError{ Inner: fmt.Errorf("a Viceroy version was not found"), @@ -349,19 +403,31 @@ func updateViceroy( segs := strings.Split(version, " ") if len(segs) < 2 { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return viceroyError } installedViceroyVersion := segs[1] if installedViceroyVersion == "" { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return viceroyError } current, err := semver.Parse(installedViceroyVersion) if err != nil { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fsterr.RemediationError{ Inner: fmt.Errorf("error reading current version: %w", err), @@ -376,19 +442,38 @@ func updateViceroy( text.Output(out, "Latest Viceroy version: %s", latest) text.Break(out) + err := spinner.Start() + if err != nil { + return err + } + msg := "Fetching latest Viceroy release..." + spinner.Message(msg) + tmpBin, err := av.Download() - progress.Step("Fetching latest Viceroy release...") if err != nil { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fmt.Errorf("error downloading latest Viceroy release: %w", err) } defer os.RemoveAll(tmpBin) - progress.Step("Replacing Viceroy binary...") + err = spinner.Start() + if err != nil { + return err + } + msg = "Replacing Viceroy binary..." + spinner.Message(msg) if err := os.Rename(tmpBin, bin); err != nil { if err := filesystem.CopyFile(tmpBin, bin); err != nil { - progress.Fail() + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fmt.Errorf("error moving latest Viceroy binary in place: %w", err) } } @@ -431,6 +516,7 @@ func local(bin, file, addr, env string, debug, watch bool, watchDir cmd.Optional } if verbose { + text.Break(out) text.Output(out, "Wasm file: %s", file) text.Output(out, "Manifest: %s", manifestPath) } @@ -441,6 +527,7 @@ func local(bin, file, addr, env string, debug, watch bool, watchDir cmd.Optional Env: os.Environ(), Output: out, SignalCh: make(chan os.Signal, 1), + Verbose: true, // force verbose so we can see the local server address } s.MonitorSignals() diff --git a/pkg/commands/compute/serve_test.go b/pkg/commands/compute/serve_test.go index 7f437d56a..c448ceefe 100644 --- a/pkg/commands/compute/serve_test.go +++ b/pkg/commands/compute/serve_test.go @@ -67,7 +67,6 @@ func TestGetViceroy(t *testing.T) { var out bytes.Buffer - progress := text.NewQuietProgress(&out) av := mock.AssetVersioner{ AssetVersion: "1.2.3", BinaryFilename: viceroyBinName, @@ -92,18 +91,14 @@ func TestGetViceroy(t *testing.T) { ErrLog: fsterr.MockLog{}, } - _, err = compute.GetViceroy(progress, &out, av, &g) + spinner, err := text.NewSpinner(&out) + if err != nil { + t.Fatal(err) + } + _, err = compute.GetViceroy(spinner, &out, av, &g) if err != nil { t.Fatal(err) } - - // NOTE: We have to call progress.Done() here to prevent a data race because - // the getViceroy() function itself doesn't call it and so if we try to read - // from the shared bytes.Buffer (as we do below to validate its content) - // before calling Done(), then we'll get a race condition (this only shows up - // when running the complete test suite or this specific test with a -count - // value of 20 or above). - progress.Done() if !strings.Contains(out.String(), "Fetching latest Viceroy release") { t.Fatalf("expected file to be downloaded successfully") diff --git a/pkg/commands/compute/update.go b/pkg/commands/compute/update.go index c047eaf44..9434337b5 100644 --- a/pkg/commands/compute/update.go +++ b/pkg/commands/compute/update.go @@ -96,18 +96,27 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) (err error) { packagePath = filepath.Join("pkg", fmt.Sprintf("%s.tar.gz", sanitize.BaseName(projectName))) } - progress := text.NewProgress(out, c.Globals.Verbose()) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } + defer func() { if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Service ID": serviceID, "Service Version": serviceVersion.Number, }) - progress.Fail() // progress.Done is handled inline } }() - progress.Step("Uploading package...") + err = spinner.Start() + if err != nil { + return err + } + msg := "Uploading package..." + spinner.Message(msg) + _, err = c.Globals.APIClient.UpdatePackage(&fastly.UpdatePackageInput{ ServiceID: serviceID, ServiceVersion: serviceVersion.Number, @@ -118,12 +127,24 @@ func (c *UpdateCommand) Exec(_ io.Reader, out io.Writer) (err error) { "Service ID": serviceID, "Service Version": serviceVersion.Number, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fsterr.RemediationError{ Inner: fmt.Errorf("error uploading package: %w", err), Remediation: "Run `fastly compute build` to produce a Compute@Edge package, alternatively use the --package flag to reference a package outside of the current project.", } } - progress.Done() + + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } text.Success(out, "Updated package (service %s, version %v)", serviceID, serviceVersion.Number) return nil diff --git a/pkg/commands/compute/update_test.go b/pkg/commands/compute/update_test.go index 603cdd847..475b6e8b4 100644 --- a/pkg/commands/compute/update_test.go +++ b/pkg/commands/compute/update_test.go @@ -52,7 +52,6 @@ func TestUpdate(t *testing.T) { }, WantError: fmt.Sprintf("error uploading package: %s", testutil.Err.Error()), WantOutputs: []string{ - "Initializing...", "Uploading package...", }, }, @@ -65,7 +64,6 @@ func TestUpdate(t *testing.T) { UpdatePackageFn: updatePackageOk, }, WantOutputs: []string{ - "Initializing...", "Uploading package...", "Updated package (service 123, version 4)", }, diff --git a/pkg/commands/dictionary/dictionary_test.go b/pkg/commands/dictionary/dictionary_test.go index 26e2995bc..1eea54070 100644 --- a/pkg/commands/dictionary/dictionary_test.go +++ b/pkg/commands/dictionary/dictionary_test.go @@ -465,6 +465,7 @@ var updateDictionaryOutputVerbose = strings.Join( []string{ "Fastly API token not provided", "Fastly API endpoint: https://api.fastly.com", + "", "Service ID (via --service-id): 123", "", "Service version 1 is not editable, so it was automatically cloned because --autoclone is", @@ -508,6 +509,7 @@ Deleted (UTC): 2001-02-03 04:05 var describeDictionaryOutputVerbose = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/domain/domain_test.go b/pkg/commands/domain/domain_test.go index 555fac95d..99ee53476 100644 --- a/pkg/commands/domain/domain_test.go +++ b/pkg/commands/domain/domain_test.go @@ -376,6 +376,7 @@ SERVICE VERSION NAME COMMENT var listDomainsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/healthcheck/healthcheck_test.go b/pkg/commands/healthcheck/healthcheck_test.go index 84fa6b57a..8b885dd61 100644 --- a/pkg/commands/healthcheck/healthcheck_test.go +++ b/pkg/commands/healthcheck/healthcheck_test.go @@ -318,6 +318,7 @@ SERVICE VERSION NAME METHOD HOST PATH var listHealthChecksVerboseOutput = strings.Join([]string{ "Fastly API token not provided", "Fastly API endpoint: https://api.fastly.com", + "", "Service ID (via --service-id): 123", "", "Version: 1", diff --git a/pkg/commands/logging/azureblob/azureblob_integration_test.go b/pkg/commands/logging/azureblob/azureblob_integration_test.go index b9f5c2938..37d6a629e 100644 --- a/pkg/commands/logging/azureblob/azureblob_integration_test.go +++ b/pkg/commands/logging/azureblob/azureblob_integration_test.go @@ -344,6 +344,7 @@ SERVICE VERSION NAME var listBlobStoragesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/bigquery/bigquery_integration_test.go b/pkg/commands/logging/bigquery/bigquery_integration_test.go index 8899f2c5b..de6f8283c 100644 --- a/pkg/commands/logging/bigquery/bigquery_integration_test.go +++ b/pkg/commands/logging/bigquery/bigquery_integration_test.go @@ -314,6 +314,7 @@ SERVICE VERSION NAME var listBigQueriesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/cloudfiles/cloudfiles_integration_test.go b/pkg/commands/logging/cloudfiles/cloudfiles_integration_test.go index e4e1362e1..3357ce489 100644 --- a/pkg/commands/logging/cloudfiles/cloudfiles_integration_test.go +++ b/pkg/commands/logging/cloudfiles/cloudfiles_integration_test.go @@ -335,6 +335,7 @@ SERVICE VERSION NAME var listCloudfilesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/datadog/datadog_integration_test.go b/pkg/commands/logging/datadog/datadog_integration_test.go index 28ef9912a..c5f02648b 100644 --- a/pkg/commands/logging/datadog/datadog_integration_test.go +++ b/pkg/commands/logging/datadog/datadog_integration_test.go @@ -311,6 +311,7 @@ SERVICE VERSION NAME var listDatadogsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/digitalocean/digitalocean_integration_test.go b/pkg/commands/logging/digitalocean/digitalocean_integration_test.go index f21d3c4af..6a085e850 100644 --- a/pkg/commands/logging/digitalocean/digitalocean_integration_test.go +++ b/pkg/commands/logging/digitalocean/digitalocean_integration_test.go @@ -335,6 +335,7 @@ SERVICE VERSION NAME var listDigitalOceansVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/elasticsearch/elasticsearch_integration_test.go b/pkg/commands/logging/elasticsearch/elasticsearch_integration_test.go index db3a736bf..59424645d 100644 --- a/pkg/commands/logging/elasticsearch/elasticsearch_integration_test.go +++ b/pkg/commands/logging/elasticsearch/elasticsearch_integration_test.go @@ -339,6 +339,7 @@ SERVICE VERSION NAME var listElasticsearchsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/ftp/ftp_integration_test.go b/pkg/commands/logging/ftp/ftp_integration_test.go index ce528fb93..773f2499c 100644 --- a/pkg/commands/logging/ftp/ftp_integration_test.go +++ b/pkg/commands/logging/ftp/ftp_integration_test.go @@ -331,6 +331,7 @@ SERVICE VERSION NAME var listFTPsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/gcs/gcs_integration_test.go b/pkg/commands/logging/gcs/gcs_integration_test.go index 086b8eeb5..f392e9795 100644 --- a/pkg/commands/logging/gcs/gcs_integration_test.go +++ b/pkg/commands/logging/gcs/gcs_integration_test.go @@ -330,6 +330,7 @@ SERVICE VERSION NAME var listGCSsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/googlepubsub/googlepubsub_integration_test.go b/pkg/commands/logging/googlepubsub/googlepubsub_integration_test.go index 26a879f31..7fbde08bf 100644 --- a/pkg/commands/logging/googlepubsub/googlepubsub_integration_test.go +++ b/pkg/commands/logging/googlepubsub/googlepubsub_integration_test.go @@ -321,6 +321,7 @@ SERVICE VERSION NAME var listGooglePubSubsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/heroku/heroku_integration_test.go b/pkg/commands/logging/heroku/heroku_integration_test.go index 508ae9a16..11b7717ae 100644 --- a/pkg/commands/logging/heroku/heroku_integration_test.go +++ b/pkg/commands/logging/heroku/heroku_integration_test.go @@ -311,6 +311,7 @@ SERVICE VERSION NAME var listHerokusVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/honeycomb/honeycomb_integration_test.go b/pkg/commands/logging/honeycomb/honeycomb_integration_test.go index bd698c15e..170478dc9 100644 --- a/pkg/commands/logging/honeycomb/honeycomb_integration_test.go +++ b/pkg/commands/logging/honeycomb/honeycomb_integration_test.go @@ -311,6 +311,7 @@ SERVICE VERSION NAME var listHoneycombsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/https/https_integration_test.go b/pkg/commands/logging/https/https_integration_test.go index 75ab9dfea..dd7d5be08 100644 --- a/pkg/commands/logging/https/https_integration_test.go +++ b/pkg/commands/logging/https/https_integration_test.go @@ -345,6 +345,7 @@ SERVICE VERSION NAME var listHTTPSsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/kafka/kafka_integration_test.go b/pkg/commands/logging/kafka/kafka_integration_test.go index f202d6bcd..21561d325 100644 --- a/pkg/commands/logging/kafka/kafka_integration_test.go +++ b/pkg/commands/logging/kafka/kafka_integration_test.go @@ -357,6 +357,7 @@ SERVICE VERSION NAME var listKafkasVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 @@ -403,7 +404,7 @@ Version: 1 Max batch size: 0 SASL authentication method: SASL authentication username: - SASL authentication password: + SASL authentication password: `) + " \n\n" func getKafkaOK(i *fastly.GetKafkaInput) (*fastly.Kafka, error) { diff --git a/pkg/commands/logging/kinesis/kinesis_integration_test.go b/pkg/commands/logging/kinesis/kinesis_integration_test.go index 206b07f18..d83b21f77 100644 --- a/pkg/commands/logging/kinesis/kinesis_integration_test.go +++ b/pkg/commands/logging/kinesis/kinesis_integration_test.go @@ -352,6 +352,7 @@ SERVICE VERSION NAME var listKinesesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/loggly/loggly_integration_test.go b/pkg/commands/logging/loggly/loggly_integration_test.go index 3f8da0e76..a95396d3d 100644 --- a/pkg/commands/logging/loggly/loggly_integration_test.go +++ b/pkg/commands/logging/loggly/loggly_integration_test.go @@ -309,6 +309,7 @@ SERVICE VERSION NAME var listLogglysVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/logshuttle/logshuttle_integration_test.go b/pkg/commands/logging/logshuttle/logshuttle_integration_test.go index 668cf1373..f7ea5760d 100644 --- a/pkg/commands/logging/logshuttle/logshuttle_integration_test.go +++ b/pkg/commands/logging/logshuttle/logshuttle_integration_test.go @@ -311,6 +311,7 @@ SERVICE VERSION NAME var listLogshuttlesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/newrelic/newrelic_test.go b/pkg/commands/logging/newrelic/newrelic_test.go index 6f6a6bd09..cfcb8948a 100644 --- a/pkg/commands/logging/newrelic/newrelic_test.go +++ b/pkg/commands/logging/newrelic/newrelic_test.go @@ -269,7 +269,7 @@ func TestNewRelicList(t *testing.T) { ListNewRelicFn: listNewRelic, }, Args: args("logging newrelic list --service-id 123 --verbose --version 1"), - WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\n\nToken: \n\nFormat: \n\nFormat Version: 0\n\nPlacement: \n\nRegion: \n\nResponse Condition: \n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\n\nToken: \n\nFormat: \n\nFormat Version: 0\n\nPlacement: \n\nRegion: \n\nResponse Condition: \n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", + WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\n\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\n\nToken: \n\nFormat: \n\nFormat Version: 0\n\nPlacement: \n\nRegion: \n\nResponse Condition: \n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\n\nToken: \n\nFormat: \n\nFormat Version: 0\n\nPlacement: \n\nRegion: \n\nResponse Condition: \n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", }, } diff --git a/pkg/commands/logging/openstack/openstack_integration_test.go b/pkg/commands/logging/openstack/openstack_integration_test.go index cd159684f..c8e414320 100644 --- a/pkg/commands/logging/openstack/openstack_integration_test.go +++ b/pkg/commands/logging/openstack/openstack_integration_test.go @@ -337,6 +337,7 @@ SERVICE VERSION NAME var listOpenstacksVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/papertrail/papertrail_integration_test.go b/pkg/commands/logging/papertrail/papertrail_integration_test.go index d9d1eabcc..1b5912260 100644 --- a/pkg/commands/logging/papertrail/papertrail_integration_test.go +++ b/pkg/commands/logging/papertrail/papertrail_integration_test.go @@ -306,6 +306,7 @@ SERVICE VERSION NAME var listPapertrailsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/s3/s3_integration_test.go b/pkg/commands/logging/s3/s3_integration_test.go index 4b07132de..a7a4154e1 100644 --- a/pkg/commands/logging/s3/s3_integration_test.go +++ b/pkg/commands/logging/s3/s3_integration_test.go @@ -397,6 +397,7 @@ SERVICE VERSION NAME var listS3sVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/scalyr/scalyr_integration_test.go b/pkg/commands/logging/scalyr/scalyr_integration_test.go index a0a93906d..8ef3fb8fb 100644 --- a/pkg/commands/logging/scalyr/scalyr_integration_test.go +++ b/pkg/commands/logging/scalyr/scalyr_integration_test.go @@ -342,6 +342,7 @@ SERVICE VERSION NAME var listScalyrsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/sftp/sftp_integration_test.go b/pkg/commands/logging/sftp/sftp_integration_test.go index 123e8bbf8..1f3145221 100644 --- a/pkg/commands/logging/sftp/sftp_integration_test.go +++ b/pkg/commands/logging/sftp/sftp_integration_test.go @@ -340,6 +340,7 @@ SERVICE VERSION NAME var listSFTPsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/splunk/splunk_integration_test.go b/pkg/commands/logging/splunk/splunk_integration_test.go index a1e9b2859..463d9a396 100644 --- a/pkg/commands/logging/splunk/splunk_integration_test.go +++ b/pkg/commands/logging/splunk/splunk_integration_test.go @@ -314,6 +314,7 @@ SERVICE VERSION NAME var listSplunksVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/sumologic/sumologic_integration_test.go b/pkg/commands/logging/sumologic/sumologic_integration_test.go index 14f6d059d..cd3720c09 100644 --- a/pkg/commands/logging/sumologic/sumologic_integration_test.go +++ b/pkg/commands/logging/sumologic/sumologic_integration_test.go @@ -306,6 +306,7 @@ SERVICE VERSION NAME var listSumologicsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/logging/syslog/syslog_integration_test.go b/pkg/commands/logging/syslog/syslog_integration_test.go index 1747d78d9..c31db1bef 100644 --- a/pkg/commands/logging/syslog/syslog_integration_test.go +++ b/pkg/commands/logging/syslog/syslog_integration_test.go @@ -324,6 +324,7 @@ SERVICE VERSION NAME var listSyslogsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Version: 1 diff --git a/pkg/commands/profile/create.go b/pkg/commands/profile/create.go index 3867bf0d2..01f2f18ec 100644 --- a/pkg/commands/profile/create.go +++ b/pkg/commands/profile/create.go @@ -17,6 +17,7 @@ import ( "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" "github.com/fastly/go-fastly/v7/fastly" + "github.com/theckman/yacspin" ) // CreateCommand represents a Kingpin command. @@ -85,22 +86,27 @@ func (c *CreateCommand) tokenFlow(profileName string, def bool, in io.Reader, ou endpoint, _ := c.Globals.Endpoint() - progress := text.NewProgress(out, c.Globals.Verbose()) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } + defer func() { if err != nil { c.Globals.ErrLog.Add(err) - progress.Fail() // progress.Done is handled inline } }() - user, err := c.validateToken(token, endpoint, progress) + user, err := c.validateToken(token, endpoint, spinner) if err != nil { return err } - c.updateInMemCfg(profileName, user.Login, token, endpoint, def, progress) + err = c.updateInMemCfg(profileName, user.Login, token, endpoint, def, spinner) + if err != nil { + return err + } - progress.Done() return nil } @@ -131,20 +137,39 @@ func validateTokenNotEmpty(s string) error { var ErrEmptyToken = errors.New("token cannot be empty") // validateToken ensures the token can be used to acquire user data. -func (c *CreateCommand) validateToken(token, endpoint string, progress text.Progress) (*fastly.User, error) { - progress.Step("Validating token...") +func (c *CreateCommand) validateToken(token, endpoint string, spinner *yacspin.Spinner) (*fastly.User, error) { + err := spinner.Start() + if err != nil { + return nil, err + } + msg := "Validating token..." + spinner.Message(msg) client, err := c.clientFactory(token, endpoint) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Endpoint": endpoint, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error regenerating Fastly API client: %w", err) } t, err := client.GetTokenSelf() if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error validating token: %w", err) } @@ -155,15 +180,32 @@ func (c *CreateCommand) validateToken(token, endpoint string, progress text.Prog c.Globals.ErrLog.AddWithContext(err, map[string]any{ "User ID": t.UserID, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error fetching token user: %w", err) } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return nil, err + } return user, nil } // updateInMemCfg persists the updated configuration data in-memory. -func (c *CreateCommand) updateInMemCfg(profileName, email, token, endpoint string, def bool, progress text.Progress) { - progress.Step("Persisting configuration...") +func (c *CreateCommand) updateInMemCfg(profileName, email, token, endpoint string, def bool, spinner *yacspin.Spinner) error { + err := spinner.Start() + if err != nil { + return err + } + msg := "Persisting configuration..." + spinner.Message(msg) c.Globals.Config.Fastly.APIEndpoint = endpoint @@ -184,6 +226,9 @@ func (c *CreateCommand) updateInMemCfg(profileName, email, token, endpoint strin c.Globals.Config.Profiles = p } } + + spinner.StopMessage(msg) + return spinner.Stop() } func (c *CreateCommand) persistCfg() error { diff --git a/pkg/commands/profile/update.go b/pkg/commands/profile/update.go index 46fec1bea..41ad3aa64 100644 --- a/pkg/commands/profile/update.go +++ b/pkg/commands/profile/update.go @@ -12,6 +12,7 @@ import ( "github.com/fastly/cli/pkg/profile" "github.com/fastly/cli/pkg/text" "github.com/fastly/go-fastly/v7/fastly" + "github.com/theckman/yacspin" ) // APIClientFactory allows the profile command to regenerate the global Fastly @@ -79,20 +80,21 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { token = p.Token } - text.Break(out) text.Break(out) - progress := text.NewProgress(out, c.Globals.Verbose()) + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } defer func() { if err != nil { c.Globals.ErrLog.Add(err) - progress.Fail() // progress.Done is handled inline } }() endpoint, _ := c.Globals.Endpoint() - u, err := c.validateToken(token, endpoint, progress) + u, err := c.validateToken(token, endpoint, spinner) if err != nil { return err } @@ -117,27 +119,44 @@ func (c *UpdateCommand) Exec(in io.Reader, out io.Writer) error { return fmt.Errorf("error saving config file: %w", err) } - progress.Done() - text.Success(out, "Profile '%s' updated", c.profile) return nil } // validateToken ensures the token can be used to acquire user data. -func (c *UpdateCommand) validateToken(token, endpoint string, progress text.Progress) (*fastly.User, error) { - progress.Step("Validating token...") +func (c *UpdateCommand) validateToken(token, endpoint string, spinner *yacspin.Spinner) (*fastly.User, error) { + err := spinner.Start() + if err != nil { + return nil, err + } + msg := "Validating token..." + spinner.Message(msg) client, err := c.clientFactory(token, endpoint) if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Endpoint": endpoint, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error regenerating Fastly API client: %w", err) } t, err := client.GetTokenSelf() if err != nil { c.Globals.ErrLog.Add(err) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error validating token: %w", err) } @@ -148,8 +167,20 @@ func (c *UpdateCommand) validateToken(token, endpoint string, progress text.Prog c.Globals.ErrLog.AddWithContext(err, map[string]any{ "User ID": t.UserID, }) + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return nil, spinErr + } + return nil, fmt.Errorf("error fetching token user: %w", err) } + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return nil, err + } return user, nil } diff --git a/pkg/commands/service/service_test.go b/pkg/commands/service/service_test.go index bdc7ad1c4..457ac9dd3 100644 --- a/pkg/commands/service/service_test.go +++ b/pkg/commands/service/service_test.go @@ -525,6 +525,7 @@ Bar 456 wasm 1 2015-03-14 12:59 var listServicesVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service 1/3 ID: 123 Name: Foo @@ -671,6 +672,7 @@ Versions: 2 var describeServiceVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 ID: 123 @@ -780,6 +782,7 @@ Versions: 2 var searchServiceVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + ID: 123 Name: Foo Type: wasm diff --git a/pkg/commands/serviceauth/service_test.go b/pkg/commands/serviceauth/service_test.go index 69fccc793..9e377c325 100644 --- a/pkg/commands/serviceauth/service_test.go +++ b/pkg/commands/serviceauth/service_test.go @@ -78,7 +78,7 @@ func TestServiceAuthList(t *testing.T) { { args: args("service-auth list --verbose"), api: mock.API{ListServiceAuthorizationsFn: listServiceAuthOK}, - wantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\nAuth ID: 123\nUser ID: 456\nService ID: 789\nPermission: read_only\n", + wantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\n\nAuth ID: 123\nUser ID: 456\nService ID: 789\nPermission: read_only\n", }, } for testcaseIdx := range scenarios { diff --git a/pkg/commands/serviceversion/serviceversion_test.go b/pkg/commands/serviceversion/serviceversion_test.go index d3b5b55c3..dec79e9f5 100644 --- a/pkg/commands/serviceversion/serviceversion_test.go +++ b/pkg/commands/serviceversion/serviceversion_test.go @@ -311,6 +311,7 @@ NUMBER ACTIVE LAST EDITED (UTC) var listVersionsVerboseOutput = strings.TrimSpace(` Fastly API token not provided Fastly API endpoint: https://api.fastly.com + Service ID (via --service-id): 123 Versions: 3 diff --git a/pkg/commands/tls/custom/certificate/list.go b/pkg/commands/tls/custom/certificate/list.go index 8ee63ab22..9ec7688d6 100644 --- a/pkg/commands/tls/custom/certificate/list.go +++ b/pkg/commands/tls/custom/certificate/list.go @@ -115,7 +115,7 @@ func (c *ListCommand) constructInput() *fastly.ListCustomTLSCertificatesInput { // format. func printVerbose(out io.Writer, rs []*fastly.CustomTLSCertificate) { for _, r := range rs { - fmt.Fprintf(out, "\nID: %s\n", r.ID) + fmt.Fprintf(out, "ID: %s\n", r.ID) fmt.Fprintf(out, "Issued to: %s\n", r.IssuedTo) fmt.Fprintf(out, "Issuer: %s\n", r.Issuer) fmt.Fprintf(out, "Name: %s\n", r.Name) diff --git a/pkg/commands/update/root.go b/pkg/commands/update/root.go index 2c5ab504b..a07dad016 100644 --- a/pkg/commands/update/root.go +++ b/pkg/commands/update/root.go @@ -35,39 +35,83 @@ func NewRootCommand(parent cmd.Registerer, configFilePath string, av github.Asse // Exec implements the command interface. func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { + spinner, err := text.NewSpinner(out) + if err != nil { + return err + } + + err = spinner.Start() + if err != nil { + return err + } + msg := "Updating versioning information..." + spinner.Message(msg) + current, latest, shouldUpdate := Check(revision.AppVersion, c.av) + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + text.Break(out) text.Output(out, "Current version: %s", current) text.Output(out, "Latest version: %s", latest) text.Break(out) - progress := text.NewProgress(out, c.Globals.Verbose()) - progress.Step("Updating versioning information...") - - progress.Step("Checking CLI binary update...") if !shouldUpdate { text.Output(out, "No update required.") return nil } - progress.Step("Fetching latest release...") + err = spinner.Start() + if err != nil { + return err + } + msg = "Fetching latest release..." + spinner.Message(msg) + tmpBin, err := c.av.Download() if err != nil { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Current CLI version": current, "Latest CLI version": latest, }) - progress.Fail() + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error downloading latest release: %w", err) } defer os.RemoveAll(tmpBin) - progress.Step("Replacing binary...") + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } + + err = spinner.Start() + if err != nil { + return err + } + msg = "Replacing binary..." + spinner.Message(msg) + execPath, err := os.Executable() if err != nil { c.Globals.ErrLog.Add(err) - progress.Fail() + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error determining executable path: %w", err) } @@ -76,7 +120,13 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { c.Globals.ErrLog.AddWithContext(err, map[string]any{ "Executable path": execPath, }) - progress.Fail() + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } + return fmt.Errorf("error determining absolute target path: %w", err) } @@ -103,13 +153,22 @@ func (c *RootCommand) Exec(_ io.Reader, out io.Writer) error { "Executable (source)": tmpBin, "Executable (destination)": currentPath, }) - progress.Fail() + + spinner.StopFailMessage(msg) + spinErr := spinner.StopFail() + if spinErr != nil { + return spinErr + } return fmt.Errorf("error moving latest binary in place: %w", err) } } - progress.Done() + spinner.StopMessage(msg) + err = spinner.Stop() + if err != nil { + return err + } text.Success(out, "Updated %s to %s.", currentPath, latest) return nil diff --git a/pkg/commands/user/user_test.go b/pkg/commands/user/user_test.go index e71569ec6..0f780ee73 100644 --- a/pkg/commands/user/user_test.go +++ b/pkg/commands/user/user_test.go @@ -408,5 +408,6 @@ bar@example.com bar superuser false current123 func listVerboseOutput() string { return fmt.Sprintf(`Fastly API token provided via --token Fastly API endpoint: https://api.fastly.com + %s%s`, describeUserOutput(), describeCurrentUserOutput()) } diff --git a/pkg/commands/vcl/custom/custom_test.go b/pkg/commands/vcl/custom/custom_test.go index 72b44cb1c..46fe1891c 100644 --- a/pkg/commands/vcl/custom/custom_test.go +++ b/pkg/commands/vcl/custom/custom_test.go @@ -353,7 +353,7 @@ func TestVCLCustomList(t *testing.T) { ListVCLsFn: listVCLs, }, Args: args("vcl custom list --service-id 123 --verbose --version 1"), - WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nMain: true\nContent: \n# some vcl content\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nMain: false\nContent: \n# some vcl content\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", + WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\n\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nMain: true\nContent: \n# some vcl content\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nMain: false\nContent: \n# some vcl content\n\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", }, } diff --git a/pkg/commands/vcl/snippet/snippet_test.go b/pkg/commands/vcl/snippet/snippet_test.go index 7a30a3f8f..2ac12caec 100644 --- a/pkg/commands/vcl/snippet/snippet_test.go +++ b/pkg/commands/vcl/snippet/snippet_test.go @@ -407,7 +407,7 @@ func TestVCLSnippetList(t *testing.T) { ListSnippetsFn: listSnippets, }, Args: args("vcl snippet list --service-id 123 --verbose --version 1"), - WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nID: abc\nPriority: 0\nDynamic: true\nType: recv\nContent: \n# some vcl content\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nID: abc\nPriority: 0\nDynamic: false\nType: recv\nContent: \n# some vcl content\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", + WantOutput: "Fastly API token not provided\nFastly API endpoint: https://api.fastly.com\n\nService ID (via --service-id): 123\n\nService Version: 1\n\nName: foo\nID: abc\nPriority: 0\nDynamic: true\nType: recv\nContent: \n# some vcl content\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n\nName: bar\nID: abc\nPriority: 0\nDynamic: false\nType: recv\nContent: \n# some vcl content\nCreated at: 2021-06-15 23:00:00 +0000 UTC\nUpdated at: 2021-06-15 23:00:00 +0000 UTC\nDeleted at: 2021-06-15 23:00:00 +0000 UTC\n", }, } diff --git a/pkg/commands/whoami/whoami_test.go b/pkg/commands/whoami/whoami_test.go index bdd3e0160..94cb0a488 100644 --- a/pkg/commands/whoami/whoami_test.go +++ b/pkg/commands/whoami/whoami_test.go @@ -144,6 +144,7 @@ var basicOutput = "Alice Programmer \n" var basicOutputVerbose = strings.TrimSpace(` Fastly API token provided via --token Fastly API endpoint: https://api.fastly.com + Customer ID: abc Customer name: Computer Company User ID: 123 diff --git a/pkg/exec/exec.go b/pkg/exec/exec.go index 8cc47e59f..cc1c5d0f6 100644 --- a/pkg/exec/exec.go +++ b/pkg/exec/exec.go @@ -28,7 +28,6 @@ type Streaming struct { Env []string Output io.Writer Process *os.Process - Progress io.Writer SignalCh chan os.Signal Timeout time.Duration Verbose bool @@ -93,23 +92,9 @@ func (s *Streaming) Exec() error { // Pipe the child process stdout and stderr to our own output writer. var stdoutBuf, stderrBuf threadsafe.Buffer - // NOTE: Historically this Exec() function would use a text.Progress as an - // io.Writer so that the command output could be constrained to a single - // line. But now we have commands such as `compute serve` that want the full - // output to be displayed so the user can see things like compilation errors. - // - // The Streaming type now has both s.Out and s.Progress fields. - // - // We presume s.Output to always be set and s.Progress to sometimes be set. - // - // With that in mind, we default to setting `output` to be s.Output and will - // override it to be s.Progress if it has been set. We'll then have to - // override it back to s.Output if the user themselves have appended the - // --verbose flag. - output := s.Output - if s.Progress != nil { - output = s.Progress - } + // We discard the build output unless we're in verbose mode. + output := io.Discard + if s.Verbose { output = s.Output text.Info(output, "Command output:") @@ -121,8 +106,8 @@ func (s *Streaming) Exec() error { if err := cmd.Start(); err != nil { if s.Verbose { - text.Break(output) text.Output(output, divider) + text.Break(output) } return err } @@ -155,15 +140,15 @@ func (s *Streaming) Exec() error { ctx = fmt.Sprintf(":%s\n\n%s", cmdOutput, err) } if s.Verbose { - text.Break(output) text.Output(output, divider) + text.Break(output) } return fmt.Errorf("error during execution process%s", ctx) } if s.Verbose { - text.Break(output) text.Output(output, divider) + text.Break(output) } return nil } diff --git a/pkg/text/progress.go b/pkg/text/progress.go deleted file mode 100644 index 5f45e63d7..000000000 --- a/pkg/text/progress.go +++ /dev/null @@ -1,406 +0,0 @@ -package text - -import ( - "bytes" - "context" - "encoding/hex" - "fmt" - "io" - "os" - "runtime" - "strings" - "sync" - "time" - "unicode/utf8" - - fstruntime "github.com/fastly/cli/pkg/runtime" - "github.com/mattn/go-isatty" - "github.com/theckman/yacspin" -) - -// NewSpinner returns a new instance of a terminal prompt spinner. -func NewSpinner(out io.Writer) (*yacspin.Spinner, error) { - spinner, err := yacspin.New(yacspin.Config{ - CharSet: yacspin.CharSets[9], - Frequency: 100 * time.Millisecond, - StopCharacter: "✓", - StopColors: []string{"fgGreen"}, - StopFailCharacter: "✗", - StopFailColors: []string{"fgRed"}, - Suffix: " ", - Writer: out, - }) - if err != nil { - return nil, err - } - return spinner, nil -} - -// Progress is a producer contract, abstracting over the quiet and verbose -// Progress types. Consumers may use a Progress value in their code, and assign -// it based on the presence of a -v, --verbose flag. Callers are expected to -// call Step for each new major step of their procedural code, and Write with -// the verbose or detailed output of those steps. Callers must eventually call -// either Done or Fail, to signal success or failure respectively. -type Progress interface { - io.Writer - Tick(rune) - Step(string) - Done() - Fail() -} - -// ProgressOptions determines if the initialization message is displayed. -// e.g. "Initializing..." step header. -type ProgressOptions struct { - reset bool -} - -// Option represents optional configuration for a Progress type. -type Option func(*ProgressOptions) - -// NewProgress returns a Progress based on the given verbosity level or whether -// the current process is running in a terminal environment. -func NewProgress(output io.Writer, verbose bool, options ...Option) Progress { - var progress Progress - if verbose { - progress = NewVerboseProgress(output) - } else if isTerminal() { - progress = NewInteractiveProgress(output, options...) - } else { - progress = NewQuietProgress(output) - } - return progress -} - -// ResetProgress wraps the NewProgress and passes through a restart option. -// -// NOTE: A Progress sometimes needs to be marked as Done() so that other output -// can be printed to the terminal (otherwise the Progress will always overwrite -// the output). If we just call NewProgress again then we'll see an -// 'Initializing...' message which looks odd. Instead we can now reset the -// progress instead which will simply tell the Progress type not to set that -// step header. -func ResetProgress(output io.Writer, verbose bool) Progress { - return NewProgress(output, verbose, WithReset()) -} - -// WithReset resets the ProgressOptions. -func WithReset() Option { - return func(p *ProgressOptions) { - p.reset = true - } -} - -// isTerminal indicates if the consumer is a modern terminal. -// -// EXAMPLE: If the user is on a standard Windows 'command prompt' the spinner -// output doesn't work, nor does any colour output, so we avoid both features. -func isTerminal() bool { - if isatty.IsTerminal(os.Stdout.Fd()) && !fstruntime.Windows || isatty.IsCygwinTerminal(os.Stdout.Fd()) { - return true - } - return false -} - -// Ticker is a small consumer contract for the Spin function, -// capturing part of the Progress interface. -type Ticker interface { - Tick(r rune) -} - -// Spin calls Tick on the target with the relevant frame every interval. It -// returns when context is canceled, so should be called in its own goroutine. -func Spin(ctx context.Context, frames []rune, interval time.Duration, target Ticker) error { - var ( - cursor = 0 - ticker = time.NewTicker(interval) - ) - defer ticker.Stop() - for { - select { - case <-ticker.C: - target.Tick(frames[cursor]) - cursor = (cursor + 1) % len(frames) - case <-ctx.Done(): - return ctx.Err() - } - } -} - -// InteractiveProgress is an implementation of Progress that includes a spinner at the -// beginning of each Step, and where newline-delimited lines written via Write -// overwrite the current step line in the output. -type InteractiveProgress struct { - mtx sync.Mutex - output io.Writer - reset bool - - stepHeader string // title of current step - writeBuffer bytes.Buffer // receives Write calls - lastBufferLine string // last full line in writeBuffer - currentOutput string // the content of the current line displayed to user - - cancel func() // tell Spin to stop - done <-chan struct{} // wait for Spin to stop -} - -// NewInteractiveProgress returns a InteractiveProgress outputting to the writer. -func NewInteractiveProgress(output io.Writer, options ...Option) *InteractiveProgress { - opts := &ProgressOptions{ - reset: false, - } - for _, o := range options { - o(opts) - } - p := &InteractiveProgress{ - output: output, - stepHeader: "Initializing...", - reset: opts.reset, - } - - var ( - ctx, cancel = context.WithCancel(context.Background()) - done = make(chan struct{}) - ) - go func() { - Spin(ctx, []rune{'-', '\\', '|', '/'}, 100*time.Millisecond, p) - close(done) - }() - p.cancel = cancel - p.done = done - - return p -} - -func (p *InteractiveProgress) replaceLine(format string, args ...any) { - // Clear the current line. - n := utf8.RuneCountInString(p.currentOutput) - switch runtime.GOOS { - case "windows": - fmt.Fprintf(p.output, "%s\r", strings.Repeat(" ", n)) - default: - del, _ := hex.DecodeString("7f") - sequence := fmt.Sprintf("\b%s\b\033[K", del) - fmt.Fprintf(p.output, "%s\r", strings.Repeat(sequence, n)) - } - - // Generate the new line. - s := fmt.Sprintf(format, args...) - p.currentOutput = s - fmt.Fprint(p.output, p.currentOutput) -} - -func (p *InteractiveProgress) getStatus() string { - if p.lastBufferLine != "" { - return p.lastBufferLine // takes precedence - } - return p.stepHeader -} - -// Tick implements the Progress interface. -func (p *InteractiveProgress) Tick(r rune) { - p.mtx.Lock() - defer p.mtx.Unlock() - - p.replaceLine("%s %s", string(r), p.getStatus()) -} - -// Write implements the Progress interface, emitting each incoming byte slice -// to the internal buffer to be written to the terminal on the next tick. -func (p *InteractiveProgress) Write(buf []byte) (int, error) { - p.mtx.Lock() - defer p.mtx.Unlock() - - _, err := p.writeBuffer.Write(buf) - if err != nil { - return len(buf), err - } - p.lastBufferLine = LastFullLine(p.writeBuffer.String()) - - return len(buf), nil -} - -// Step implements the Progress interface. -func (p *InteractiveProgress) Step(msg string) { - msg = strings.TrimSpace(msg) - - p.mtx.Lock() - defer p.mtx.Unlock() - - if !p.reset { - // Previous step complete. - p.replaceLine("%s %s", Bold("✓"), p.stepHeader) - fmt.Fprintln(p.output) - } - - // Avoid printing Initializing... - if p.reset && p.stepHeader == "Initializing..." { - p.reset = false - } - - // Reset all the stepwise state. - p.stepHeader = msg - p.writeBuffer.Reset() - p.lastBufferLine = "" - p.currentOutput = "" - - // New step beginning. - p.replaceLine("%s %s", Bold("·"), p.stepHeader) -} - -// Done implements the Progress interface. -func (p *InteractiveProgress) Done() { - // It's important to cancel the Spin goroutine before taking the lock, - // because otherwise it's possible to generate a deadlock if the output - // io.Writer is also synchronized. - p.cancel() - <-p.done - - p.mtx.Lock() - defer p.mtx.Unlock() - - p.replaceLine("%s %s", Bold("✓"), p.stepHeader) - fmt.Fprintln(p.output) -} - -// Fail implements the Progress interface. -func (p *InteractiveProgress) Fail() { - p.cancel() - <-p.done - - p.mtx.Lock() - defer p.mtx.Unlock() - - p.replaceLine("%s %s", Bold("✗"), p.stepHeader) - fmt.Fprintln(p.output) -} - -// LastFullLine returns the last full \n delimited line in s. That is, s must -// contain at least one \n for LastFullLine to return anything. -func LastFullLine(s string) string { - last := strings.LastIndex(s, "\n") - if last < 0 { - return "" - } - - prev := strings.LastIndex(s[:last], "\n") - if prev < 0 { - prev = 0 - } - - return strings.TrimSpace(s[prev:last]) -} - -// -// -// - -// QuietProgress is an implementation of Progress that attempts to be quiet in -// it's output. I.e. it only prints each Step as it progresses and discards any -// intermediary writes between steps. No spinners are used, therefore it's -// useful for non-TTY environiments, such as CI. -type QuietProgress struct { - output io.Writer - nullWriter io.Writer -} - -// NewQuietProgress returns a QuietProgress outputting to the writer. -func NewQuietProgress(output io.Writer) *QuietProgress { - qp := &QuietProgress{ - output: output, - nullWriter: io.Discard, - } - qp.Step("Initializing...") - return qp -} - -// Tick implements the Progress interface. It's a no-op. -func (p *QuietProgress) Tick(_ rune) {} - -// Tick implements the Progress interface. -func (p *QuietProgress) Write(buf []byte) (int, error) { - return p.nullWriter.Write(buf) -} - -// Step implements the Progress interface. -func (p *QuietProgress) Step(msg string) { - fmt.Fprintln(p.output, strings.TrimSpace(msg)) -} - -// Done implements the Progress interface. It's a no-op. -func (p *QuietProgress) Done() {} - -// Fail implements the Progress interface. It's a no-op. -func (p *QuietProgress) Fail() {} - -// -// -// - -// VerboseProgress is an implementation of Progress that treats Step and Write -// more or less the same: it simply pipes all output to the provided Writer. No -// spinners are used. -type VerboseProgress struct { - output io.Writer -} - -// NewVerboseProgress returns a VerboseProgress outputting to the writer. -func NewVerboseProgress(output io.Writer) *VerboseProgress { - return &VerboseProgress{ - output: output, - } -} - -// Tick implements the Progress interface. It's a no-op. -func (p *VerboseProgress) Tick(_ rune) {} - -// Tick implements the Progress interface. -func (p *VerboseProgress) Write(buf []byte) (int, error) { - return p.output.Write(buf) -} - -// Step implements the Progress interface. -func (p *VerboseProgress) Step(msg string) { - fmt.Fprintln(p.output, strings.TrimSpace(msg)) -} - -// Done implements the Progress interface. It's a no-op. -func (p *VerboseProgress) Done() {} - -// Fail implements the Progress interface. It's a no-op. -func (p *VerboseProgress) Fail() {} - -// -// -// - -// NullProgress is an implementation of Progress which discards everything -// written into it and produces no output. -type NullProgress struct { - output io.Writer -} - -// NewNullProgress returns a NullProgress. -func NewNullProgress() *NullProgress { - return &NullProgress{ - output: io.Discard, - } -} - -// Tick implements the Progress interface. It's a no-opt -func (p *NullProgress) Tick(_ rune) {} - -// Tick implements the Progress interface. -func (p *NullProgress) Write(buf []byte) (int, error) { - return p.output.Write(buf) -} - -// Step implements the Progress interface. -func (p *NullProgress) Step(_ string) {} - -// Done implements the Progress interface. It's a no-op. -func (p *NullProgress) Done() {} - -// Fail implements the Progress interface. It's a no-op. -func (p *NullProgress) Fail() {} diff --git a/pkg/text/progress_test.go b/pkg/text/progress_test.go deleted file mode 100644 index 1c218e8c3..000000000 --- a/pkg/text/progress_test.go +++ /dev/null @@ -1,98 +0,0 @@ -package text_test - -import ( - "fmt" - "io" - "os" - "testing" - "time" - - "github.com/fastly/cli/pkg/text" -) - -func TestProgress(t *testing.T) { - for _, testcase := range []struct { - name string - constructor func(io.Writer) text.Progress - }{ - { - name: "quiet", - constructor: func(w io.Writer) text.Progress { return text.NewQuietProgress(w) }, - }, - { - name: "verbose", - constructor: func(w io.Writer) text.Progress { return text.NewVerboseProgress(w) }, - }, - } { - t.Run(testcase.name, func(t *testing.T) { - p := testcase.constructor(os.Stdout) - for _, f := range []func(){ - func() { fmt.Fprintf(p, "Alpha\n") }, - func() { p.Step("Step one...") }, - func() { fmt.Fprintf(p, "Beta\n") }, - func() { fmt.Fprintf(p, "Delta\n") }, - func() { p.Step("Step two...") }, - func() { fmt.Fprintf(p, "Iota\n") }, - func() { fmt.Fprintf(p, "Kappa\n") }, - func() { fmt.Fprintf(p, "Gamma\n") }, - func() { fmt.Fprintf(p, "Omicron\n") }, - func() { p.Step("Step three...") }, - func() { fmt.Fprintf(p, "Nü\n") }, - func() { p.Done() }, - } { - f() - time.Sleep(250 * time.Millisecond) - } - }) - } -} - -func TestLastFullLine(t *testing.T) { - for _, testcase := range []struct { - name string - input string - want string - }{ - { - name: "empty", - input: "", - want: "", - }, - { - name: "no newline", - input: "abc def ghi", - want: "", - }, - { - name: "one newline at end", - input: "abc def ghi\n", - want: "abc def ghi", - }, - { - name: "one full line and a partial", - input: "foo bar\nbaz quux", - want: "foo bar", - }, - { - name: "multiple lines", - input: "alpha beta\ndelta kappa\ngamma iota\nomicron nu", - want: "gamma iota", - }, - { - name: "multiple newlines at end", - input: "alpha beta\n\n\ndelta kappa\n\ngamma iota\n\nomicron nu\n\n", - want: "", - }, - { - name: "multiple newlines in middle", - input: "alpha beta\n\n\ndelta kappa\n\ngamma iota\n\nomicron nu", - want: "", - }, - } { - t.Run(testcase.name, func(t *testing.T) { - if want, have := testcase.want, text.LastFullLine(testcase.input); want != have { - t.Fatalf("want %q, have %q", want, have) - } - }) - } -} diff --git a/pkg/text/spinner.go b/pkg/text/spinner.go new file mode 100644 index 000000000..1c1882b95 --- /dev/null +++ b/pkg/text/spinner.go @@ -0,0 +1,26 @@ +package text + +import ( + "io" + "time" + + "github.com/theckman/yacspin" +) + +// NewSpinner returns a new instance of a terminal prompt spinner. +func NewSpinner(out io.Writer) (*yacspin.Spinner, error) { + spinner, err := yacspin.New(yacspin.Config{ + CharSet: yacspin.CharSets[9], + Frequency: 100 * time.Millisecond, + StopCharacter: "✓", + StopColors: []string{"fgGreen"}, + StopFailCharacter: "✗", + StopFailColors: []string{"fgRed"}, + Suffix: " ", + Writer: out, + }) + if err != nil { + return nil, err + } + return spinner, nil +}