diff --git a/cmd/workflow.go b/cmd/workflow.go index a80a849c95..3c96fcdee5 100644 --- a/cmd/workflow.go +++ b/cmd/workflow.go @@ -2,58 +2,144 @@ package cmd import ( "fmt" + "strings" "github.com/spf13/cobra" e "github.com/cloudposse/atmos/internal/exec" - "github.com/cloudposse/atmos/internal/tui/utils" "github.com/cloudposse/atmos/pkg/schema" + "github.com/cloudposse/atmos/pkg/ui/markdown" u "github.com/cloudposse/atmos/pkg/utils" ) +// ErrorMessage represents a structured error message +type ErrorMessage struct { + Title string + Details string + Suggestion string +} + +// String returns the markdown representation of the error message +func (e ErrorMessage) String() string { + var sb strings.Builder + sb.WriteString(fmt.Sprintf("# ❌ %s\n\n", e.Title)) + + if e.Details != "" { + sb.WriteString(fmt.Sprintf("## Details\n%s\n\n", e.Details)) + } + + if e.Suggestion != "" { + sb.WriteString(fmt.Sprintf("## Suggestion\n%s\n\n", e.Suggestion)) + } + + return sb.String() +} + +// renderError renders an error message using the markdown renderer +func renderError(msg ErrorMessage) error { + renderer, err := markdown.NewRenderer( + markdown.WithWidth(80), + ) + if err != nil { + return fmt.Errorf("failed to create markdown renderer: %w", err) + } + + rendered, err := renderer.Render(msg.String()) + if err != nil { + return fmt.Errorf("failed to render error message: %w", err) + } + + fmt.Print(rendered) + return nil +} + // workflowCmd executes a workflow var workflowCmd = &cobra.Command{ Use: "workflow", Short: "Execute a workflow", Long: `This command executes a workflow: atmos workflow --file `, Example: "atmos workflow\n" + - "atmos workflow -f \n" + - "atmos workflow -f -s \n" + - "atmos workflow -f -from-step \n\n" + + "atmos workflow --file \n" + + "atmos workflow --file --stack \n" + + "atmos workflow --file --from-step \n\n" + "To resume the workflow from this step, run:\n" + - "atmos workflow deploy-infra -f workflow1 -from-step deploy-vpc\n\n" + + "atmos workflow deploy-infra --file workflow1 --from-step deploy-vpc\n\n" + "For more details refer to https://atmos.tools/cli/commands/workflow/", FParseErrWhitelist: struct{ UnknownFlags bool }{UnknownFlags: false}, Run: func(cmd *cobra.Command, args []string) { - // If the first arg is "list", show a clear error message - if len(args) > 0 && args[0] == "list" { - errorMsg := "# Error! Invalid command.\n\n" + - "## Usage\n" + - "`atmos workflow --file `\n\n" + - "Run `atmos workflow --help` for more information." - - rendered, err := utils.RenderMarkdown(errorMsg, "dark") + // If no arguments are provided, start the workflow UI + if len(args) == 0 { + err := e.ExecuteWorkflowCmd(cmd, args) if err != nil { - fmt.Println(errorMsg) // Fallback to plain text if rendering fails - } else { - fmt.Print(rendered) + u.LogErrorAndExit(schema.CliConfiguration{}, err) } return } + // Check if the workflow name is "list" (invalid command) + if args[0] == "list" { + err := renderError(ErrorMessage{ + Title: "Invalid Command", + Details: "The command `atmos workflow list` is not valid.", + Suggestion: "Use `atmos workflow --file ` to execute a workflow, or run `atmos workflow` without arguments to use the interactive UI.", + }) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + return + } + + // Get the --file flag value + workflowFile, _ := cmd.Flags().GetString("file") + if workflowFile == "" { + err := renderError(ErrorMessage{ + Title: "Missing Required Flag", + Details: "The `--file` flag is required to specify a workflow manifest.", + Suggestion: "Example:\n`atmos workflow deploy-infra --file workflow1`", + }) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + return + } + + // Execute the workflow command err := e.ExecuteWorkflowCmd(cmd, args) if err != nil { - u.LogErrorAndExit(schema.CliConfiguration{}, err) + // Format common error messages + if strings.Contains(err.Error(), "does not exist") { + err := renderError(ErrorMessage{ + Title: "Workflow File Not Found", + Details: fmt.Sprintf("The workflow manifest file '%s' could not be found.", workflowFile), + Suggestion: "Check if the file exists in the workflows directory and the path is correct.", + }) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + } else if strings.Contains(err.Error(), "does not have the") { + err := renderError(ErrorMessage{ + Title: "Invalid Workflow", + Details: err.Error(), + Suggestion: fmt.Sprintf("Check the available workflows in '%s' and make sure you're using the correct workflow name.", workflowFile), + }) + if err != nil { + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + } else { + // For other errors, use the standard error handler + u.LogErrorAndExit(schema.CliConfiguration{}, err) + } + return } }, } func init() { workflowCmd.DisableFlagParsing = false - workflowCmd.PersistentFlags().StringP("file", "f", "", "atmos workflow -f ") - workflowCmd.PersistentFlags().Bool("dry-run", false, "atmos workflow -f --dry-run") - workflowCmd.PersistentFlags().StringP("stack", "s", "", "atmos workflow -f -s ") - workflowCmd.PersistentFlags().String("from-step", "", "atmos workflow -f -from-step ") + workflowCmd.PersistentFlags().StringP("file", "f", "", "atmos workflow --file ") + workflowCmd.PersistentFlags().Bool("dry-run", false, "atmos workflow --file --dry-run") + workflowCmd.PersistentFlags().StringP("stack", "s", "", "atmos workflow --file --stack ") + workflowCmd.PersistentFlags().String("from-step", "", "atmos workflow --file --from-step ") RootCmd.AddCommand(workflowCmd) } diff --git a/go.mod b/go.mod index 84aab497b0..dd51b59e49 100644 --- a/go.mod +++ b/go.mod @@ -39,6 +39,7 @@ require ( github.com/mitchellh/go-homedir v1.1.0 github.com/mitchellh/go-wordwrap v1.0.1 github.com/mitchellh/mapstructure v1.5.0 + github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a github.com/open-policy-agent/opa v0.70.0 github.com/otiai10/copy v1.14.0 github.com/pkg/errors v0.9.1 @@ -194,7 +195,6 @@ require ( github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/reflow v0.3.0 // indirect - github.com/muesli/termenv v0.15.3-0.20240618155329-98d742f6907a // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/oklog/run v1.1.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect