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 11 commits into
base: main
Choose a base branch
from
Open
55 changes: 55 additions & 0 deletions cmd/list_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package cmd

import (
"fmt"

"github.com/fatih/color"
"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, showing their associated files and workflow names for easy reference.",
Example: "atmos list workflows\n" +
"atmos list workflows -f <file>",
Run: func(cmd *cobra.Command, args []string) {
// Check Atmos configuration
checkAtmosConfig()

flags := cmd.Flags()

fileFlag, err := flags.GetString("file")
if err != nil {
u.PrintMessageInColor(fmt.Sprintf("Error getting the 'file' flag: %v", err), color.New(color.FgRed))
return
}
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

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)
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)")
listCmd.AddCommand(listWorkflowsCmd)
}
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
115 changes: 115 additions & 0 deletions pkg/list/list_workflows.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package list

import (
"fmt"
"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"
)

// getWorkflowsFromManifest 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
}

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

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

// TODO: Implement actual workflow manifest loading logic
// For now using example data
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, "\t")
})
sort.Slice(rows, func(i, j int) bool {
return strings.Join(rows[i], "\t") < strings.Join(rows[j], "\t")
})
Comment on lines +85 to +90
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's support an optional --delimiter flag, that defaults to \t


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

// Calculate column widths
colWidths := make([]int, len(header))
for i, h := range header {
colWidths[i] = len(h)
}
for _, row := range rows {
for i, field := range row {
if len(field) > colWidths[i] {
colWidths[i] = len(field)
}
}
}

// Check if TTY is attached
if !exec.CheckTTYSupport() {
// Degrade to simple tabular format
var output strings.Builder
output.WriteString(strings.Join(header, "\t") + "\n")
for _, row := range rows {
output.WriteString(strings.Join(row, "\t") + "\n")
}
return output.String(), nil
}

// Create a styled table
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we should use probably the OS new line characters

}
47 changes: 47 additions & 0 deletions pkg/list/list_workflows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package list

import (
"testing"

"github.com/stretchr/testify/assert"

"github.com/cloudposse/atmos/pkg/schema"
)

func TestListWorkflows(t *testing.T) {
listConfig := schema.ListConfig{
Columns: []schema.ListColumnConfig{
{Name: "File", Value: "{{ .workflow_file }}"},
{Name: "Workflow", Value: "{{ .workflow_name }}"},
{Name: "Description", Value: "{{ .workflow_description }}"},
},
}

output, err := FilterAndListWorkflows("", listConfig)
assert.Nil(t, err)
assert.Contains(t, output, "File")
assert.Contains(t, output, "Workflow")
assert.Contains(t, output, "Description")
assert.Contains(t, output, "example")
assert.Contains(t, output, "test-1")
assert.Contains(t, output, "Test workflow")
}
Cerebrovinny marked this conversation as resolved.
Show resolved Hide resolved

func TestListWorkflowsWithFile(t *testing.T) {
listConfig := schema.ListConfig{
Columns: []schema.ListColumnConfig{
{Name: "File", Value: "{{ .workflow_file }}"},
{Name: "Workflow", Value: "{{ .workflow_name }}"},
{Name: "Description", Value: "{{ .workflow_description }}"},
},
}

output, err := FilterAndListWorkflows("example", listConfig)
assert.Nil(t, err)
assert.Contains(t, output, "File")
assert.Contains(t, output, "Workflow")
assert.Contains(t, output, "Description")
assert.Contains(t, output, "example")
assert.Contains(t, output, "test-1")
assert.Contains(t, output, "Test workflow")
}
13 changes: 12 additions & 1 deletion pkg/schema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -134,7 +134,8 @@ type Stacks struct {
}

type Workflows struct {
BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"`
BasePath string `yaml:"base_path" json:"base_path" mapstructure:"base_path"`
List ListConfig `yaml:"list" json:"list" mapstructure:"list"`
}

type Logs struct {
Expand Down Expand Up @@ -668,3 +669,13 @@ type MarkdownStyle struct {
type ChromaStyle struct {
Color string `yaml:"color,omitempty" json:"color,omitempty" mapstructure:"color"`
}

type ListConfig struct {
Format string `yaml:"format" json:"format" mapstructure:"format"`
Columns []ListColumnConfig `yaml:"columns" json:"columns" mapstructure:"columns"`
}

type ListColumnConfig struct {
Name string `yaml:"name" json:"name" mapstructure:"name"`
Value string `yaml:"value" json:"value" mapstructure:"value"`
}
Loading