codecatalyst-runner/pkg/workflows/workflow_runner.go (117 lines of code) (raw):
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()
}