/
workflow_runner.go
143 lines (128 loc) · 4.62 KB
/
workflow_runner.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
package workflows
import (
"context"
"fmt"
"os"
"path/filepath"
"reflect"
"github.com/aws/codecatalyst-runner-cli/command-runner/pkg/runner"
"github.com/manifoldco/promptui"
"github.com/rs/zerolog/log"
"gopkg.in/yaml.v2"
)
type RunParams struct {
NewWorkflowPlansProviderParams
NewWorkflowFeaturesProviderParams
Concurrency int
WorkflowPath string
WorkflowName string
}
func Run(ctx context.Context, params *RunParams) error {
log.Ctx(ctx).Debug().Msgf("running workflow with params %+v", *params)
if params.WorkflowPath != "" {
if _, err := os.Stat(params.WorkflowPath); err != nil {
return fmt.Errorf("unable to load workflow file '%s': %w", params.WorkflowPath, err)
}
params.WorkingDir, _ = filepath.Abs(filepath.Dir(filepath.Dir(filepath.Dir(params.WorkflowPath))))
} else {
params.WorkingDir, _ = filepath.Abs(params.WorkingDir)
if workflows, err := os.ReadDir(filepath.Join(params.WorkingDir, ".codecatalyst", "workflows")); err != nil {
return err
} else {
workflowOptions := make(map[string]string, 0)
for _, workflow := range workflows {
ext := filepath.Ext(workflow.Name())
if ext != ".yml" && ext != ".yaml" {
continue
}
workflowFile := filepath.Join(params.WorkingDir, ".codecatalyst", "workflows", workflow.Name())
log.Debug().Msgf("considering workflow file %s", workflowFile)
if workflow, err := readWorkflow(workflowFile); err != nil {
return fmt.Errorf("unable to read workflow file '%s': %w", workflowFile, err)
} else {
workflowOptions[workflow.Name] = workflowFile
}
}
if params.WorkflowName != "" {
if val, ok := workflowOptions[params.WorkflowName]; !ok {
return fmt.Errorf("no workflow defined named '%s'", params.WorkflowName)
} else {
params.WorkflowPath = val
}
} else {
// prompt to select a workflow
prompt := promptui.Select{
Label: "Select workflow",
Items: reflect.ValueOf(workflowOptions).MapKeys(),
Stdout: &bellSkipper{},
}
if _, result, err := prompt.Run(); err != nil {
return fmt.Errorf("unable to select a workflow: %w", err)
} else {
params.WorkflowPath = workflowOptions[result]
}
}
}
}
if !filepath.IsAbs(params.WorkflowPath) {
if absWorkflowPath, err := filepath.Abs(params.WorkflowPath); err != nil {
return err
} else {
params.WorkflowPath = absWorkflowPath
}
}
log.Debug().Msgf("🚚 Running workflow file '%s'", params.WorkflowPath)
workflow, err := readWorkflow(params.WorkflowPath)
if err != nil {
return fmt.Errorf("unable to read workflow file '%s': %w", params.WorkflowPath, err)
}
params.NewWorkflowPlansProviderParams.Workflow = workflow
plans := NewWorkflowPlansProvider(¶ms.NewWorkflowPlansProviderParams)
params.NewWorkflowFeaturesProviderParams.Workflow = workflow
params.NewWorkflowFeaturesProviderParams.EnvironmentConfiguration.WorkingDir = params.WorkingDir
features, err := NewWorkflowFeaturesProvider(¶ms.NewWorkflowFeaturesProviderParams)
if err != nil {
return fmt.Errorf("unable to create features provider: %w", err)
}
return runner.RunAll(ctx, &runner.RunAllParams{
Namespace: workflow.Name,
Plans: plans,
Features: features,
Concurrency: params.Concurrency,
ExecutionType: params.ExecutionType,
})
}
func readWorkflow(workflowPath string) (*Workflow, error) {
workflow := &Workflow{
Path: workflowPath,
}
if workflowContent, err := os.ReadFile(workflowPath); err != nil {
return nil, fmt.Errorf("unable to read workflow file '%s': %w", workflowPath, err)
} else if err = yaml.Unmarshal(workflowContent, workflow); err != nil {
return nil, fmt.Errorf("unable to unmarshal workflow file '%s': %w", workflowPath, err)
} else if workflow.SchemaVersion != "1.0" {
return nil, fmt.Errorf("unsupported SchemaVersion=%s found in workflow %s", workflow.SchemaVersion, workflowPath)
}
return workflow, nil
}
// bellSkipper implements an io.WriteCloser that skips the terminal bell
// character (ASCII code 7), and writes the rest to os.Stderr. It is used to
// replace readline.Stdout, that is the package used by promptui to display the
// prompts.
//
// This is a workaround for the bell issue documented in
// https://github.com/manifoldco/promptui/issues/49.
type bellSkipper struct{}
// Write implements an io.WriterCloser over os.Stderr, but it skips the terminal
// bell character.
func (bs *bellSkipper) Write(b []byte) (int, error) {
const charBell = 7 // c.f. readline.CharBell
if len(b) == 1 && b[0] == charBell {
return 0, nil
}
return os.Stderr.Write(b)
}
// Close implements an io.WriterCloser over os.Stderr.
func (bs *bellSkipper) Close() error {
return os.Stderr.Close()
}