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

Merged
merged 27 commits into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
514acfb
fix: suppress config error when help is requested
Cerebrovinny Jan 17, 2025
e4ebc35
feat: add list configuration to workflows schema
Cerebrovinny Jan 14, 2025
99e154c
feat: add list workflows command to display Atmos workflows
Cerebrovinny Jan 14, 2025
c31c03a
feat: add workflow listing functionality with table output support
Cerebrovinny Jan 15, 2025
8a30e04
Add tests for workflow listing functionality
Cerebrovinny Jan 15, 2025
c6bf17d
fix: add newline at the end of workflow list output
Cerebrovinny Jan 15, 2025
665d403
refactor: use CheckTTYSupport for terminal detection
Cerebrovinny Jan 16, 2025
1425b8e
refactor: use theme styles for workflow list table formatting
Cerebrovinny Jan 16, 2025
6e26a8f
feat(workflows): add file loading and parsing functionality for workf…
Cerebrovinny Jan 16, 2025
df3d8f9
test(list): enhance workflow listing test coverage with temporary files
Cerebrovinny Jan 16, 2025
9ec99ab
feat(workflows): add JSON and CSV output formats for workflow listing
Cerebrovinny Jan 16, 2025
9648e08
fixes log level
Cerebrovinny Jan 19, 2025
eb95d39
update cli tests
Cerebrovinny Jan 20, 2025
d2fc75e
fix: use OS-specific line endings in workflow list output
Cerebrovinny Jan 20, 2025
51da66f
feat: add OS-specific line ending support and format validation tests
Cerebrovinny Jan 20, 2025
ad5d75f
feat: add format validation for workflow list output
Cerebrovinny Jan 20, 2025
a7b317f
feat: add file path validation and handle nil workflows in manifest
Cerebrovinny Jan 20, 2025
f9a2431
refactor: move line ending logic to utils package
Cerebrovinny Jan 21, 2025
809a76a
Add test snapshots for log level validation and CLI command output
Cerebrovinny Jan 21, 2025
2210ce7
Enable snapshots and add diff validation for log level tests
Cerebrovinny Jan 21, 2025
c02b97d
feat: add GetLineEnding function to handle platform-specific line end…
Cerebrovinny Jan 21, 2025
d566118
refactor: use runtime.GOOS to detect Windows platform for line endings
Cerebrovinny Jan 21, 2025
a8130b9
update copy
Cerebrovinny Jan 21, 2025
50cd2db
fix expected diff
Cerebrovinny Jan 21, 2025
bf14e63
feat: implement workflow discovery from configured workflow directory
Cerebrovinny Jan 22, 2025
c8e10c1
Remove alternating row styles in workflow list
Cerebrovinny Jan 24, 2025
6b0797b
Merge branch 'main' into DEV-2801
aknysh Jan 24, 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
229 changes: 229 additions & 0 deletions pkg/list/list_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
package list

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

"github.com/charmbracelet/lipgloss"
"github.com/charmbracelet/lipgloss/table"
"github.com/cloudposse/atmos/internal/exec"
"github.com/cloudposse/atmos/pkg/config"
"github.com/cloudposse/atmos/pkg/schema"
"github.com/cloudposse/atmos/pkg/ui/theme"
"github.com/cloudposse/atmos/pkg/utils"
"github.com/samber/lo"
"gopkg.in/yaml.v3"
)

const (
FormatTable = "table"
FormatJSON = "json"
FormatCSV = "csv"
)

// ValidateFormat checks if the given format is supported
func ValidateFormat(format string) error {
if format == "" {
return nil
}
validFormats := []string{FormatTable, FormatJSON, FormatCSV}
for _, f := range validFormats {
if format == f {
return nil
}
}
return fmt.Errorf("invalid format '%s'. Supported formats are: %s", format, strings.Join(validFormats, ", "))
}

// Extracts workflows from a workflow manifest
func getWorkflowsFromManifest(manifest schema.WorkflowManifest) ([][]string, error) {
var rows [][]string
if manifest.Workflows == nil {
return rows, nil
}
for workflowName, workflow := range manifest.Workflows {
rows = append(rows, []string{
manifest.Name,
workflowName,
workflow.Description,
})
}
return rows, nil
}

// FilterAndListWorkflows filters and lists workflows based on the given file
func FilterAndListWorkflows(fileFlag string, listConfig schema.ListConfig, format string, delimiter string) (string, error) {
if err := ValidateFormat(format); err != nil {
return "", err
}

if format == "" && listConfig.Format != "" {
if err := ValidateFormat(listConfig.Format); err != nil {
return "", err
}
format = listConfig.Format
}

// 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 != "" {
// Validate file path
cleanPath := filepath.Clean(fileFlag)
if !utils.IsYaml(cleanPath) {
return "", fmt.Errorf("invalid workflow file extension: %s", 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)
}

manifestRows, err := getWorkflowsFromManifest(manifest)
if err != nil {
return "", fmt.Errorf("error processing manifest: %w", err)
}
rows = append(rows, manifestRows...)
} else {
configAndStacksInfo := schema.ConfigAndStacksInfo{}
atmosConfig, err := config.InitCliConfig(configAndStacksInfo, true)
if err != nil {
return "", fmt.Errorf("error initializing CLI config: %w", err)
}

// Get the workflows directory
var workflowsDir string
if utils.IsPathAbsolute(atmosConfig.Workflows.BasePath) {
workflowsDir = atmosConfig.Workflows.BasePath
} else {
workflowsDir = filepath.Join(atmosConfig.BasePath, atmosConfig.Workflows.BasePath)
}

isDirectory, err := utils.IsDirectory(workflowsDir)
if err != nil || !isDirectory {
return "", fmt.Errorf("the workflow directory '%s' does not exist. Review 'workflows.base_path' in 'atmos.yaml'", workflowsDir)
}

files, err := utils.GetAllYamlFilesInDir(workflowsDir)
if err != nil {
return "", fmt.Errorf("error reading the directory '%s' defined in 'workflows.base_path' in 'atmos.yaml': %v",
atmosConfig.Workflows.BasePath, err)
}

for _, f := range files {
var workflowPath string
if utils.IsPathAbsolute(atmosConfig.Workflows.BasePath) {
workflowPath = filepath.Join(atmosConfig.Workflows.BasePath, f)
} else {
workflowPath = filepath.Join(atmosConfig.BasePath, atmosConfig.Workflows.BasePath, f)
}

fileContent, err := os.ReadFile(workflowPath)
if err != nil {
return "", err
}

var manifest schema.WorkflowManifest
if err := yaml.Unmarshal(fileContent, &manifest); err != nil {
return "", fmt.Errorf("error parsing the workflow manifest '%s': %v", f, err)
}

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) + utils.GetLineEnding())
for _, row := range rows {
output.WriteString(strings.Join(row, delimiter) + utils.GetLineEnding())
}
return output.String(), nil

Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved
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)
}
// Use consistent style for all rows
return style.Inherit(theme.Styles.Description)
}).
Headers(header...).
Rows(rows...)

return t.String() + utils.GetLineEnding(), 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) + utils.GetLineEnding())
for _, row := range rows {
output.WriteString(strings.Join(row, delimiter) + utils.GetLineEnding())
}
return output.String(), nil
}
}
Loading
Loading