Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement: atmos list workflows #941

Open
wants to merge 17 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6750c43
fix: suppress config error when help is requested
Cerebrovinny Jan 17, 2025
b03c6bd
feat: add list configuration to workflows schema
Cerebrovinny Jan 14, 2025
75855f6
feat: add list workflows command to display Atmos workflows
Cerebrovinny Jan 14, 2025
a12aaab
feat: add workflow listing functionality with table output support
Cerebrovinny Jan 15, 2025
f5a00ac
Add tests for workflow listing functionality
Cerebrovinny Jan 15, 2025
264b897
fix: add newline at the end of workflow list output
Cerebrovinny Jan 15, 2025
f96cf94
refactor: use CheckTTYSupport for terminal detection
Cerebrovinny Jan 16, 2025
8f071ea
refactor: use theme styles for workflow list table formatting
Cerebrovinny Jan 16, 2025
757205a
feat(workflows): add file loading and parsing functionality for workf…
Cerebrovinny Jan 16, 2025
f8a0faf
test(list): enhance workflow listing test coverage with temporary files
Cerebrovinny Jan 16, 2025
e72cdea
feat(workflows): add JSON and CSV output formats for workflow listing
Cerebrovinny Jan 16, 2025
09f84b8
fixes log level
Cerebrovinny Jan 19, 2025
33b3ff2
update cli tests
Cerebrovinny Jan 20, 2025
2dacafc
fix: use OS-specific line endings in workflow list output
Cerebrovinny Jan 20, 2025
768d257
feat: add OS-specific line ending support and format validation tests
Cerebrovinny Jan 20, 2025
e125a03
feat: add format validation for workflow list output
Cerebrovinny Jan 20, 2025
25f6079
feat: add file path validation and handle nil workflows in manifest
Cerebrovinny Jan 20, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 67 additions & 0 deletions cmd/list_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package cmd

import (
"fmt"

"github.com/spf13/cobra"

"github.com/cloudposse/atmos/pkg/config"
l "github.com/cloudposse/atmos/pkg/list"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/theme"
u "github.com/cloudposse/atmos/pkg/utils"
)

// listWorkflowsCmd lists atmos workflows
var listWorkflowsCmd = &cobra.Command{
Use: "workflows",
Short: "List all Atmos workflows",
Long: "List Atmos workflows, with options to filter results by specific files.",
Example: "atmos list workflows\n" +
"atmos list workflows -f <file>\n" +
"atmos list workflows --format json\n" +
"atmos list workflows --format csv --delimiter ','",
Run: func(cmd *cobra.Command, args []string) {
flags := cmd.Flags()

fileFlag, err := flags.GetString("file")
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error getting the 'file' flag: %v", err), theme.Colors.Error)
return
}

formatFlag, err := flags.GetString("format")
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error getting the 'format' flag: %v", err), theme.Colors.Error)
return
}
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

delimiterFlag, err := flags.GetString("delimiter")
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error getting the 'delimiter' flag: %v", err), theme.Colors.Error)
return
}

configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error initializing CLI config: %v", err), theme.Colors.Error)
return
}

output, err := l.FilterAndListWorkflows(fileFlag, atmosConfig.Workflows.List, formatFlag, delimiterFlag)
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error: %v"+"\n", err), theme.Colors.Warning)
return
}

u.PrintMessageInColor(output, theme.Colors.Success)
},
}

func init() {
listWorkflowsCmd.PersistentFlags().StringP("file", "f", "", "Filter workflows by file (e.g., atmos list workflows -f workflow1)")
listWorkflowsCmd.PersistentFlags().String("format", "", "Output format (table, json, csv)")
listWorkflowsCmd.PersistentFlags().String("delimiter", "\t", "Delimiter for csv output")
listCmd.AddCommand(listWorkflowsCmd)
}
7 changes: 6 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,12 @@ var RootCmd = &cobra.Command{
// Only validate the config, don't store it yet since commands may need to add more info
_, err := cfg.InitCliConfig(configAndStacksInfo, false)
if err != nil {
if !errors.Is(err, cfg.NotFound) {
if errors.Is(err, cfg.NotFound) {
// For help commands or when help flag is set, we don't want to show the error
if !isHelpRequested {
u.LogWarning(errorConfig, err.Error())
}
} else {
u.LogErrorAndExit(errorConfig, err)
}
}
Expand Down
3 changes: 2 additions & 1 deletion internal/tui/templates/term/term_writer.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"io"
"os"

"github.com/cloudposse/atmos/internal/exec"
"github.com/mitchellh/go-wordwrap"
"golang.org/x/term"
)
Expand All @@ -30,7 +31,7 @@ func NewResponsiveWriter(w io.Writer) io.Writer {
return w
}

if !term.IsTerminal(int(file.Fd())) {
if !exec.CheckTTYSupport() {
return w
}

Expand Down
162 changes: 162 additions & 0 deletions pkg/list/list_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
package list

import (
"encoding/json"
"fmt"
"os"
"sort"
"strings"

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/ui/theme"
"github.com/samber/lo"

"github.com/cloudposse/atmos/pkg/schema"
"gopkg.in/yaml.v3"
)

// Extracts workflows from a workflow manifest
func getWorkflowsFromManifest(manifest schema.WorkflowManifest) ([][]string, error) {
var rows [][]string
for workflowName, workflow := range manifest.Workflows {
rows = append(rows, []string{
manifest.Name,
workflowName,
workflow.Description,
})
}
return rows, nil
}
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

// FilterAndListWorkflows filters and lists workflows based on the given file
func FilterAndListWorkflows(fileFlag string, listConfig schema.ListConfig, format string, delimiter string) (string, error) {
// Parse columns configuration
header := []string{"File", "Workflow", "Description"}

// Get all workflows from manifests
var rows [][]string

// If a specific file is provided, validate and load it
if fileFlag != "" {
if _, err := os.Stat(fileFlag); os.IsNotExist(err) {
return "", fmt.Errorf("workflow file not found: %s", fileFlag)
}

// Read and parse the workflow file
data, err := os.ReadFile(fileFlag)
if err != nil {
return "", fmt.Errorf("error reading workflow file: %w", err)
}

var manifest schema.WorkflowManifest
if err := yaml.Unmarshal(data, &manifest); err != nil {
return "", fmt.Errorf("error parsing workflow file: %w", err)
}
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

manifestRows, err := getWorkflowsFromManifest(manifest)
if err != nil {
return "", fmt.Errorf("error processing manifest: %w", err)
}
rows = append(rows, manifestRows...)
} else {
// Use example data for empty fileFlag
manifest := schema.WorkflowManifest{
Name: "example",
Workflows: schema.WorkflowConfig{
"test-1": schema.WorkflowDefinition{
Description: "Test workflow",
Steps: []schema.WorkflowStep{
{Command: "echo Command 1", Name: "step1", Type: "shell"},
{Command: "echo Command 2", Name: "step2", Type: "shell"},
{Command: "echo Command 3", Name: "step3", Type: "shell"},
{Command: "echo Command 4", Type: "shell"},
},
},
},
}

manifestRows, err := getWorkflowsFromManifest(manifest)
if err != nil {
return "", fmt.Errorf("error processing manifest: %w", err)
}
rows = append(rows, manifestRows...)
}

// Remove duplicates and sort
rows = lo.UniqBy(rows, func(row []string) string {
return strings.Join(row, delimiter)
})
sort.Slice(rows, func(i, j int) bool {
return strings.Join(rows[i], delimiter) < strings.Join(rows[j], delimiter)
})
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

if len(rows) == 0 {
return "No workflows found", nil
}

// Handle different output formats
switch format {
case "json":
// Convert to JSON format
type workflow struct {
File string `json:"file"`
Name string `json:"name"`
Description string `json:"description"`
}
var workflows []workflow
for _, row := range rows {
workflows = append(workflows, workflow{
File: row[0],
Name: row[1],
Description: row[2],
})
}
jsonBytes, err := json.MarshalIndent(workflows, "", " ")
if err != nil {
return "", fmt.Errorf("error formatting JSON output: %w", err)
}
return string(jsonBytes), nil

case "csv":
// Use the provided delimiter for CSV output
var output strings.Builder
output.WriteString(strings.Join(header, delimiter) + "\n")
for _, row := range rows {
output.WriteString(strings.Join(row, delimiter) + "\n")
}
return output.String(), nil

default:
// If format is empty or "table", use table format
if format == "" && exec.CheckTTYSupport() {
// Create a styled table for TTY
t := table.New().
Border(lipgloss.ThickBorder()).
BorderStyle(lipgloss.NewStyle().Foreground(lipgloss.Color(theme.ColorBorder))).
StyleFunc(func(row, col int) lipgloss.Style {
style := lipgloss.NewStyle().PaddingLeft(1).PaddingRight(1)
if row == 0 {
return style.Inherit(theme.Styles.CommandName).Align(lipgloss.Center)
}
if row%2 == 0 {
return style.Inherit(theme.Styles.GrayText)
}
return style.Inherit(theme.Styles.Description)
}).
Headers(header...).
Rows(rows...)

return t.String() + "\n", nil
}

// Default to simple tabular format for non-TTY or when format is explicitly "table"
var output strings.Builder
output.WriteString(strings.Join(header, delimiter) + "\n")
for _, row := range rows {
output.WriteString(strings.Join(row, delimiter) + "\n")
}
return output.String(), nil
}
}
Loading
Loading