diff --git a/cmd/meroxa/builder/builder.go b/cmd/meroxa/builder/builder.go index 62271740d..15eba88c6 100644 --- a/cmd/meroxa/builder/builder.go +++ b/cmd/meroxa/builder/builder.go @@ -481,7 +481,11 @@ func buildCommandWithExecute(cmd *cobra.Command, c Command) { return err } } - return v.Execute(cmd.Context()) + err := v.Execute(cmd.Context()) + if err != nil && strings.Contains(err.Error(), "Unknown or invalid refresh token") { + return fmt.Errorf("unknown or invalid refresh token, please run `meroxa login` again") + } + return err } } diff --git a/cmd/meroxa/global/logger.go b/cmd/meroxa/global/logger.go index 85dbe2e21..7432b24ca 100644 --- a/cmd/meroxa/global/logger.go +++ b/cmd/meroxa/global/logger.go @@ -28,6 +28,7 @@ func NewLogger() log.Logger { logLevel = log.Info leveledLoggerOut = os.Stdout jsonLoggerOut = io.Discard + spinnerLoggerOut = os.Stdout ) if flagJSON { @@ -41,5 +42,6 @@ func NewLogger() log.Logger { return log.New( log.NewLeveledLogger(leveledLoggerOut, logLevel), log.NewJSONLogger(jsonLoggerOut), + log.NewSpinnerLogger(spinnerLoggerOut), ) } diff --git a/cmd/meroxa/root/apps/deploy.go b/cmd/meroxa/root/apps/deploy.go index 23c5572a4..1077541aa 100644 --- a/cmd/meroxa/root/apps/deploy.go +++ b/cmd/meroxa/root/apps/deploy.go @@ -62,13 +62,15 @@ type Deploy struct { DockerHubAccessToken string `long:"docker-hub-access-token" description:"DockerHub access token to use to build and deploy the app" hidden:"true"` //nolint:lll } - client deployApplicationClient - config config.Config - logger log.Logger - appName string - path string - lang string - goDeploy turbineGo.Deploy + client deployApplicationClient + config config.Config + logger log.Logger + appName string + path string + lang string + localDeploy turbineCLI.LocalDeploy + fnName string + tempPath string // find something more elegant to this } var ( @@ -123,14 +125,14 @@ func (d *Deploy) getDockerHubAccessTokenEnv() string { func (d *Deploy) validateDockerHubFlags() error { if d.flags.DockerHubUserName != "" { - d.goDeploy.DockerHubUserNameEnv = d.flags.DockerHubUserName + d.localDeploy.DockerHubUserNameEnv = d.flags.DockerHubUserName if d.flags.DockerHubAccessToken == "" { return errors.New("--docker-hub-access-token is required when --docker-hub-username is present") } } if d.flags.DockerHubAccessToken != "" { - d.goDeploy.DockerHubAccessTokenEnv = d.flags.DockerHubAccessToken + d.localDeploy.DockerHubAccessTokenEnv = d.flags.DockerHubAccessToken if d.flags.DockerHubUserName == "" { return errors.New("--docker-hub-username is required when --docker-hub-access-token is present") } @@ -140,13 +142,13 @@ func (d *Deploy) validateDockerHubFlags() error { func (d *Deploy) validateDockerHubEnvVars() error { if d.getDockerHubUserNameEnv() != "" { - d.goDeploy.DockerHubUserNameEnv = d.getDockerHubUserNameEnv() + d.localDeploy.DockerHubUserNameEnv = d.getDockerHubUserNameEnv() if d.getDockerHubAccessTokenEnv() == "" { return fmt.Errorf("%s is required when %s is present", dockerHubAccessTokenEnv, dockerHubUserNameEnv) } } if d.getDockerHubAccessTokenEnv() != "" { - d.goDeploy.DockerHubAccessTokenEnv = d.getDockerHubAccessTokenEnv() + d.localDeploy.DockerHubAccessTokenEnv = d.getDockerHubAccessTokenEnv() if d.getDockerHubUserNameEnv() == "" { return fmt.Errorf("%s is required when %s is present", dockerHubUserNameEnv, dockerHubAccessTokenEnv) } @@ -168,8 +170,8 @@ func (d *Deploy) validateLocalDeploymentConfig() error { } // If there are DockerHub credentials, we consider it a local deployment - if d.goDeploy.DockerHubUserNameEnv != "" && d.goDeploy.DockerHubAccessTokenEnv != "" { - d.goDeploy.LocalDeployment = true + if d.localDeploy.DockerHubUserNameEnv != "" && d.localDeploy.DockerHubAccessTokenEnv != "" { + d.localDeploy.Enabled = true } return nil } @@ -191,7 +193,8 @@ func (d *Deploy) createApplication(ctx context.Context, pipelineUUID, gitSha str GitSha: gitSha, Pipeline: meroxa.EntityIdentifier{UUID: null.StringFrom(pipelineUUID)}, } - d.logger.Infof(ctx, "Creating application %q with language %q...", input.Name, d.lang) + + d.logger.StartSpinner("\t", fmt.Sprintf("Creating application %q (%s)...", input.Name, d.lang)) res, err := d.client.CreateApplication(ctx, &input) if err != nil { @@ -200,17 +203,25 @@ func (d *Deploy) createApplication(ctx context.Context, pipelineUUID, gitSha str var app *meroxa.Application app, err = d.client.GetApplication(ctx, appName) if err != nil { + d.logger.StopSpinner("\t") return err } if app.Pipeline.UUID.String != pipelineUUID { - return fmt.Errorf("unable to finish creating the %s Application because its entities are in an"+ - " unrecoverable state; try deleting and re-deploying", appName) + d.logger.StopSpinner(fmt.Sprintf("\t ๐„‚ unable to finish creating the %s Application because its entities are in an"+ + " unrecoverable state; try deleting and re-deploying", appName)) + + d.logger.StopSpinner("\t") + // TODO: Rollback here? + return fmt.Errorf("unable to finish creating application") } } + d.logger.StopSpinner("\t") return err } - d.logger.Infof(ctx, "Application %q successfully created!", res.Name) + dashboardURL := fmt.Sprintf("https://dashboard.meroxa.io/v2/apps/%s/detail", res.UUID) + output := fmt.Sprintf("\tโœ” Application %q successfully created!\n\n โœจ To visualize your application visit %s", res.Name, dashboardURL) + d.logger.StopSpinner(output) d.logger.JSON(ctx, res) return nil } @@ -218,16 +229,23 @@ func (d *Deploy) createApplication(ctx context.Context, pipelineUUID, gitSha str // uploadSource creates first a Dockerfile to then, package the entire folder which will be later uploaded // this should ignore .git files and fixtures/. func (d *Deploy) uploadSource(ctx context.Context, appPath, url string) error { - // Before creating a .tar.zip, we make sure it contains a Dockerfile. - err := turbine.CreateDockerfile(appPath) - if err != nil { - return err + var err error + + if d.lang == GoLang { + err = turbine.CreateDockerfile(appPath) + if err != nil { + return err + } } dFile := fmt.Sprintf("turbine-%s.tar.gz", d.appName) var buf bytes.Buffer - d.logger.Infof(ctx, "Packaging application located at %q...", appPath) + + if d.lang == JavaScript || d.lang == Python { + appPath = d.tempPath + } + err = turbineCLI.CreateTarAndZipFile(appPath, &buf) if err != nil { return err @@ -248,10 +266,12 @@ func (d *Deploy) uploadSource(ctx context.Context, appPath, url string) error { return err } - // We clean up Dockerfile as last step - err = os.Remove(filepath.Join(appPath, "Dockerfile")) - if err != nil { - return err + if d.lang == GoLang { + // We clean up Dockerfile as last step + err = os.Remove(filepath.Join(appPath, "Dockerfile")) + if err != nil { + return err + } } err = d.uploadFile(ctx, dFile, url) @@ -259,30 +279,32 @@ func (d *Deploy) uploadSource(ctx context.Context, appPath, url string) error { return err } + // TODO: Remove d.tempPath for JS and Python apps // remove .tar.gz file return os.Remove(dFile) } func (d *Deploy) uploadFile(ctx context.Context, filePath, url string) error { - d.logger.Info(ctx, "Uploading file to our build service...") + d.logger.StartSpinner("\t", " Uploading source...") + fh, err := os.Open(filePath) if err != nil { + d.logger.StopSpinner("\t") return err } defer func(fh *os.File) { - err = fh.Close() - if err != nil { - d.logger.Warn(ctx, err.Error()) - } + fh.Close() }(fh) req, err := http.NewRequestWithContext(ctx, "PUT", url, fh) if err != nil { + d.logger.StopSpinner("\t") return err } fi, err := fh.Stat() if err != nil { + d.logger.StopSpinner("\t") return err } @@ -296,24 +318,30 @@ func (d *Deploy) uploadFile(ctx context.Context, filePath, url string) error { client := &http.Client{} res, err := client.Do(req) //nolint:bodyclose if err != nil { + d.logger.StopSpinner("\t") return err } defer func(Body io.ReadCloser) { err := Body.Close() - d.logger.Infof(ctx, "Uploaded!") if err != nil { d.logger.Error(ctx, err.Error()) } }(res.Body) + d.logger.StopSpinner("\tโœ” Source uploaded!") return nil } func (d *Deploy) getPlatformImage(ctx context.Context, appPath string) (string, error) { + d.logger.StartSpinner("\t", " Fetching Meroxa Platform source...") + s, err := d.client.CreateSource(ctx) if err != nil { + d.logger.Errorf(ctx, "\t ๐„‚ Unable to fetch source") + d.logger.StopSpinner("\t") return "", err } + d.logger.StopSpinner("\tโœ” Platform source fetched!") err = d.uploadSource(ctx, appPath, s.PutUrl) if err != nil { @@ -323,116 +351,201 @@ func (d *Deploy) getPlatformImage(ctx context.Context, appPath string) (string, sourceBlob := meroxa.SourceBlob{Url: s.GetUrl} buildInput := &meroxa.CreateBuildInput{SourceBlob: sourceBlob} + d.logger.StartSpinner("\t", " Building Meroxa Process image...") + build, err := d.client.CreateBuild(ctx, buildInput) if err != nil { + d.logger.StopSpinner("\t") return "", err } - fmt.Printf("Getting status for build: %s ", build.Uuid) for { - fmt.Printf(".") b, err := d.client.GetBuild(ctx, build.Uuid) if err != nil { + d.logger.StopSpinner("\t") return "", err } switch b.Status.State { case "error": - return "", fmt.Errorf("build with uuid %q errored ", b.Uuid) + d.logger.StopSpinner(fmt.Sprintf("\t ๐„‚ build with uuid %q errored\nRun `meroxa build logs %s` for more information", b.Uuid, b.Uuid)) + return "", fmt.Errorf("build with uuid %q errored", b.Uuid) case "complete": - fmt.Println("\nImage built! ") + d.logger.StopSpinner("\tโœ” Process image built!") return build.Image, nil } time.Sleep(pollDuration) } } -// Deploy takes care of all the necessary steps to deploy a Turbine application -// 1. Build binary // different for jS -// 2. Build image // common -// 3. Push image // common -// 4. Run Turbine deploy // different -func (d *Deploy) deploy(ctx context.Context, appPath string, l log.Logger) (string, error) { +func (d *Deploy) deployApp(ctx context.Context, imageName string) (string, error) { + var output string + var err error + + d.logger.StartSpinner("\t", fmt.Sprintf(" Deploying application %q...", d.appName)) + switch d.lang { + case GoLang: + output, err = turbineGo.RunDeployApp(ctx, d.logger, d.path, d.appName, imageName) + case JavaScript: + // TODO: @james + // TODO: Do less here!!! + output, err = turbineJS.Deploy(ctx, d.path, imageName, d.logger) + case Python: + // TODO: @eric + } + + if err != nil { + d.logger.StopSpinner("\t๐„‚ Deployment failed\n\n") + return "", err + } + + d.logger.StopSpinner("\tโœ” Deploy complete!") + return output, nil +} + +// buildApp will call any specifics to the turbine library to prepare a directory that's ready +// to compress, and build, to then later on call the specific command to deploy depending on the language. +func (d *Deploy) buildApp(ctx context.Context) error { + var err error + + d.logger.StartSpinner("\t", "Building application...") + + switch d.lang { + case GoLang: + err = turbineGo.BuildBinary(ctx, d.logger, d.path, d.appName, true) + case JavaScript: + d.tempPath, err = turbineJS.BuildApp(d.path) + case Python: + // TODO: @eric + // path only for zipping = turbinePy.BuildApp(ctx, d.logger, d.path) => newPath string + // Dockerfile will already exist + } + if err != nil { + d.logger.StopSpinner("\t") + return err + } + d.logger.StopSpinner("\tโœ” Application built!") + return nil +} + +// getAppImage will check what type of build needs to perform and ultimately will return the image name to use +// when deploying. +func (d *Deploy) getAppImage(ctx context.Context) (string, error) { + d.logger.StartSpinner("\t", "Checking if application has processes...") var fqImageName string - d.appName = path.Base(appPath) + var needsToBuild bool + var err error - err := turbineGo.BuildBinary(ctx, l, appPath, d.appName, true) + switch d.lang { + case GoLang: + needsToBuild, err = turbineGo.NeedsToBuild(d.path, d.appName) + case JavaScript: + needsToBuild, err = turbineJS.NeedsToBuild(d.path) + case Python: + // TODO: @eric + } if err != nil { + d.logger.StopSpinner("\t") return "", err } - var ok bool - // check for image instances - if ok, err = turbineGo.NeedsToBuild(appPath, d.appName); ok { + // If no need to build, return empty imageName which won't be utilized by the deploy process anyways + if !needsToBuild { + d.logger.StopSpinner("\tโœ” No need to create process image...") + return "", nil + } + + d.logger.StopSpinner("\tโœ” Application processes found. Creating application image...") + + d.localDeploy.TempPath = d.tempPath + d.localDeploy.Lang = d.lang + if d.localDeploy.Enabled { + fqImageName, err = d.localDeploy.GetDockerImageName(ctx, d.logger, d.path, d.appName, d.lang) if err != nil { - l.Errorf(ctx, err.Error()) return "", err } - - if d.goDeploy.LocalDeployment { - fqImageName, err = d.goDeploy.GetDockerImageName(ctx, l, appPath, d.appName) - if err != nil { - return "", err - } - } else { - fqImageName, err = d.getPlatformImage(ctx, appPath) - if err != nil { - return "", err - } + } else { + fqImageName, err = d.getPlatformImage(ctx, d.path) + if err != nil { + return "", err } } - // creates all resources - output, err := turbineGo.RunDeployApp(ctx, l, appPath, d.appName, fqImageName) - if err != nil { - return output, err + return fqImageName, nil +} + +// validateLanguage stops execution of the deployment in case language is not supported. +// It also consolidates lang used in API in case user specified a supported language using an unexpected description. +func (d *Deploy) validateLanguage() error { + switch d.lang { + case "go", GoLang: + d.lang = GoLang + case "js", JavaScript, NodeJs: + d.lang = JavaScript + case "py", Python: + d.lang = Python + default: + return fmt.Errorf("language %q not supported. %s", d.lang, LanguageNotSupportedError) } - return output, nil + return nil } -func (d *Deploy) Execute(ctx context.Context) error { +func (d *Deploy) validate(ctx context.Context) error { // validateLocalDeploymentConfig will look for DockerHub credentials to determine whether it's a local deployment or not. err := d.validateLocalDeploymentConfig() if err != nil { return err } - var deployOutput string d.path, err = turbineCLI.GetPath(d.flags.Path) if err != nil { return err } + d.appName = path.Base(d.path) + d.lang, err = turbineCLI.GetLangFromAppJSON(d.path) if err != nil { return err } + err = d.validateLanguage() + if err != nil { + return err + } + err = turbineCLI.GitChecks(ctx, d.logger, d.path) if err != nil { return err } - err = turbineCLI.ValidateBranch(d.path) + return turbineCLI.ValidateBranch(ctx, d.logger, d.path) +} + +func (d *Deploy) prepareAppForDeployment(ctx context.Context) error { + d.logger.Infof(ctx, "Preparing application %q (%s) for deployment...", d.appName, d.lang) + + // After this point, CLI will package it up and will build it + err := d.buildApp(ctx) if err != nil { return err } - // 1. set up the app structure (CLI does this for any language) - // 2. *depending on the language* call something to create the dockerfile <= - // 3. CLI would handle: - // 3.1 creating the tar.zip, - // 3.2 post /sources - // 3.3 uploading the tar.zip - // 3.4 post /builds - // 4. CLI would call (depending on language) the deploy script <= - switch d.lang { - case GoLang: - deployOutput, err = d.deploy(ctx, d.path, d.logger) - case "js", JavaScript, NodeJs: - deployOutput, err = turbineJS.Deploy(ctx, d.path, d.logger) - default: - return fmt.Errorf("language %q not supported. %s", d.lang, LanguageNotSupportedError) + d.fnName, err = d.getAppImage(ctx) + return err +} + +func (d *Deploy) Execute(ctx context.Context) error { + err := d.validate(ctx) + if err != nil { + return err } + + err = d.prepareAppForDeployment(ctx) + if err != nil { + return err + } + + deployOutput, err := d.deployApp(ctx, d.fnName) if err != nil { return err } @@ -441,6 +554,7 @@ func (d *Deploy) Execute(ctx context.Context) error { if err != nil { return err } + gitSha, err := turbineCLI.GetGitSha(d.path) if err != nil { return err diff --git a/cmd/meroxa/root/apps/deploy_test.go b/cmd/meroxa/root/apps/deploy_test.go index 2fa330e81..8aa93e795 100644 --- a/cmd/meroxa/root/apps/deploy_test.go +++ b/cmd/meroxa/root/apps/deploy_test.go @@ -107,12 +107,12 @@ func TestValidateDockerHubFlags(t *testing.T) { } if err == nil { - if d.goDeploy.DockerHubUserNameEnv != tc.dockerHubUserName { - t.Fatalf("expected DockerHubUserNameEnv to be %q, got %q", tc.dockerHubUserName, d.goDeploy.DockerHubUserNameEnv) + if d.localDeploy.DockerHubUserNameEnv != tc.dockerHubUserName { + t.Fatalf("expected DockerHubUserNameEnv to be %q, got %q", tc.dockerHubUserName, d.localDeploy.DockerHubUserNameEnv) } - if d.goDeploy.DockerHubAccessTokenEnv != tc.dockerHubAccessToken { - t.Fatalf("expected DockerHubAccessTokenEnv to be %q, got %q", tc.dockerHubAccessToken, d.goDeploy.DockerHubAccessTokenEnv) + if d.localDeploy.DockerHubAccessTokenEnv != tc.dockerHubAccessToken { + t.Fatalf("expected DockerHubAccessTokenEnv to be %q, got %q", tc.dockerHubAccessToken, d.localDeploy.DockerHubAccessTokenEnv) } } }) @@ -168,12 +168,12 @@ func TestValidateDockerHubEnVars(t *testing.T) { } if err == nil { - if d.goDeploy.DockerHubUserNameEnv != tc.dockerHubUserName { - t.Fatalf("expected DockerHubUserNameEnv to be %q, got %q", tc.dockerHubUserName, d.goDeploy.DockerHubUserNameEnv) + if d.localDeploy.DockerHubUserNameEnv != tc.dockerHubUserName { + t.Fatalf("expected DockerHubUserNameEnv to be %q, got %q", tc.dockerHubUserName, d.localDeploy.DockerHubUserNameEnv) } - if d.goDeploy.DockerHubAccessTokenEnv != tc.dockerHubAccessToken { - t.Fatalf("expected DockerHubAccessTokenEnv to be %q, got %q", tc.dockerHubAccessToken, d.goDeploy.DockerHubAccessTokenEnv) + if d.localDeploy.DockerHubAccessTokenEnv != tc.dockerHubAccessToken { + t.Fatalf("expected DockerHubAccessTokenEnv to be %q, got %q", tc.dockerHubAccessToken, d.localDeploy.DockerHubAccessTokenEnv) } } }) @@ -214,8 +214,8 @@ func TestValidateLocalDeploymentConfig(t *testing.T) { err := d.validateLocalDeploymentConfig() - if err == nil && d.goDeploy.LocalDeployment != tc.localDeployment { - t.Fatalf("expected localDeployment to be %v, got %v", tc.localDeployment, d.goDeploy.LocalDeployment) + if err == nil && d.localDeploy.Enabled != tc.localDeployment { + t.Fatalf("expected localDeployment to be %v, got %v", tc.localDeployment, d.localDeploy.Enabled) } }) } @@ -276,15 +276,6 @@ func TestCreateApplication(t *testing.T) { t.Fatalf("not expected error, got \"%s\"", err.Error()) } - gotLeveledOutput := logger.LeveledOutput() - wantLeveledOutput := fmt.Sprintf(`Creating application %q with language %q... -Application %q successfully created! -`, name, lang, name) - - if gotLeveledOutput != wantLeveledOutput { - t.Fatalf("expected output:\n%s\ngot:\n%s", wantLeveledOutput, gotLeveledOutput) - } - gotJSONOutput := logger.JSONOutput() var gotApplication meroxa.Application err = json.Unmarshal([]byte(gotJSONOutput), &gotApplication) diff --git a/cmd/meroxa/root/builds/builds.go b/cmd/meroxa/root/builds/builds.go index 22d4a4dc8..72eb7bb43 100644 --- a/cmd/meroxa/root/builds/builds.go +++ b/cmd/meroxa/root/builds/builds.go @@ -34,7 +34,7 @@ var ( type Builds struct{} -func (o *Builds) Usage() string { +func (*Builds) Usage() string { return "builds" } @@ -47,16 +47,16 @@ func (*Builds) Hidden() bool { } func (*Builds) FeatureFlag() (string, error) { - return "turbine", fmt.Errorf("no access to the Meroxa Data Processes feature") + return "turbine", fmt.Errorf("no access to the Meroxa Data Application feature") } -func (o *Builds) Docs() builder.Docs { +func (*Builds) Docs() builder.Docs { return builder.Docs{ - Short: "Manage Process builds on Meroxa", + Short: "Manage Process Builds on Meroxa", } } -func (o *Builds) SubCommands() []*cobra.Command { +func (*Builds) SubCommands() []*cobra.Command { return []*cobra.Command{ builder.BuildCobraCommand(&Describe{}), builder.BuildCobraCommand(&Logs{}), diff --git a/cmd/meroxa/root/builds/logs.go b/cmd/meroxa/root/builds/logs.go index 07b291af0..16cb9abf7 100644 --- a/cmd/meroxa/root/builds/logs.go +++ b/cmd/meroxa/root/builds/logs.go @@ -29,6 +29,7 @@ import ( var ( _ builder.CommandWithDocs = (*Logs)(nil) + _ builder.CommandWithAliases = (*Logs)(nil) _ builder.CommandWithArgs = (*Logs)(nil) _ builder.CommandWithClient = (*Logs)(nil) _ builder.CommandWithLogger = (*Logs)(nil) @@ -48,18 +49,22 @@ type Logs struct { } } -func (d *Logs) Usage() string { +func (l *Logs) Usage() string { return "logs [UUID]" } -func (d *Logs) Docs() builder.Docs { +func (*Logs) Aliases() []string { + return []string{"log"} +} + +func (l *Logs) Docs() builder.Docs { return builder.Docs{ Short: "List a Meroxa Process Build's Logs", } } -func (d *Logs) Execute(ctx context.Context) error { - response, err := d.client.GetBuildLogs(ctx, d.args.UUID) +func (l *Logs) Execute(ctx context.Context) error { + response, err := l.client.GetBuildLogs(ctx, l.args.UUID) if err != nil { return err } @@ -70,24 +75,24 @@ func (d *Logs) Execute(ctx context.Context) error { return err } - d.logger.Info(ctx, string(body)) + l.logger.Info(ctx, string(body)) return nil } -func (d *Logs) Client(client meroxa.Client) { - d.client = client +func (l *Logs) Client(client meroxa.Client) { + l.client = client } -func (d *Logs) Logger(logger log.Logger) { - d.logger = logger +func (l *Logs) Logger(logger log.Logger) { + l.logger = logger } -func (d *Logs) ParseArgs(args []string) error { +func (l *Logs) ParseArgs(args []string) error { if len(args) < 1 { return errors.New("requires build UUID") } - d.args.UUID = args[0] + l.args.UUID = args[0] return nil } diff --git a/cmd/meroxa/root/builds/logs_test.go b/cmd/meroxa/root/builds/logs_test.go index 7136a86f3..d1d73a7ac 100644 --- a/cmd/meroxa/root/builds/logs_test.go +++ b/cmd/meroxa/root/builds/logs_test.go @@ -62,12 +62,12 @@ func TestLogsBuildExecution(t *testing.T) { buildUUID := "236d6e81-6a22-4805-b64f-3fa0a57fdbdc" - c := &Logs{ + l := &Logs{ client: client, logger: logger, } - c.args.UUID = buildUUID + l.args.UUID = buildUUID var responseDetails = io.NopCloser(bytes.NewReader([]byte( `[2021-04-29T12:16:42Z] Beep boop, robots doing build things`, @@ -83,7 +83,7 @@ func TestLogsBuildExecution(t *testing.T) { GetBuildLogs(ctx, buildUUID). Return(httpResponse, nil) - err := c.Execute(ctx) + err := l.Execute(ctx) if err != nil { t.Fatalf("not expected error, got \"%s\"", err.Error()) diff --git a/cmd/meroxa/turbine_cli/golang/deploy.go b/cmd/meroxa/turbine_cli/golang/deploy.go index 6d0381d8a..a151b8cc2 100644 --- a/cmd/meroxa/turbine_cli/golang/deploy.go +++ b/cmd/meroxa/turbine_cli/golang/deploy.go @@ -2,6 +2,7 @@ package turbinego import ( "context" + "errors" "fmt" "os" "os/exec" @@ -11,15 +12,8 @@ import ( "github.com/meroxa/cli/log" ) -type Deploy struct { - DockerHubUserNameEnv string - DockerHubAccessTokenEnv string - LocalDeployment bool -} - // RunDeployApp runs the binary previously built with the `--deploy` flag which should create all necessary resources. func RunDeployApp(ctx context.Context, l log.Logger, appPath, appName, imageName string) (string, error) { - l.Infof(ctx, "Deploying application %q...", appName) var cmd *exec.Cmd if imageName != "" { @@ -37,10 +31,8 @@ func RunDeployApp(ctx context.Context, l log.Logger, appPath, appName, imageName output, err := cmd.CombinedOutput() if err != nil { - l.Errorf(ctx, "%s", string(output)) - return "", fmt.Errorf("deploy failed") + return "", errors.New(string(output)) } - l.Infof(ctx, "%s\ndeploy complete!", string(output)) return string(output), nil } diff --git a/cmd/meroxa/turbine_cli/golang/local_deployment.go b/cmd/meroxa/turbine_cli/golang/local_deployment.go deleted file mode 100644 index 663ea4c3c..000000000 --- a/cmd/meroxa/turbine_cli/golang/local_deployment.go +++ /dev/null @@ -1,134 +0,0 @@ -// Package turbineGo TODO: Reorganize this in a different pkg -package turbinego - -import ( - "context" - "encoding/base64" - "encoding/json" - "io" - "os" - "path/filepath" - "time" - - "github.com/docker/docker/pkg/archive" - turbine "github.com/meroxa/turbine-go/deploy" - - "github.com/docker/docker/api/types" - "github.com/docker/docker/client" - "github.com/meroxa/cli/log" -) - -func (gd *Deploy) getAuthConfig() string { - dhUsername := gd.DockerHubUserNameEnv - dhAccessToken := gd.DockerHubAccessTokenEnv - authConfig := types.AuthConfig{ - Username: dhUsername, - Password: dhAccessToken, - ServerAddress: "https://index.docker.io/v1/", - } - authConfigBytes, _ := json.Marshal(authConfig) - return base64.URLEncoding.EncodeToString(authConfigBytes) -} - -// GetDockerImageName Will create the image via DockerHub. -func (gd *Deploy) GetDockerImageName(ctx context.Context, l log.Logger, appPath, appName string) (string, error) { - // fqImageName will be eventually taken from the build endpoint. - fqImageName := gd.DockerHubUserNameEnv + "/" + appName - - err := gd.buildImage(ctx, l, appPath, fqImageName) - if err != nil { - l.Errorf(ctx, "unable to build image; %q\n%s", fqImageName, err) - return "", err - } - - // this will go away when using the new service. - err = gd.pushImage(l, fqImageName) - if err != nil { - l.Errorf(ctx, "unable to push image; %q\n%s", fqImageName, err) - return "", err - } - - return fqImageName, nil -} - -func (*Deploy) buildImage(ctx context.Context, l log.Logger, pwd, imageName string) error { - l.Infof(ctx, "Building image %q located at %q", imageName, pwd) - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - l.Errorf(ctx, "unable to init docker client; %s", err) - } - - // Generate dockerfile - err = turbine.CreateDockerfile(pwd) - if err != nil { - return err - } - - // Read local Dockerfile - tar, err := archive.TarWithOptions(pwd, &archive.TarOptions{ - Compression: archive.Uncompressed, - ExcludePatterns: []string{".git", "fixtures"}, - }) - if err != nil { - l.Errorf(ctx, "unable to create tar; %s", err) - } - - buildOptions := types.ImageBuildOptions{ - Context: tar, - Dockerfile: "Dockerfile", - Remove: true, - Tags: []string{imageName}} - - resp, err := cli.ImageBuild( - ctx, - tar, - buildOptions, - ) - if err != nil { - l.Errorf(ctx, "unable to build docker image; %s", err) - } - defer func(Body io.ReadCloser) { - err = Body.Close() - if err != nil { - l.Errorf(ctx, "unable to close docker build response body; %s", err) - } - }(resp.Body) - _, err = io.Copy(os.Stdout, resp.Body) - if err != nil { - l.Errorf(ctx, "unable to read image build response; %s", err) - } - - // Cleanup - return os.Remove(filepath.Join(pwd, "Dockerfile")) -} - -func (gd *Deploy) pushImage(l log.Logger, imageName string) error { - cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) - if err != nil { - return err - } - authConfig := gd.getAuthConfig() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) // nolint:gomnd - defer cancel() - - l.Infof(ctx, "pushing image %q to container registry", imageName) - opts := types.ImagePushOptions{RegistryAuth: authConfig} - rd, err := cli.ImagePush(ctx, imageName, opts) - if err != nil { - return err - } - defer func(rd io.ReadCloser) { - err = rd.Close() - if err != nil { - l.Error(ctx, err.Error()) - } - }(rd) - - _, err = io.Copy(os.Stdout, rd) - if err != nil { - return err - } - l.Info(ctx, "image successfully pushed to container registry!") - - return nil -} diff --git a/cmd/meroxa/turbine_cli/javascript/deploy.go b/cmd/meroxa/turbine_cli/javascript/deploy.go index d61979480..b795fe2e7 100644 --- a/cmd/meroxa/turbine_cli/javascript/deploy.go +++ b/cmd/meroxa/turbine_cli/javascript/deploy.go @@ -5,16 +5,38 @@ import ( "fmt" "os" "os/exec" + "strconv" + "strings" "github.com/meroxa/cli/cmd/meroxa/global" turbinecli "github.com/meroxa/cli/cmd/meroxa/turbine_cli" "github.com/meroxa/cli/log" ) -// npx turbine whatever path => CLI could carry on with creating the tar.zip, post source, build... -// once that's complete, it's when we'd call `npx turbine deploy path`. -func Deploy(ctx context.Context, path string, l log.Logger) (string, error) { - cmd := exec.Command("npx", "turbine", "deploy", path) +func NeedsToBuild(path string) (bool, error) { + cmd := exec.Command("npx", "turbine", "hasfunctions", path) + output, err := cmd.CombinedOutput() + if err != nil { + err := fmt.Errorf( + "unable to determine if the Meroxa Application at %s has a Process; %s", + path, + string(output)) + return false, err + } + return strconv.ParseBool(strings.TrimSpace(string(output))) +} + +func BuildApp(path string) (string, error) { + cmd := exec.Command("npx", "turbine", "clibuild", path) + output, err := cmd.CombinedOutput() + if err != nil { + return "", fmt.Errorf("unable to build Meroxa Application at %s; %s", path, string(output)) + } + return strings.TrimSpace(string(output)), err +} + +func Deploy(ctx context.Context, path, imageName string, l log.Logger) (string, error) { + cmd := exec.Command("npx", "turbine", "clideploy", imageName, path) accessToken, _, err := global.GetUserToken() if err != nil { @@ -25,7 +47,3 @@ func Deploy(ctx context.Context, path string, l log.Logger) (string, error) { return turbinecli.RunCmdWithErrorDetection(ctx, cmd, l) } - -// 1. we build binary (Go) // we set up the app structure (JS/Python) <- CLI could do this -// 2. we create the docker file (each turbine-lib does this) -// 3. we call the binary passing --platform ("deploying") diff --git a/cmd/meroxa/turbine_cli/local_deployment.go b/cmd/meroxa/turbine_cli/local_deployment.go new file mode 100644 index 000000000..23be02141 --- /dev/null +++ b/cmd/meroxa/turbine_cli/local_deployment.go @@ -0,0 +1,177 @@ +package turbinecli + +import ( + "context" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "os" + "path/filepath" + "regexp" + "strings" + "time" + + "github.com/docker/docker/pkg/archive" + turbine "github.com/meroxa/turbine-go/deploy" + + "github.com/docker/docker/api/types" + "github.com/docker/docker/client" + "github.com/meroxa/cli/log" +) + +type LocalDeploy struct { + DockerHubUserNameEnv string + DockerHubAccessTokenEnv string + Enabled bool + TempPath string + Lang string +} + +func (ld *LocalDeploy) getAuthConfig() string { + dhUsername := ld.DockerHubUserNameEnv + dhAccessToken := ld.DockerHubAccessTokenEnv + authConfig := types.AuthConfig{ + Username: dhUsername, + Password: dhAccessToken, + ServerAddress: "https://index.docker.io/v1/", + } + authConfigBytes, _ := json.Marshal(authConfig) + return base64.URLEncoding.EncodeToString(authConfigBytes) +} + +// GetDockerImageName Will create the image via DockerHub. +func (ld *LocalDeploy) GetDockerImageName(ctx context.Context, l log.Logger, appPath, appName, lang string) (string, error) { + l.Info(ctx, "\t Using DockerHub...") + // fqImageName will be eventually taken from the build endpoint. + fqImageName := ld.DockerHubUserNameEnv + "/" + appName + + err := ld.buildImage(ctx, l, appPath, fqImageName) + if err != nil { + return "", err + } + + // this will go away when using the new service. + err = ld.pushImage(l, fqImageName) + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to push image %q", fqImageName) + return "", err + } + + l.Infof(ctx, "\tโœ” Image built!") + return fqImageName, nil +} + +func (ld *LocalDeploy) buildImage(ctx context.Context, l log.Logger, pwd, imageName string) error { + l.Infof(ctx, "\t Building image %q located at %q", imageName, pwd) + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to init docker client") + return err + } + + // Generate dockerfile + if ld.Lang == "golang" { + err = turbine.CreateDockerfile(pwd) + if err != nil { + return err + } + } + + if ld.Lang == "javascript" || ld.Lang == "python" { + pwd = ld.TempPath + } + // Read local Dockerfile + tar, err := archive.TarWithOptions(pwd, &archive.TarOptions{ + Compression: archive.Uncompressed, + ExcludePatterns: []string{".git", "fixtures"}, + }) + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to create tar") + return err + } + + buildOptions := types.ImageBuildOptions{ + Context: tar, + Dockerfile: "Dockerfile", + Remove: true, + Tags: []string{imageName}} + + resp, err := cli.ImageBuild( + ctx, + tar, + buildOptions, + ) + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to build docker image") + return err + } + defer func(Body io.ReadCloser) { + err = Body.Close() + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to close docker build response body; %s", err) + } + }(resp.Body) + + buf := new(strings.Builder) + _, err = io.Copy(buf, resp.Body) + if err != nil { + l.Errorf(ctx, "\t ๐„‚ Unable to read image build response") + return err + } + dockerBuildOutput := buf.String() + + re := regexp.MustCompile(`{"errorDetail":{"message":"([^"]+)"}`) + matches := re.FindAllStringSubmatch(dockerBuildOutput, -1) + if len(matches) != 0 { + const subMatchArraySize = 2 + errMsg := "" + for _, str := range matches { + if len(str) < subMatchArraySize { + continue + } + errMsg += "\n" + str[1] + } + err = fmt.Errorf("%s", errMsg) + l.Errorf(ctx, "\t ๐„‚ Unable to build docker image") + return err + } + l.Info(ctx, dockerBuildOutput) + + if ld.Lang == "golang" { + // Cleanup + return os.Remove(filepath.Join(pwd, "Dockerfile")) + } + return nil +} + +func (ld *LocalDeploy) pushImage(l log.Logger, imageName string) error { + cli, err := client.NewClientWithOpts(client.FromEnv, client.WithAPIVersionNegotiation()) + if err != nil { + return err + } + authConfig := ld.getAuthConfig() + ctx, cancel := context.WithTimeout(context.Background(), time.Second*120) // nolint:gomnd + defer cancel() + + l.Infof(ctx, "pushing image %q to container registry", imageName) + opts := types.ImagePushOptions{RegistryAuth: authConfig} + rd, err := cli.ImagePush(ctx, imageName, opts) + if err != nil { + return err + } + defer func(rd io.ReadCloser) { + err = rd.Close() + if err != nil { + l.Error(ctx, err.Error()) + } + }(rd) + + _, err = io.Copy(os.Stdout, rd) + if err != nil { + return err + } + l.Info(ctx, "image successfully pushed to container registry!") + + return nil +} diff --git a/cmd/meroxa/turbine_cli/python/deploy.go b/cmd/meroxa/turbine_cli/python/deploy.go new file mode 100644 index 000000000..40abb68a4 --- /dev/null +++ b/cmd/meroxa/turbine_cli/python/deploy.go @@ -0,0 +1,9 @@ +package turbinepy + +// TODO: Add a function that creates the needed structure for a python app + +// TODO: Add a function to return whether the app has functions or not + +// TODO: Add a function that actually creates the meroxa resources... + +// TODO: Have a script to cleanup the temp directory used (right after source is uploaded) diff --git a/cmd/meroxa/turbine_cli/utils.go b/cmd/meroxa/turbine_cli/utils.go index 8e9a8eb60..1376e924c 100644 --- a/cmd/meroxa/turbine_cli/utils.go +++ b/cmd/meroxa/turbine_cli/utils.go @@ -147,6 +147,7 @@ func writeConfigFile(appPath string, cfg AppConfig) error { // GitChecks prints warnings about uncommitted tracked and untracked files. func GitChecks(ctx context.Context, l log.Logger, appPath string) error { + l.Info(ctx, "Checking for uncommitted changes...") // temporarily switching to the app's directory pwd, err := switchToAppDirectory(appPath) if err != nil { @@ -173,6 +174,7 @@ func GitChecks(ctx context.Context, l log.Logger, appPath string) error { } return fmt.Errorf("unable to proceed with deployment because of uncommitted changes") } + l.Info(ctx, "\tโœ” No uncommitted changes!") return os.Chdir(pwd) } @@ -189,7 +191,8 @@ func GetPipelineUUID(output string) (string, error) { } // ValidateBranch validates the deployment is being performed from one of the allowed branches. -func ValidateBranch(appPath string) error { +func ValidateBranch(ctx context.Context, l log.Logger, appPath string) error { + l.Info(ctx, "Validating branch...") // temporarily switching to the app's directory pwd, err := switchToAppDirectory(appPath) if err != nil { @@ -205,7 +208,7 @@ func ValidateBranch(appPath string) error { if branchName != "main" && branchName != "master" { return fmt.Errorf("deployment allowed only from 'main' or 'master' branch, not %s", branchName) } - + l.Infof(ctx, "\tโœ” Deployment allowed from %s branch!", branchName) err = os.Chdir(pwd) if err != nil { return err @@ -333,7 +336,9 @@ func RunCmdWithErrorDetection(ctx context.Context, cmd *exec.Cmd, l log.Logger) } return "", errors.New(errLog) } - l.Info(ctx, stdOutMsg) + if stdOutMsg != "" { + l.Info(ctx, stdOutMsg) + } return stdOutMsg, nil } @@ -343,7 +348,7 @@ func CreateTarAndZipFile(src string, buf io.Writer) error { appDir := filepath.Base(src) // Change to parent's app directory - pwd, err := switchToAppDirectory(filepath.Dir(appDir)) + pwd, err := switchToAppDirectory(filepath.Dir(src)) if err != nil { return err } @@ -364,6 +369,9 @@ func CreateTarAndZipFile(src string, buf io.Writer) error { if err := tarWriter.WriteHeader(header); err != nil { //nolint:govet return err } + if !fi.Mode().IsRegular() { + return nil + } if !fi.IsDir() { var data *os.File data, err = os.Open(file) diff --git a/go.mod b/go.mod index 141f54966..7af3f507b 100644 --- a/go.mod +++ b/go.mod @@ -5,13 +5,13 @@ go 1.17 require ( github.com/alexeyco/simpletable v0.0.0-20200730140406-5bb24159ccfb github.com/cased/cased-go v1.0.4 - github.com/fatih/color v1.9.0 + github.com/fatih/color v1.13.0 github.com/golang/mock v1.6.0 github.com/google/go-cmp v0.5.6 github.com/google/uuid v1.3.0 github.com/gorilla/mux v1.7.3 github.com/manifoldco/promptui v0.8.0 - github.com/mattn/go-colorable v0.1.8 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect github.com/mattn/go-runewidth v0.0.10 // indirect github.com/meroxa/meroxa-go v0.0.0-20220404191043-2fff202066b1 github.com/nirasan/go-oauth-pkce-code-verifier v0.0.0-20170819232839-0fbfe93532da @@ -25,6 +25,7 @@ require ( ) require ( + github.com/briandowns/spinner v1.18.1 github.com/docker/docker v20.10.12+incompatible github.com/mattn/go-shellwords v1.0.12 github.com/meroxa/turbine-go v0.0.0-20220405102922-2d9a299a1e02 diff --git a/go.sum b/go.sum index d36b9490f..8c2f0e918 100644 --- a/go.sum +++ b/go.sum @@ -109,6 +109,8 @@ github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqO github.com/blang/semver v3.1.0+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/blang/semver v3.5.1+incompatible/go.mod h1:kRBLl5iJ+tD4TcOOxsy/0fnwebNt5EWlYSAyrTnjyyk= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/briandowns/spinner v1.18.1 h1:yhQmQtM1zsqFsouh09Bk/jCjd50pC3EOGsh28gLVvwY= +github.com/briandowns/spinner v1.18.1/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= github.com/bshuster-repo/logrus-logstash-hook v0.4.1/go.mod h1:zsTqEiSzDgAa/8GZR7E1qaXrhYNDKBYy5/dWPTIflbk= github.com/buger/jsonparser v0.0.0-20180808090653-f4dd9f5a6b44/go.mod h1:bbYlZJ7hK1yFx9hf58LP0zeX7UjIGs20ufpu3evjr+s= github.com/bugsnag/bugsnag-go v0.0.0-20141110184014-b1d153021fcd/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= @@ -312,8 +314,8 @@ github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go. github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/evanphx/json-patch v4.9.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.9.0 h1:8xPHl4/q1VyqGIPif1F+1V3Y3lSmrq01EabUW3CoW5s= -github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= github.com/frankban/quicktest v1.11.3/go.mod h1:wRf/ReqHper53s+kmmSZizM8NamnL3IM0I9ntUbOk+k= github.com/friendsofgo/errors v0.9.2 h1:X6NYxef4efCBdwI7BgS820zFaN7Cphrmb+Pljdzjtgk= @@ -549,13 +551,13 @@ github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEX github.com/marstr/guid v1.1.0/go.mod h1:74gB1z2wpxxInTG6yaqA7KrtM0NZ+RbrcqDvYHefzho= github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= -github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= -github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= @@ -1073,6 +1075,7 @@ golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210906170528-6f6e22806c34/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211116061358-0a5406a5449c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/log/log.go b/log/log.go index 99f5b90f0..7e2519611 100644 --- a/log/log.go +++ b/log/log.go @@ -3,16 +3,19 @@ package log type Logger interface { LeveledLogger JSONLogger + SpinnerLogger } type logger struct { LeveledLogger JSONLogger + SpinnerLogger } -func New(l1 LeveledLogger, l2 JSONLogger) Logger { +func New(l1 LeveledLogger, l2 JSONLogger, l3 SpinnerLogger) Logger { return logger{ LeveledLogger: l1, JSONLogger: l2, + SpinnerLogger: l3, } } diff --git a/log/spinner.go b/log/spinner.go new file mode 100644 index 000000000..723701eed --- /dev/null +++ b/log/spinner.go @@ -0,0 +1,35 @@ +package log + +import ( + "io" + "log" + "time" + + "github.com/briandowns/spinner" +) + +type SpinnerLogger interface { + StartSpinner(prefix, suffix string) + StopSpinner(msg string) +} + +func NewSpinnerLogger(out io.Writer) SpinnerLogger { + return &spinnerLogger{l: log.New(out, "", 0)} +} + +type spinnerLogger struct { + l *log.Logger + s *spinner.Spinner +} + +func (l *spinnerLogger) StartSpinner(prefix, suffix string) { + l.s = spinner.New(spinner.CharSets[14], 100*time.Millisecond) // nolint:gomnd + l.s.Prefix = prefix + l.s.Suffix = suffix + l.s.Start() +} + +func (l *spinnerLogger) StopSpinner(msg string) { + l.s.Stop() + l.l.Printf(msg) +} diff --git a/log/test.go b/log/test.go index a5b910cae..018584a90 100644 --- a/log/test.go +++ b/log/test.go @@ -5,12 +5,14 @@ import "bytes" func NewTestLogger() *TestLogger { var leveledBuf bytes.Buffer var jsonBuf bytes.Buffer + var spinnerBuf bytes.Buffer return &TestLogger{ leveledBuf: &leveledBuf, jsonBuf: &jsonBuf, Logger: New( NewLeveledLogger(&leveledBuf, Debug), NewJSONLogger(&jsonBuf), + NewSpinnerLogger(&spinnerBuf), ), } } @@ -19,6 +21,7 @@ type TestLogger struct { Logger leveledBuf *bytes.Buffer jsonBuf *bytes.Buffer + spinnerBuf *bytes.Buffer } var _ Logger = (*TestLogger)(nil) @@ -30,3 +33,7 @@ func (l *TestLogger) JSONOutput() string { func (l *TestLogger) LeveledOutput() string { return l.leveledBuf.String() } + +func (l *TestLogger) SpinnerOutput() string { + return l.spinnerBuf.String() +} diff --git a/vendor/github.com/briandowns/spinner/.gitignore b/vendor/github.com/briandowns/spinner/.gitignore new file mode 100644 index 000000000..21ec6b71b --- /dev/null +++ b/vendor/github.com/briandowns/spinner/.gitignore @@ -0,0 +1,29 @@ +# Created by .gitignore support plugin (hsz.mobi) +### Go template +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof + +.idea +*.iml diff --git a/vendor/github.com/briandowns/spinner/.travis.yml b/vendor/github.com/briandowns/spinner/.travis.yml new file mode 100644 index 000000000..74d205aec --- /dev/null +++ b/vendor/github.com/briandowns/spinner/.travis.yml @@ -0,0 +1,18 @@ +arch: + - amd64 + - ppc64le +language: go +go: + - 1.16 + - 1.17.5 +env: + - GOARCH: amd64 + - GOARCH: 386 +script: + - go test -v +notifications: + email: + recipients: + - brian.downs@gmail.com + on_success: change + on_failure: always diff --git a/vendor/github.com/briandowns/spinner/LICENSE b/vendor/github.com/briandowns/spinner/LICENSE new file mode 100644 index 000000000..dd5b3a58a --- /dev/null +++ b/vendor/github.com/briandowns/spinner/LICENSE @@ -0,0 +1,174 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/vendor/github.com/briandowns/spinner/Makefile b/vendor/github.com/briandowns/spinner/Makefile new file mode 100644 index 000000000..3cfdeb23c --- /dev/null +++ b/vendor/github.com/briandowns/spinner/Makefile @@ -0,0 +1,20 @@ +GO = go + +.PHONY: deps +deps: go.mod + +go.mod: + go mod init + go mod tidy + +.PHONY: test +test: + $(GO) test -v -cover ./... + +.PHONY: check +check: + if [ -d vendor ]; then cp -r vendor/* ${GOPATH}/src/; fi + +.PHONY: clean +clean: + $(GO) clean diff --git a/vendor/github.com/briandowns/spinner/NOTICE.txt b/vendor/github.com/briandowns/spinner/NOTICE.txt new file mode 100644 index 000000000..95e2a248b --- /dev/null +++ b/vendor/github.com/briandowns/spinner/NOTICE.txt @@ -0,0 +1,4 @@ +Spinner +Copyright (c) 2022 Brian J. Downs +This product is licensed to you under the Apache 2.0 license (the "License"). You may not use this product except in compliance with the Apache 2.0 License. +This product may include a number of subcomponents with separate copyright notices and license terms. Your use of these subcomponents is subject to the terms and conditions of the subcomponent's license, as noted in the LICENSE file. diff --git a/vendor/github.com/briandowns/spinner/README.md b/vendor/github.com/briandowns/spinner/README.md new file mode 100644 index 000000000..20b315fbe --- /dev/null +++ b/vendor/github.com/briandowns/spinner/README.md @@ -0,0 +1,285 @@ +# Spinner + +[![GoDoc](https://godoc.org/github.com/briandowns/spinner?status.svg)](https://godoc.org/github.com/briandowns/spinner) [![Build Status](https://travis-ci.org/briandowns/spinner.svg?branch=master)](https://travis-ci.org/briandowns/spinner) + +spinner is a simple package to add a spinner / progress indicator to any terminal application. Examples can be found below as well as full examples in the examples directory. + +For more detail about the library and its features, reference your local godoc once installed. + +Contributions welcome! + +## Installation + +```bash +go get github.com/briandowns/spinner +``` + +## Available Character Sets + +90 Character Sets. Some examples below: + +(Numbered by their slice index) + +| index | character set | sample gif | +| ----- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------- | +| 0 | ```โ†โ†–โ†‘โ†—โ†’โ†˜โ†“โ†™``` | ![Sample Gif](gifs/0.gif) | +| 1 | ```โ–โ–ƒโ–„โ–…โ–†โ–‡โ–ˆโ–‡โ–†โ–…โ–„โ–ƒโ–``` | ![Sample Gif](gifs/1.gif) | +| 2 | ```โ––โ–˜โ–โ–—``` | ![Sample Gif](gifs/2.gif) | +| 3 | ```โ”คโ”˜โ”ดโ””โ”œโ”Œโ”ฌโ”``` | ![Sample Gif](gifs/3.gif) | +| 4 | ```โ—ขโ—ฃโ—คโ—ฅ``` | ![Sample Gif](gifs/4.gif) | +| 5 | ```โ—ฐโ—ณโ—ฒโ—ฑ``` | ![Sample Gif](gifs/5.gif) | +| 6 | ```โ—ดโ—ทโ—ถโ—ต``` | ![Sample Gif](gifs/6.gif) | +| 7 | ```โ—โ—“โ—‘โ—’``` | ![Sample Gif](gifs/7.gif) | +| 8 | ```.oO@*``` | ![Sample Gif](gifs/8.gif) | +| 9 | ```\|/-\``` | ![Sample Gif](gifs/9.gif) | +| 10 | ```โ—กโ—กโŠ™โŠ™โ— โ— ``` | ![Sample Gif](gifs/10.gif) | +| 11 | ```โฃพโฃฝโฃปโขฟโกฟโฃŸโฃฏโฃท``` | ![Sample Gif](gifs/11.gif) | +| 12 | ```>))'> >))'> >))'> >))'> >))'> <'((< <'((< <'((<``` | ![Sample Gif](gifs/12.gif) | +| 13 | ```โ โ ‚โ „โก€โข€โ  โ โ ˆ``` | ![Sample Gif](gifs/13.gif) | +| 14 | ```โ ‹โ ™โ นโ ธโ ผโ ดโ ฆโ งโ ‡โ ``` | ![Sample Gif](gifs/14.gif) | +| 15 | ```abcdefghijklmnopqrstuvwxyz``` | ![Sample Gif](gifs/15.gif) | +| 16 | ```โ–‰โ–Šโ–‹โ–Œโ–โ–Žโ–โ–Žโ–โ–Œโ–‹โ–Šโ–‰``` | ![Sample Gif](gifs/16.gif) | +| 17 | ```โ– โ–กโ–ชโ–ซ``` | ![Sample Gif](gifs/17.gif) | +| 18 | ```โ†โ†‘โ†’โ†“``` | ![Sample Gif](gifs/18.gif) | +| 19 | ```โ•ซโ•ช``` | ![Sample Gif](gifs/19.gif) | +| 20 | ```โ‡โ‡–โ‡‘โ‡—โ‡’โ‡˜โ‡“โ‡™``` | ![Sample Gif](gifs/20.gif) | +| 21 | ```โ โ โ ‰โ ™โ šโ ’โ ‚โ ‚โ ’โ ฒโ ดโ คโ „โ „โ คโ  โ  โ คโ ฆโ –โ ’โ โ โ ’โ “โ ‹โ ‰โ ˆโ ˆ``` | ![Sample Gif](gifs/21.gif) | +| 22 | ```โ ˆโ ‰โ ‹โ “โ ’โ โ โ ’โ –โ ฆโ คโ  โ  โ คโ ฆโ –โ ’โ โ โ ’โ “โ ‹โ ‰โ ˆ``` | ![Sample Gif](gifs/22.gif) | +| 23 | ```โ โ ‰โ ™โ šโ ’โ ‚โ ‚โ ’โ ฒโ ดโ คโ „โ „โ คโ ดโ ฒโ ’โ ‚โ ‚โ ’โ šโ ™โ ‰โ ``` | ![Sample Gif](gifs/23.gif) | +| 24 | ```โ ‹โ ™โ šโ ’โ ‚โ ‚โ ’โ ฒโ ดโ ฆโ –โ ’โ โ โ ’โ “โ ‹``` | ![Sample Gif](gifs/24.gif) | +| 25 | ```๏ฝฆ๏ฝง๏ฝจ๏ฝฉ๏ฝช๏ฝซ๏ฝฌ๏ฝญ๏ฝฎ๏ฝฏ๏ฝฑ๏ฝฒ๏ฝณ๏ฝด๏ฝต๏ฝถ๏ฝท๏ฝธ๏ฝน๏ฝบ๏ฝป๏ฝผ๏ฝฝ๏ฝพ๏ฝฟ๏พ€๏พ๏พ‚๏พƒ๏พ„๏พ…๏พ†๏พ‡๏พˆ๏พ‰๏พŠ๏พ‹๏พŒ๏พ๏พŽ๏พ๏พ๏พ‘๏พ’๏พ“๏พ”๏พ•๏พ–๏พ—๏พ˜๏พ™๏พš๏พ›๏พœ๏พ``` | ![Sample Gif](gifs/25.gif) | +| 26 | ```. .. ...``` | ![Sample Gif](gifs/26.gif) | +| 27 | ```โ–โ–‚โ–ƒโ–„โ–…โ–†โ–‡โ–ˆโ–‰โ–Šโ–‹โ–Œโ–โ–Žโ–โ–โ–Žโ–โ–Œโ–‹โ–Šโ–‰โ–ˆโ–‡โ–†โ–…โ–„โ–ƒโ–‚โ–``` | ![Sample Gif](gifs/27.gif) | +| 28 | ```.oOยฐOo.``` | ![Sample Gif](gifs/28.gif) | +| 29 | ```+x``` | ![Sample Gif](gifs/29.gif) | +| 30 | ```v<^>``` | ![Sample Gif](gifs/30.gif) | +| 31 | ```>>---> >>---> >>---> >>---> >>---> <---<< <---<< <---<< <---<< <---<<``` | ![Sample Gif](gifs/31.gif) | +| 32 | ```\| \|\| \|\|\| \|\|\|\| \|\|\|\|\| \|\|\|\|\|\| \|\|\|\|\| \|\|\|\| \|\|\| \|\| \|``` | ![Sample Gif](gifs/32.gif) | +| 33 | ```[] [=] [==] [===] [====] [=====] [======] [=======] [========] [=========] [==========]``` | ![Sample Gif](gifs/33.gif) | +| 34 | ```(*---------) (-*--------) (--*-------) (---*------) (----*-----) (-----*----) (------*---) (-------*--) (--------*-) (---------*)``` | ![Sample Gif](gifs/34.gif) | +| 35 | ```โ–ˆโ–’โ–’โ–’โ–’โ–’โ–’โ–’โ–’โ–’ โ–ˆโ–ˆโ–ˆโ–’โ–’โ–’โ–’โ–’โ–’โ–’ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–’โ–’โ–’โ–’โ–’ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–’โ–’โ–’ โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ``` | ![Sample Gif](gifs/35.gif) | +| 36 | ```[ ] [=> ] [===> ] [=====> ] [======> ] [========> ] [==========> ] [============> ] [==============> ] [================> ] [==================> ] [===================>]``` | ![Sample Gif](gifs/36.gif) | +| 37 | ```๐Ÿ• ๐Ÿ•‘ ๐Ÿ•’ ๐Ÿ•“ ๐Ÿ•” ๐Ÿ•• ๐Ÿ•– ๐Ÿ•— ๐Ÿ•˜ ๐Ÿ•™ ๐Ÿ•š ๐Ÿ•›``` | ![Sample Gif](gifs/37.gif) | +| 38 | ```๐Ÿ• ๐Ÿ•œ ๐Ÿ•‘ ๐Ÿ• ๐Ÿ•’ ๐Ÿ•ž ๐Ÿ•“ ๐Ÿ•Ÿ ๐Ÿ•” ๐Ÿ•  ๐Ÿ•• ๐Ÿ•ก ๐Ÿ•– ๐Ÿ•ข ๐Ÿ•— ๐Ÿ•ฃ ๐Ÿ•˜ ๐Ÿ•ค ๐Ÿ•™ ๐Ÿ•ฅ ๐Ÿ•š ๐Ÿ•ฆ ๐Ÿ•› ๐Ÿ•ง``` | ![Sample Gif](gifs/38.gif) | +| 39 | ```๐ŸŒ ๐ŸŒŽ ๐ŸŒ``` | ![Sample Gif](gifs/39.gif) | +| 40 | ```โ—œ โ— โ—ž โ—Ÿ``` | ![Sample Gif](gifs/40.gif) | +| 41 | ```โฌ’ โฌ” โฌ“ โฌ•``` | ![Sample Gif](gifs/41.gif) | +| 42 | ```โฌ– โฌ˜ โฌ— โฌ™``` | ![Sample Gif](gifs/42.gif) | +| 43 | ```[>>> >] []>>>> [] [] >>>> [] [] >>>> [] [] >>>> [] [] >>>>[] [>> >>]``` | ![Sample Gif](gifs/43.gif) | + +## Features + +* Start +* Stop +* Restart +* Reverse direction +* Update the spinner character set +* Update the spinner speed +* Prefix or append text +* Change spinner color, background, and text attributes such as bold / italics +* Get spinner status +* Chain, pipe, redirect output +* Output final string on spinner/indicator completion + +## Examples + +```Go +package main + +import ( + "github.com/briandowns/spinner" + "time" +) + +func main() { + s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) // Build our new spinner + s.Start() // Start the spinner + time.Sleep(4 * time.Second) // Run for some time to simulate work + s.Stop() +} +``` + +## Update the character set and restart the spinner + +```Go +s.UpdateCharSet(spinner.CharSets[1]) // Update spinner to use a different character set +s.Restart() // Restart the spinner +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Update spin speed and restart the spinner + +```Go +s.UpdateSpeed(200 * time.Millisecond) // Update the speed the spinner spins at +s.Restart() +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Reverse the direction of the spinner + +```Go +s.Reverse() // Reverse the direction the spinner is spinning +s.Restart() +time.Sleep(4 * time.Second) +s.Stop() +``` + +## Provide your own spinner + +(or send me an issue or pull request to add to the project) + +```Go +someSet := []string{"+", "-"} +s := spinner.New(someSet, 100*time.Millisecond) +``` + +## Prefix or append text to the spinner + +```Go +s.Prefix = "prefixed text: " // Prefix text before the spinner +s.Suffix = " :appended text" // Append text after the spinner +``` + +## Set or change the color of the spinner. Default color is white. The spinner will need to be restarted to pick up the change. + +```Go +s.Color("red") // Set the spinner color to red +``` + +You can specify both the background and foreground color, as well as additional attributes such as `bold` or `underline`. + +```Go +s.Color("red", "bold") // Set the spinner color to a bold red +``` + +To set the background to black, the foreground to a bold red: + +```Go +s.Color("bgBlack", "bold", "fgRed") +``` + +Below is the full color and attribute list: + +```Go +// default colors +red +black +green +yellow +blue +magenta +cyan +white + +// attributes +reset +bold +faint +italic +underline +blinkslow +blinkrapid +reversevideo +concealed +crossedout + +// foreground text +fgBlack +fgRed +fgGreen +fgYellow +fgBlue +fgMagenta +fgCyan +fgWhite + +// foreground Hi-Intensity text +fgHiBlack +fgHiRed +fgHiGreen +fgHiYellow +fgHiBlue +fgHiMagenta +fgHiCyan +fgHiWhite + +// background text +bgBlack +bgRed +bgGreen +bgYellow +bgBlue +bgMagenta +bgCyan +bgWhite + +// background Hi-Intensity text +bgHiBlack +bgHiRed +bgHiGreen +bgHiYellow +bgHiBlue +bgHiMagenta +bgHiCyan +bgHiWhite +``` + +## Generate a sequence of numbers + +```Go +setOfDigits := spinner.GenerateNumberSequence(25) // Generate a 25 digit string of numbers +s := spinner.New(setOfDigits, 100*time.Millisecond) +``` + +## Get spinner status + +```Go +fmt.Println(s.Active()) +``` + +## Unix pipe and redirect + +Feature suggested and write up by [dekz](https://github.com/dekz) + +Setting the Spinner Writer to Stderr helps show progress to the user, with the enhancement to chain, pipe or redirect the output. + +This is the preferred method of setting a Writer at this time. + +```go +s := spinner.New(spinner.CharSets[11], 100*time.Millisecond, spinner.WithWriter(os.Stderr)) +s.Suffix = " Encrypting data..." +s.Start() +// Encrypt the data into ciphertext +fmt.Println(os.Stdout, ciphertext) +``` + +```sh +> myprog encrypt "Secret text" > encrypted.txt +โฃฏ Encrypting data... +``` + +```sh +> cat encrypted.txt +1243hjkbas23i9ah27sj39jghv237n2oa93hg83 +``` + +## Final String Output + +Add additional output when the spinner/indicator has completed. The "final" output string can be multi-lined and will be written to wherever the `io.Writer` has been configured for. + +```Go +s := spinner.New(spinner.CharSets[9], 100*time.Millisecond) +s.FinalMSG = "Complete!\nNew line!\nAnother one!\n" +s.Start() +time.Sleep(4 * time.Second) +s.Stop() +``` + +Output +```sh +Complete! +New line! +Another one! +``` diff --git a/vendor/github.com/briandowns/spinner/character_sets.go b/vendor/github.com/briandowns/spinner/character_sets.go new file mode 100644 index 000000000..df41a0f2c --- /dev/null +++ b/vendor/github.com/briandowns/spinner/character_sets.go @@ -0,0 +1,121 @@ +// Copyright (c) 2022 Brian J. Downs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package spinner + +const ( + clockOneOClock = '\U0001F550' + clockOneThirty = '\U0001F55C' +) + +// CharSets contains the available character sets +var CharSets = map[int][]string{ + 0: {"โ†", "โ†–", "โ†‘", "โ†—", "โ†’", "โ†˜", "โ†“", "โ†™"}, + 1: {"โ–", "โ–ƒ", "โ–„", "โ–…", "โ–†", "โ–‡", "โ–ˆ", "โ–‡", "โ–†", "โ–…", "โ–„", "โ–ƒ", "โ–"}, + 2: {"โ––", "โ–˜", "โ–", "โ–—"}, + 3: {"โ”ค", "โ”˜", "โ”ด", "โ””", "โ”œ", "โ”Œ", "โ”ฌ", "โ”"}, + 4: {"โ—ข", "โ—ฃ", "โ—ค", "โ—ฅ"}, + 5: {"โ—ฐ", "โ—ณ", "โ—ฒ", "โ—ฑ"}, + 6: {"โ—ด", "โ—ท", "โ—ถ", "โ—ต"}, + 7: {"โ—", "โ—“", "โ—‘", "โ—’"}, + 8: {".", "o", "O", "@", "*"}, + 9: {"|", "/", "-", "\\"}, + 10: {"โ—กโ—ก", "โŠ™โŠ™", "โ— โ— "}, + 11: {"โฃพ", "โฃฝ", "โฃป", "โขฟ", "โกฟ", "โฃŸ", "โฃฏ", "โฃท"}, + 12: {">))'>", " >))'>", " >))'>", " >))'>", " >))'>", " <'((<", " <'((<", " <'((<"}, + 13: {"โ ", "โ ‚", "โ „", "โก€", "โข€", "โ  ", "โ ", "โ ˆ"}, + 14: {"โ ‹", "โ ™", "โ น", "โ ธ", "โ ผ", "โ ด", "โ ฆ", "โ ง", "โ ‡", "โ "}, + 15: {"a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z"}, + 16: {"โ–‰", "โ–Š", "โ–‹", "โ–Œ", "โ–", "โ–Ž", "โ–", "โ–Ž", "โ–", "โ–Œ", "โ–‹", "โ–Š", "โ–‰"}, + 17: {"โ– ", "โ–ก", "โ–ช", "โ–ซ"}, + + 18: {"โ†", "โ†‘", "โ†’", "โ†“"}, + 19: {"โ•ซ", "โ•ช"}, + 20: {"โ‡", "โ‡–", "โ‡‘", "โ‡—", "โ‡’", "โ‡˜", "โ‡“", "โ‡™"}, + 21: {"โ ", "โ ", "โ ‰", "โ ™", "โ š", "โ ’", "โ ‚", "โ ‚", "โ ’", "โ ฒ", "โ ด", "โ ค", "โ „", "โ „", "โ ค", "โ  ", "โ  ", "โ ค", "โ ฆ", "โ –", "โ ’", "โ ", "โ ", "โ ’", "โ “", "โ ‹", "โ ‰", "โ ˆ", "โ ˆ"}, + 22: {"โ ˆ", "โ ‰", "โ ‹", "โ “", "โ ’", "โ ", "โ ", "โ ’", "โ –", "โ ฆ", "โ ค", "โ  ", "โ  ", "โ ค", "โ ฆ", "โ –", "โ ’", "โ ", "โ ", "โ ’", "โ “", "โ ‹", "โ ‰", "โ ˆ"}, + 23: {"โ ", "โ ‰", "โ ™", "โ š", "โ ’", "โ ‚", "โ ‚", "โ ’", "โ ฒ", "โ ด", "โ ค", "โ „", "โ „", "โ ค", "โ ด", "โ ฒ", "โ ’", "โ ‚", "โ ‚", "โ ’", "โ š", "โ ™", "โ ‰", "โ "}, + 24: {"โ ‹", "โ ™", "โ š", "โ ’", "โ ‚", "โ ‚", "โ ’", "โ ฒ", "โ ด", "โ ฆ", "โ –", "โ ’", "โ ", "โ ", "โ ’", "โ “", "โ ‹"}, + 25: {"๏ฝฆ", "๏ฝง", "๏ฝจ", "๏ฝฉ", "๏ฝช", "๏ฝซ", "๏ฝฌ", "๏ฝญ", "๏ฝฎ", "๏ฝฏ", "๏ฝฑ", "๏ฝฒ", "๏ฝณ", "๏ฝด", "๏ฝต", "๏ฝถ", "๏ฝท", "๏ฝธ", "๏ฝน", "๏ฝบ", "๏ฝป", "๏ฝผ", "๏ฝฝ", "๏ฝพ", "๏ฝฟ", "๏พ€", "๏พ", "๏พ‚", "๏พƒ", "๏พ„", "๏พ…", "๏พ†", "๏พ‡", "๏พˆ", "๏พ‰", "๏พŠ", "๏พ‹", "๏พŒ", "๏พ", "๏พŽ", "๏พ", "๏พ", "๏พ‘", "๏พ’", "๏พ“", "๏พ”", "๏พ•", "๏พ–", "๏พ—", "๏พ˜", "๏พ™", "๏พš", "๏พ›", "๏พœ", "๏พ"}, + 26: {".", "..", "..."}, + 27: {"โ–", "โ–‚", "โ–ƒ", "โ–„", "โ–…", "โ–†", "โ–‡", "โ–ˆ", "โ–‰", "โ–Š", "โ–‹", "โ–Œ", "โ–", "โ–Ž", "โ–", "โ–", "โ–Ž", "โ–", "โ–Œ", "โ–‹", "โ–Š", "โ–‰", "โ–ˆ", "โ–‡", "โ–†", "โ–…", "โ–„", "โ–ƒ", "โ–‚", "โ–"}, + 28: {".", "o", "O", "ยฐ", "O", "o", "."}, + 29: {"+", "x"}, + 30: {"v", "<", "^", ">"}, + 31: {">>--->", " >>--->", " >>--->", " >>--->", " >>--->", " <---<<", " <---<<", " <---<<", " <---<<", "<---<<"}, + 32: {"|", "||", "|||", "||||", "|||||", "|||||||", "||||||||", "|||||||", "||||||", "|||||", "||||", "|||", "||", "|"}, + 33: {"[ ]", "[= ]", "[== ]", "[=== ]", "[==== ]", "[===== ]", "[====== ]", "[======= ]", "[======== ]", "[========= ]", "[==========]"}, + 34: {"(*---------)", "(-*--------)", "(--*-------)", "(---*------)", "(----*-----)", "(-----*----)", "(------*---)", "(-------*--)", "(--------*-)", "(---------*)"}, + 35: {"โ–ˆโ–’โ–’โ–’โ–’โ–’โ–’โ–’โ–’โ–’", "โ–ˆโ–ˆโ–ˆโ–’โ–’โ–’โ–’โ–’โ–’โ–’", "โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–’โ–’โ–’โ–’โ–’", "โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–’โ–’โ–’", "โ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆโ–ˆ"}, + 36: {"[ ]", "[=> ]", "[===> ]", "[=====> ]", "[======> ]", "[========> ]", "[==========> ]", "[============> ]", "[==============> ]", "[================> ]", "[==================> ]", "[===================>]"}, + 39: {"๐ŸŒ", "๐ŸŒŽ", "๐ŸŒ"}, + 40: {"โ—œ", "โ—", "โ—ž", "โ—Ÿ"}, + 41: {"โฌ’", "โฌ”", "โฌ“", "โฌ•"}, + 42: {"โฌ–", "โฌ˜", "โฌ—", "โฌ™"}, + 43: {"[>>> >]", "[]>>>> []", "[] >>>> []", "[] >>>> []", "[] >>>> []", "[] >>>>[]", "[>> >>]"}, + 44: {"โ™ ", "โ™ฃ", "โ™ฅ", "โ™ฆ"}, + 45: {"โžž", "โžŸ", "โž ", "โžก", "โž ", "โžŸ"}, + 46: {" | ", ` \ `, "_ ", ` \ `, " | ", " / ", " _", " / "}, + 47: {" . . . .", ". . . .", ". . . .", ". . . .", ". . . . ", ". . . . ."}, + 48: {" | ", " / ", " _ ", ` \ `, " | ", ` \ `, " _ ", " / "}, + 49: {"โŽบ", "โŽป", "โŽผ", "โŽฝ", "โŽผ", "โŽป"}, + 50: {"โ–นโ–นโ–นโ–นโ–น", "โ–ธโ–นโ–นโ–นโ–น", "โ–นโ–ธโ–นโ–นโ–น", "โ–นโ–นโ–ธโ–นโ–น", "โ–นโ–นโ–นโ–ธโ–น", "โ–นโ–นโ–นโ–นโ–ธ"}, + 51: {"[ ]", "[ =]", "[ ==]", "[ ===]", "[====]", "[=== ]", "[== ]", "[= ]"}, + 52: {"( โ— )", "( โ— )", "( โ— )", "( โ— )", "( โ—)", "( โ— )", "( โ— )", "( โ— )", "( โ— )"}, + 53: {"โœถ", "โœธ", "โœน", "โœบ", "โœน", "โœท"}, + 54: {"โ–|\\____________โ–Œ", "โ–_|\\___________โ–Œ", "โ–__|\\__________โ–Œ", "โ–___|\\_________โ–Œ", "โ–____|\\________โ–Œ", "โ–_____|\\_______โ–Œ", "โ–______|\\______โ–Œ", "โ–_______|\\_____โ–Œ", "โ–________|\\____โ–Œ", "โ–_________|\\___โ–Œ", "โ–__________|\\__โ–Œ", "โ–___________|\\_โ–Œ", "โ–____________|\\โ–Œ", "โ–____________/|โ–Œ", "โ–___________/|_โ–Œ", "โ–__________/|__โ–Œ", "โ–_________/|___โ–Œ", "โ–________/|____โ–Œ", "โ–_______/|_____โ–Œ", "โ–______/|______โ–Œ", "โ–_____/|_______โ–Œ", "โ–____/|________โ–Œ", "โ–___/|_________โ–Œ", "โ–__/|__________โ–Œ", "โ–_/|___________โ–Œ", "โ–/|____________โ–Œ"}, + 55: {"โ–โ ‚ โ–Œ", "โ–โ ˆ โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ   โ–Œ", "โ– โก€ โ–Œ", "โ– โ   โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ ˆ โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ   โ–Œ", "โ– โก€ โ–Œ", "โ– โ   โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ ˆ โ–Œ", "โ– โ ‚โ–Œ", "โ– โ  โ–Œ", "โ– โก€โ–Œ", "โ– โ   โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ ˆ โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ   โ–Œ", "โ– โก€ โ–Œ", "โ– โ   โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ ˆ โ–Œ", "โ– โ ‚ โ–Œ", "โ– โ   โ–Œ", "โ– โก€ โ–Œ", "โ–โ   โ–Œ"}, + 56: {"ยฟ", "?"}, + 57: {"โขน", "โขบ", "โขผ", "โฃธ", "โฃ‡", "โกง", "โก—", "โก"}, + 58: {"โข„", "โข‚", "โข", "โก", "โกˆ", "โก", "โก "}, + 59: {". ", ".. ", "...", " ..", " .", " "}, + 60: {".", "o", "O", "ยฐ", "O", "o", "."}, + 61: {"โ–“", "โ–’", "โ–‘"}, + 62: {"โ–Œ", "โ–€", "โ–", "โ–„"}, + 63: {"โŠถ", "โŠท"}, + 64: {"โ–ช", "โ–ซ"}, + 65: {"โ–ก", "โ– "}, + 66: {"โ–ฎ", "โ–ฏ"}, + 67: {"-", "=", "โ‰ก"}, + 68: {"d", "q", "p", "b"}, + 69: {"โˆ™โˆ™โˆ™", "โ—โˆ™โˆ™", "โˆ™โ—โˆ™", "โˆ™โˆ™โ—", "โˆ™โˆ™โˆ™"}, + 70: {"๐ŸŒ‘ ", "๐ŸŒ’ ", "๐ŸŒ“ ", "๐ŸŒ” ", "๐ŸŒ• ", "๐ŸŒ– ", "๐ŸŒ— ", "๐ŸŒ˜ "}, + 71: {"โ˜—", "โ˜–"}, + 72: {"โง‡", "โง†"}, + 73: {"โ—‰", "โ—Ž"}, + 74: {"ใŠ‚", "ใŠ€", "ใŠ"}, + 75: {"โฆพ", "โฆฟ"}, + 76: {"แ€", "แ€"}, + 77: {"โ–Œ", "โ–€", "โ–โ–„"}, + 78: {"โ ˆโ ", "โ ˆโ ‘", "โ ˆโ ฑ", "โ ˆโกฑ", "โข€โกฑ", "โข„โกฑ", "โข„โกฑ", "โข†โกฑ", "โขŽโกฑ", "โขŽโกฐ", "โขŽโก ", "โขŽโก€", "โขŽโ ", "โ Žโ ", "โ Šโ "}, + 79: {"________", "-_______", "_-______", "__-_____", "___-____", "____-___", "_____-__", "______-_", "_______-", "________", "_______-", "______-_", "_____-__", "____-___", "___-____", "__-_____", "_-______", "-_______", "________"}, + 80: {"|_______", "_/______", "__-_____", "___\\____", "____|___", "_____/__", "______-_", "_______\\", "_______|", "______\\_", "_____-__", "____/___", "___|____", "__\\_____", "_-______"}, + 81: {"โ–ก", "โ—ฑ", "โ—ง", "โ–ฃ", "โ– "}, + 82: {"โ–ก", "โ—ฑ", "โ–จ", "โ–ฉ", "โ– "}, + 83: {"โ–‘", "โ–’", "โ–“", "โ–ˆ"}, + 84: {"โ–‘", "โ–ˆ"}, + 85: {"โšช", "โšซ"}, + 86: {"โ—ฏ", "โฌค"}, + 87: {"โ–ฑ", "โ–ฐ"}, + 88: {"โžŠ", "โž‹", "โžŒ", "โž", "โžŽ", "โž", "โž", "โž‘", "โž’", "โž“"}, + 89: {"ยฝ", "โ…“", "โ…”", "ยผ", "ยพ", "โ…›", "โ…œ", "โ…", "โ…ž"}, + 90: {"โ†ž", "โ†Ÿ", "โ† ", "โ†ก"}, +} + +func init() { + for i := rune(0); i < 12; i++ { + CharSets[37] = append(CharSets[37], string([]rune{clockOneOClock + i})) + CharSets[38] = append(CharSets[38], string([]rune{clockOneOClock + i}), string([]rune{clockOneThirty + i})) + } +} diff --git a/vendor/github.com/briandowns/spinner/spinner.go b/vendor/github.com/briandowns/spinner/spinner.go new file mode 100644 index 000000000..f6bb029f8 --- /dev/null +++ b/vendor/github.com/briandowns/spinner/spinner.go @@ -0,0 +1,447 @@ +// Copyright (c) 2021 Brian J. Downs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package spinner is a simple package to add a spinner / progress indicator to any terminal application. +package spinner + +import ( + "errors" + "fmt" + "io" + "os" + "runtime" + "strconv" + "strings" + "sync" + "time" + "unicode/utf8" + + "github.com/fatih/color" + "github.com/mattn/go-isatty" +) + +// errInvalidColor is returned when attempting to set an invalid color +var errInvalidColor = errors.New("invalid color") + +// validColors holds an array of the only colors allowed +var validColors = map[string]bool{ + // default colors for backwards compatibility + "black": true, + "red": true, + "green": true, + "yellow": true, + "blue": true, + "magenta": true, + "cyan": true, + "white": true, + + // attributes + "reset": true, + "bold": true, + "faint": true, + "italic": true, + "underline": true, + "blinkslow": true, + "blinkrapid": true, + "reversevideo": true, + "concealed": true, + "crossedout": true, + + // foreground text + "fgBlack": true, + "fgRed": true, + "fgGreen": true, + "fgYellow": true, + "fgBlue": true, + "fgMagenta": true, + "fgCyan": true, + "fgWhite": true, + + // foreground Hi-Intensity text + "fgHiBlack": true, + "fgHiRed": true, + "fgHiGreen": true, + "fgHiYellow": true, + "fgHiBlue": true, + "fgHiMagenta": true, + "fgHiCyan": true, + "fgHiWhite": true, + + // background text + "bgBlack": true, + "bgRed": true, + "bgGreen": true, + "bgYellow": true, + "bgBlue": true, + "bgMagenta": true, + "bgCyan": true, + "bgWhite": true, + + // background Hi-Intensity text + "bgHiBlack": true, + "bgHiRed": true, + "bgHiGreen": true, + "bgHiYellow": true, + "bgHiBlue": true, + "bgHiMagenta": true, + "bgHiCyan": true, + "bgHiWhite": true, +} + +// returns true if the OS is windows and the WT_SESSION env variable is set. +var isWindowsTerminalOnWindows = len(os.Getenv("WT_SESSION")) > 0 && runtime.GOOS == "windows" + +// returns a valid color's foreground text color attribute +var colorAttributeMap = map[string]color.Attribute{ + // default colors for backwards compatibility + "black": color.FgBlack, + "red": color.FgRed, + "green": color.FgGreen, + "yellow": color.FgYellow, + "blue": color.FgBlue, + "magenta": color.FgMagenta, + "cyan": color.FgCyan, + "white": color.FgWhite, + + // attributes + "reset": color.Reset, + "bold": color.Bold, + "faint": color.Faint, + "italic": color.Italic, + "underline": color.Underline, + "blinkslow": color.BlinkSlow, + "blinkrapid": color.BlinkRapid, + "reversevideo": color.ReverseVideo, + "concealed": color.Concealed, + "crossedout": color.CrossedOut, + + // foreground text colors + "fgBlack": color.FgBlack, + "fgRed": color.FgRed, + "fgGreen": color.FgGreen, + "fgYellow": color.FgYellow, + "fgBlue": color.FgBlue, + "fgMagenta": color.FgMagenta, + "fgCyan": color.FgCyan, + "fgWhite": color.FgWhite, + + // foreground Hi-Intensity text colors + "fgHiBlack": color.FgHiBlack, + "fgHiRed": color.FgHiRed, + "fgHiGreen": color.FgHiGreen, + "fgHiYellow": color.FgHiYellow, + "fgHiBlue": color.FgHiBlue, + "fgHiMagenta": color.FgHiMagenta, + "fgHiCyan": color.FgHiCyan, + "fgHiWhite": color.FgHiWhite, + + // background text colors + "bgBlack": color.BgBlack, + "bgRed": color.BgRed, + "bgGreen": color.BgGreen, + "bgYellow": color.BgYellow, + "bgBlue": color.BgBlue, + "bgMagenta": color.BgMagenta, + "bgCyan": color.BgCyan, + "bgWhite": color.BgWhite, + + // background Hi-Intensity text colors + "bgHiBlack": color.BgHiBlack, + "bgHiRed": color.BgHiRed, + "bgHiGreen": color.BgHiGreen, + "bgHiYellow": color.BgHiYellow, + "bgHiBlue": color.BgHiBlue, + "bgHiMagenta": color.BgHiMagenta, + "bgHiCyan": color.BgHiCyan, + "bgHiWhite": color.BgHiWhite, +} + +// validColor will make sure the given color is actually allowed. +func validColor(c string) bool { + return validColors[c] +} + +// Spinner struct to hold the provided options. +type Spinner struct { + mu *sync.RWMutex + Delay time.Duration // Delay is the speed of the indicator + chars []string // chars holds the chosen character set + Prefix string // Prefix is the text preppended to the indicator + Suffix string // Suffix is the text appended to the indicator + FinalMSG string // string displayed after Stop() is called + lastOutput string // last character(set) written + color func(a ...interface{}) string // default color is white + Writer io.Writer // to make testing better, exported so users have access. Use `WithWriter` to update after initialization. + active bool // active holds the state of the spinner + stopChan chan struct{} // stopChan is a channel used to stop the indicator + HideCursor bool // hideCursor determines if the cursor is visible + PreUpdate func(s *Spinner) // will be triggered before every spinner update + PostUpdate func(s *Spinner) // will be triggered after every spinner update +} + +// New provides a pointer to an instance of Spinner with the supplied options. +func New(cs []string, d time.Duration, options ...Option) *Spinner { + s := &Spinner{ + Delay: d, + chars: cs, + color: color.New(color.FgWhite).SprintFunc(), + mu: &sync.RWMutex{}, + Writer: color.Output, + stopChan: make(chan struct{}, 1), + active: false, + HideCursor: true, + } + + for _, option := range options { + option(s) + } + + return s +} + +// Option is a function that takes a spinner and applies +// a given configuration. +type Option func(*Spinner) + +// Options contains fields to configure the spinner. +type Options struct { + Color string + Suffix string + FinalMSG string + HideCursor bool +} + +// WithColor adds the given color to the spinner. +func WithColor(color string) Option { + return func(s *Spinner) { + s.Color(color) + } +} + +// WithSuffix adds the given string to the spinner +// as the suffix. +func WithSuffix(suffix string) Option { + return func(s *Spinner) { + s.Suffix = suffix + } +} + +// WithFinalMSG adds the given string ot the spinner +// as the final message to be written. +func WithFinalMSG(finalMsg string) Option { + return func(s *Spinner) { + s.FinalMSG = finalMsg + } +} + +// WithHiddenCursor hides the cursor +// if hideCursor = true given. +func WithHiddenCursor(hideCursor bool) Option { + return func(s *Spinner) { + s.HideCursor = hideCursor + } +} + +// WithWriter adds the given writer to the spinner. This +// function should be favored over directly assigning to +// the struct value. +func WithWriter(w io.Writer) Option { + return func(s *Spinner) { + s.mu.Lock() + s.Writer = w + s.mu.Unlock() + } +} + +// Active will return whether or not the spinner is currently active. +func (s *Spinner) Active() bool { + return s.active +} + +// Start will start the indicator. +func (s *Spinner) Start() { + s.mu.Lock() + if s.active || !isRunningInTerminal() { + s.mu.Unlock() + return + } + if s.HideCursor && !isWindowsTerminalOnWindows { + // hides the cursor + fmt.Fprint(s.Writer, "\033[?25l") + } + s.active = true + s.mu.Unlock() + + go func() { + for { + for i := 0; i < len(s.chars); i++ { + select { + case <-s.stopChan: + return + default: + s.mu.Lock() + if !s.active { + s.mu.Unlock() + return + } + if !isWindowsTerminalOnWindows { + s.erase() + } + + if s.PreUpdate != nil { + s.PreUpdate(s) + } + + var outColor string + if runtime.GOOS == "windows" { + if s.Writer == os.Stderr { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) + } else { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) + } + } else { + outColor = fmt.Sprintf("\r%s%s%s", s.Prefix, s.color(s.chars[i]), s.Suffix) + } + outPlain := fmt.Sprintf("\r%s%s%s", s.Prefix, s.chars[i], s.Suffix) + fmt.Fprint(s.Writer, outColor) + s.lastOutput = outPlain + delay := s.Delay + + if s.PostUpdate != nil { + s.PostUpdate(s) + } + + s.mu.Unlock() + time.Sleep(delay) + } + } + } + }() +} + +// Stop stops the indicator. +func (s *Spinner) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + if s.active { + s.active = false + if s.HideCursor && !isWindowsTerminalOnWindows { + // makes the cursor visible + fmt.Fprint(s.Writer, "\033[?25h") + } + s.erase() + if s.FinalMSG != "" { + if isWindowsTerminalOnWindows { + fmt.Fprint(s.Writer, "\r", s.FinalMSG) + } else { + fmt.Fprint(s.Writer, s.FinalMSG) + } + } + s.stopChan <- struct{}{} + } +} + +// Restart will stop and start the indicator. +func (s *Spinner) Restart() { + s.Stop() + s.Start() +} + +// Reverse will reverse the order of the slice assigned to the indicator. +func (s *Spinner) Reverse() { + s.mu.Lock() + for i, j := 0, len(s.chars)-1; i < j; i, j = i+1, j-1 { + s.chars[i], s.chars[j] = s.chars[j], s.chars[i] + } + s.mu.Unlock() +} + +// Color will set the struct field for the given color to be used. The spinner +// will need to be explicitly restarted. +func (s *Spinner) Color(colors ...string) error { + colorAttributes := make([]color.Attribute, len(colors)) + + // Verify colours are valid and place the appropriate attribute in the array + for index, c := range colors { + if !validColor(c) { + return errInvalidColor + } + colorAttributes[index] = colorAttributeMap[c] + } + + s.mu.Lock() + s.color = color.New(colorAttributes...).SprintFunc() + s.mu.Unlock() + return nil +} + +// UpdateSpeed will set the indicator delay to the given value. +func (s *Spinner) UpdateSpeed(d time.Duration) { + s.mu.Lock() + s.Delay = d + s.mu.Unlock() +} + +// UpdateCharSet will change the current character set to the given one. +func (s *Spinner) UpdateCharSet(cs []string) { + s.mu.Lock() + s.chars = cs + s.mu.Unlock() +} + +// erase deletes written characters on the current line. +// Caller must already hold s.lock. +func (s *Spinner) erase() { + n := utf8.RuneCountInString(s.lastOutput) + if runtime.GOOS == "windows" && !isWindowsTerminalOnWindows { + clearString := "\r" + strings.Repeat(" ", n) + "\r" + fmt.Fprint(s.Writer, clearString) + s.lastOutput = "" + return + } + + // Taken from https://en.wikipedia.org/wiki/ANSI_escape_code: + // \r - Carriage return - Moves the cursor to column zero + // \033[K - Erases part of the line. If n is 0 (or missing), clear from + // cursor to the end of the line. If n is 1, clear from cursor to beginning + // of the line. If n is 2, clear entire line. Cursor position does not + // change. + fmt.Fprintf(s.Writer, "\r\033[K") + s.lastOutput = "" +} + +// Lock allows for manual control to lock the spinner. +func (s *Spinner) Lock() { + s.mu.Lock() +} + +// Unlock allows for manual control to unlock the spinner. +func (s *Spinner) Unlock() { + s.mu.Unlock() +} + +// GenerateNumberSequence will generate a slice of integers at the +// provided length and convert them each to a string. +func GenerateNumberSequence(length int) []string { + numSeq := make([]string, length) + for i := 0; i < length; i++ { + numSeq[i] = strconv.Itoa(i) + } + return numSeq +} + +// isRunningInTerminal check if stdout file descriptor is terminal +func isRunningInTerminal() bool { + return isatty.IsTerminal(os.Stdout.Fd()) +} diff --git a/vendor/github.com/fatih/color/README.md b/vendor/github.com/fatih/color/README.md index 42d9abc07..5152bf59b 100644 --- a/vendor/github.com/fatih/color/README.md +++ b/vendor/github.com/fatih/color/README.md @@ -1,20 +1,11 @@ -# Archived project. No maintenance. - -This project is not maintained anymore and is archived. Feel free to fork and -make your own changes if needed. For more detail read my blog post: [Taking an indefinite sabbatical from my projects](https://arslan.io/2018/10/09/taking-an-indefinite-sabbatical-from-my-projects/) - -Thanks to everyone for their valuable feedback and contributions. - - -# Color [![GoDoc](https://godoc.org/github.com/fatih/color?status.svg)](https://godoc.org/github.com/fatih/color) +# color [![](https://github.com/fatih/color/workflows/build/badge.svg)](https://github.com/fatih/color/actions) [![PkgGoDev](https://pkg.go.dev/badge/github.com/fatih/color)](https://pkg.go.dev/github.com/fatih/color) Color lets you use colorized outputs in terms of [ANSI Escape Codes](http://en.wikipedia.org/wiki/ANSI_escape_code#Colors) in Go (Golang). It has support for Windows too! The API can be used in several ways, pick one that suits you. - -![Color](https://i.imgur.com/c1JI0lA.png) +![Color](https://user-images.githubusercontent.com/438920/96832689-03b3e000-13f4-11eb-9803-46f4c4de3406.jpg) ## Install @@ -87,7 +78,7 @@ notice("Don't forget this...") ### Custom fprint functions (FprintFunc) ```go -blue := color.New(FgBlue).FprintfFunc() +blue := color.New(color.FgBlue).FprintfFunc() blue(myWriter, "important notice: %s", stars) // Mix up with multiple attributes @@ -136,14 +127,16 @@ fmt.Println("All text will now be bold magenta.") There might be a case where you want to explicitly disable/enable color output. the `go-isatty` package will automatically disable color output for non-tty output streams -(for example if the output were piped directly to `less`) +(for example if the output were piped directly to `less`). -`Color` has support to disable/enable colors both globally and for single color -definitions. For example suppose you have a CLI app and a `--no-color` bool flag. You -can easily disable the color output with: +The `color` package also disables color output if the [`NO_COLOR`](https://no-color.org) environment +variable is set (regardless of its value). -```go +`Color` has support to disable/enable colors programatically both globally and +for single color definitions. For example suppose you have a CLI app and a +`--no-color` bool flag. You can easily disable the color output with: +```go var flagNoColor = flag.Bool("no-color", false, "Disable color output") if *flagNoColor { @@ -165,6 +158,10 @@ c.EnableColor() c.Println("This prints again cyan...") ``` +## GitHub Actions + +To output color in GitHub Actions (or other CI systems that support ANSI colors), make sure to set `color.NoColor = false` so that it bypasses the check for non-tty output streams. + ## Todo * Save/Return previous values @@ -179,4 +176,3 @@ c.Println("This prints again cyan...") ## License The MIT License (MIT) - see [`LICENSE.md`](https://github.com/fatih/color/blob/master/LICENSE.md) for more details - diff --git a/vendor/github.com/fatih/color/color.go b/vendor/github.com/fatih/color/color.go index 91c8e9f06..98a60f3c8 100644 --- a/vendor/github.com/fatih/color/color.go +++ b/vendor/github.com/fatih/color/color.go @@ -15,9 +15,11 @@ import ( var ( // NoColor defines if the output is colorized or not. It's dynamically set to // false or true based on the stdout's file descriptor referring to a terminal - // or not. This is a global option and affects all colors. For more control - // over each color block use the methods DisableColor() individually. - NoColor = os.Getenv("TERM") == "dumb" || + // or not. It's also set to true if the NO_COLOR environment variable is + // set (regardless of its value). This is a global option and affects all + // colors. For more control over each color block use the methods + // DisableColor() individually. + NoColor = noColorExists() || os.Getenv("TERM") == "dumb" || (!isatty.IsTerminal(os.Stdout.Fd()) && !isatty.IsCygwinTerminal(os.Stdout.Fd())) // Output defines the standard output of the print functions. By default @@ -33,6 +35,12 @@ var ( colorsCacheMu sync.Mutex // protects colorsCache ) +// noColorExists returns true if the environment variable NO_COLOR exists. +func noColorExists() bool { + _, exists := os.LookupEnv("NO_COLOR") + return exists +} + // Color defines a custom color object which is defined by SGR parameters. type Color struct { params []Attribute @@ -108,7 +116,14 @@ const ( // New returns a newly created color object. func New(value ...Attribute) *Color { - c := &Color{params: make([]Attribute, 0)} + c := &Color{ + params: make([]Attribute, 0), + } + + if noColorExists() { + c.noColor = boolPtr(true) + } + c.Add(value...) return c } @@ -387,7 +402,7 @@ func (c *Color) EnableColor() { } func (c *Color) isNoColorSet() bool { - // check first if we have user setted action + // check first if we have user set action if c.noColor != nil { return *c.noColor } diff --git a/vendor/github.com/fatih/color/doc.go b/vendor/github.com/fatih/color/doc.go index cf1e96500..04541de78 100644 --- a/vendor/github.com/fatih/color/doc.go +++ b/vendor/github.com/fatih/color/doc.go @@ -118,6 +118,8 @@ the color output with: color.NoColor = true // disables colorized output } +You can also disable the color by setting the NO_COLOR environment variable to any value. + It also has support for single color definitions (local). You can disable/enable color output on the fly: diff --git a/vendor/github.com/mattn/go-colorable/.travis.yml b/vendor/github.com/mattn/go-colorable/.travis.yml deleted file mode 100644 index 7942c565c..000000000 --- a/vendor/github.com/mattn/go-colorable/.travis.yml +++ /dev/null @@ -1,15 +0,0 @@ -language: go -sudo: false -go: - - 1.13.x - - tip - -before_install: - - go get -t -v ./... - -script: - - ./go.test.sh - -after_success: - - bash <(curl -s https://codecov.io/bash) - diff --git a/vendor/github.com/mattn/go-colorable/README.md b/vendor/github.com/mattn/go-colorable/README.md index e055952b6..ca0483711 100644 --- a/vendor/github.com/mattn/go-colorable/README.md +++ b/vendor/github.com/mattn/go-colorable/README.md @@ -1,6 +1,6 @@ # go-colorable -[![Build Status](https://travis-ci.org/mattn/go-colorable.svg?branch=master)](https://travis-ci.org/mattn/go-colorable) +[![Build Status](https://github.com/mattn/go-colorable/workflows/test/badge.svg)](https://github.com/mattn/go-colorable/actions?query=workflow%3Atest) [![Codecov](https://codecov.io/gh/mattn/go-colorable/branch/master/graph/badge.svg)](https://codecov.io/gh/mattn/go-colorable) [![GoDoc](https://godoc.org/github.com/mattn/go-colorable?status.svg)](http://godoc.org/github.com/mattn/go-colorable) [![Go Report Card](https://goreportcard.com/badge/mattn/go-colorable)](https://goreportcard.com/report/mattn/go-colorable) diff --git a/vendor/github.com/mattn/go-colorable/colorable_appengine.go b/vendor/github.com/mattn/go-colorable/colorable_appengine.go index 1f7806fe1..416d1bbbf 100644 --- a/vendor/github.com/mattn/go-colorable/colorable_appengine.go +++ b/vendor/github.com/mattn/go-colorable/colorable_appengine.go @@ -1,3 +1,4 @@ +//go:build appengine // +build appengine package colorable diff --git a/vendor/github.com/mattn/go-colorable/colorable_others.go b/vendor/github.com/mattn/go-colorable/colorable_others.go index 08cbd1e0f..766d94603 100644 --- a/vendor/github.com/mattn/go-colorable/colorable_others.go +++ b/vendor/github.com/mattn/go-colorable/colorable_others.go @@ -1,5 +1,5 @@ -// +build !windows -// +build !appengine +//go:build !windows && !appengine +// +build !windows,!appengine package colorable diff --git a/vendor/github.com/mattn/go-colorable/colorable_windows.go b/vendor/github.com/mattn/go-colorable/colorable_windows.go index 41215d7fc..1846ad5ab 100644 --- a/vendor/github.com/mattn/go-colorable/colorable_windows.go +++ b/vendor/github.com/mattn/go-colorable/colorable_windows.go @@ -1,5 +1,5 @@ -// +build windows -// +build !appengine +//go:build windows && !appengine +// +build windows,!appengine package colorable @@ -452,18 +452,22 @@ func (w *Writer) Write(data []byte) (n int, err error) { } else { er = bytes.NewReader(data) } - var bw [1]byte + var plaintext bytes.Buffer loop: for { c1, err := er.ReadByte() if err != nil { + plaintext.WriteTo(w.out) break loop } if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) + plaintext.WriteByte(c1) continue } + _, err = plaintext.WriteTo(w.out) + if err != nil { + break loop + } c2, err := er.ReadByte() if err != nil { break loop diff --git a/vendor/github.com/mattn/go-colorable/noncolorable.go b/vendor/github.com/mattn/go-colorable/noncolorable.go index 95f2c6be2..05d6f74bf 100644 --- a/vendor/github.com/mattn/go-colorable/noncolorable.go +++ b/vendor/github.com/mattn/go-colorable/noncolorable.go @@ -18,18 +18,22 @@ func NewNonColorable(w io.Writer) io.Writer { // Write writes data on console func (w *NonColorable) Write(data []byte) (n int, err error) { er := bytes.NewReader(data) - var bw [1]byte + var plaintext bytes.Buffer loop: for { c1, err := er.ReadByte() if err != nil { + plaintext.WriteTo(w.out) break loop } if c1 != 0x1b { - bw[0] = c1 - w.out.Write(bw[:]) + plaintext.WriteByte(c1) continue } + _, err = plaintext.WriteTo(w.out) + if err != nil { + break loop + } c2, err := er.ReadByte() if err != nil { break loop @@ -38,7 +42,6 @@ loop: continue } - var buf bytes.Buffer for { c, err := er.ReadByte() if err != nil { @@ -47,7 +50,6 @@ loop: if ('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || c == '@' { break } - buf.Write([]byte(string(c))) } } diff --git a/vendor/modules.txt b/vendor/modules.txt index 03a346341..231982cc5 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -29,6 +29,9 @@ github.com/Microsoft/hcsshim/osversion # github.com/alexeyco/simpletable v0.0.0-20200730140406-5bb24159ccfb ## explicit github.com/alexeyco/simpletable +# github.com/briandowns/spinner v1.18.1 +## explicit; go 1.14 +github.com/briandowns/spinner # github.com/cased/cased-go v1.0.4 ## explicit; go 1.14 github.com/cased/cased-go @@ -94,7 +97,7 @@ github.com/docker/go-connections/tlsconfig # github.com/docker/go-units v0.4.0 ## explicit github.com/docker/go-units -# github.com/fatih/color v1.9.0 +# github.com/fatih/color v1.13.0 ## explicit; go 1.13 github.com/fatih/color # github.com/friendsofgo/errors v0.9.2 @@ -171,7 +174,7 @@ github.com/magiconair/properties github.com/manifoldco/promptui github.com/manifoldco/promptui/list github.com/manifoldco/promptui/screenbuf -# github.com/mattn/go-colorable v0.1.8 +# github.com/mattn/go-colorable v0.1.12 ## explicit; go 1.13 github.com/mattn/go-colorable # github.com/mattn/go-isatty v0.0.14