kubevela/references/cli/workflow.go

634 lines
21 KiB
Go

/*
Copyright 2021 The KubeVela Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cli
import (
"context"
"fmt"
"io"
"os"
"github.com/AlecAivazis/survey/v2"
"github.com/gosuri/uitable"
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
"github.com/pkg/errors"
"github.com/spf13/cobra"
"sigs.k8s.io/controller-runtime/pkg/client"
pkgmulticluster "github.com/kubevela/pkg/multicluster"
wfTypes "github.com/kubevela/workflow/pkg/types"
wfUtils "github.com/kubevela/workflow/pkg/utils"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1alpha1"
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
"github.com/oam-dev/kubevela/apis/types"
"github.com/oam-dev/kubevela/pkg/utils/common"
querytypes "github.com/oam-dev/kubevela/pkg/utils/types"
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
"github.com/oam-dev/kubevela/pkg/workflow/operation"
)
// NewWorkflowCommand create `workflow` command
func NewWorkflowCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
cmd := &cobra.Command{
Use: "workflow",
Short: "Operate application delivery workflow.",
Long: "Operate the Workflow during Application Delivery. Note that workflow command is both valid for Application Workflow and WorkflowRun(expect for [restart, rollout] command, they're only valid for Application Workflow). The command will try to find the Application first, if not found, it will try to find WorkflowRun. You can also specify the resource type by using --type flag.",
Annotations: map[string]string{
types.TagCommandType: types.TypeCD,
types.TagCommandOrder: order,
},
}
wargs := &WorkflowArgs{
Args: c,
Writer: ioStreams.Out,
}
cmd.SetOut(ioStreams.Out)
cmd.AddCommand(
NewWorkflowSuspendCommand(c, ioStreams, wargs),
NewWorkflowResumeCommand(c, ioStreams, wargs),
NewWorkflowTerminateCommand(c, ioStreams, wargs),
NewWorkflowRestartCommand(c, ioStreams, wargs),
NewWorkflowRollbackCommand(c, ioStreams, wargs),
NewWorkflowLogsCommand(c, ioStreams, wargs),
NewWorkflowDebugCommand(c, ioStreams, wargs),
NewWorkflowListCommand(c, ioStreams, wargs),
)
return cmd
}
// NewWorkflowSuspendCommand create workflow suspend command
func NewWorkflowSuspendCommand(_ common.Args, _ cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "suspend",
Short: "Suspend a workflow.",
Long: "Suspend a workflow in cluster.",
Example: "vela workflow suspend <workflow-name>",
PreRun: wargs.checkWorkflowNotComplete(),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
if wargs.StepName != "" {
return wargs.StepOperator.Suspend(ctx, wargs.StepName)
}
return wargs.Operator.Suspend(ctx)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&wargs.StepName, "step", "s", "", "specify the step name in the workflow")
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
return cmd
}
// NewWorkflowResumeCommand create workflow resume command
func NewWorkflowResumeCommand(_ common.Args, _ cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "resume",
Short: "Resume a suspend workflow.",
Long: "Resume a suspend workflow in cluster.",
Example: "vela workflow resume <workflow-name>",
PreRun: wargs.checkWorkflowNotComplete(),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
if wargs.StepName != "" {
return wargs.StepOperator.Resume(ctx, wargs.StepName)
}
return wargs.Operator.Resume(ctx)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&wargs.StepName, "step", "s", "", "specify the step name in the workflow")
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
return cmd
}
// NewWorkflowTerminateCommand create workflow terminate command
func NewWorkflowTerminateCommand(_ common.Args, _ cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "terminate",
Short: "Terminate a workflow.",
Long: "Terminate a workflow in cluster.",
Example: "vela workflow terminate <workflow-name>",
PreRun: wargs.checkWorkflowNotComplete(),
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
return wargs.Operator.Terminate(ctx)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
return cmd
}
// NewWorkflowRestartCommand create workflow restart command
func NewWorkflowRestartCommand(_ common.Args, _ cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "restart",
Short: "Restart a workflow.",
Long: "Restart a workflow in cluster.",
Example: "vela workflow restart <workflow-name>",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
if wargs.StepName != "" {
return wargs.StepOperator.Restart(ctx, wargs.StepName)
}
return wargs.Operator.Restart(ctx)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&wargs.StepName, "step", "s", "", "specify the step name in the workflow")
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
return cmd
}
// NewWorkflowRollbackCommand create workflow rollback command
func NewWorkflowRollbackCommand(_ common.Args, _ cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "rollback",
Short: "Rollback an application workflow to the latest revision.",
Long: "Rollback an application workflow to the latest revision.",
Example: "vela workflow rollback <application-name>",
RunE: func(cmd *cobra.Command, args []string) error {
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
return wargs.Operator.Rollback(ctx)
},
}
addNamespaceAndEnvArg(cmd)
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
return cmd
}
// NewWorkflowLogsCommand create workflow logs command
func NewWorkflowLogsCommand(c common.Args, ioStream cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "logs",
Short: "Tail logs for workflow steps",
Long: "Tail logs for workflow steps, note that you need to use op.#Logs in step definition to set the log config of the step.",
Example: "vela workflow logs <workflow-name>",
RunE: func(cmd *cobra.Command, args []string) error {
cli, err := c.GetClient()
if err != nil {
return err
}
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
return wargs.printStepLogs(ctx, cli, ioStream)
},
}
cmd.Flags().StringVarP(&wargs.StepName, "step", "s", "", "specify the step name in the workflow")
cmd.Flags().StringVarP(&wargs.Output, "output", "o", "default", "output format for logs, support: [default, raw, json]")
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
addNamespaceAndEnvArg(cmd)
return cmd
}
// NewWorkflowDebugCommand create workflow debug command
func NewWorkflowDebugCommand(c common.Args, ioStream cmdutil.IOStreams, wargs *WorkflowArgs) *cobra.Command {
dOpts := &debugOpts{
step: wargs.StepName,
}
cmd := &cobra.Command{
Use: "debug",
Short: "Debug workflow steps",
Long: "Debug workflow steps",
Example: "vela workflow debug <workflow-name>",
PreRun: wargs.checkDebugMode(),
RunE: func(cmd *cobra.Command, args []string) error {
cli, err := c.GetClient()
if err != nil {
return err
}
ctx := context.Background()
if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
return err
}
dOpts.opts = wargs.getWorkflowSteps()
dOpts.errMap = wargs.ErrMap
return dOpts.debugWorkflow(ctx, wargs, cli, ioStream)
},
}
cmd.Flags().StringVarP(&wargs.StepName, "step", "s", "", "specify the step name in the workflow")
cmd.Flags().StringVarP(&dOpts.focus, "focus", "f", "", "specify the focus value to debug, only valid for application with workflow")
cmd.Flags().StringVarP(&wargs.Type, "type", "t", "", "the type of the resource, support: [app, workflow]")
addNamespaceAndEnvArg(cmd)
return cmd
}
// NewWorkflowListCommand create workflow list command
func NewWorkflowListCommand(c common.Args, ioStream cmdutil.IOStreams, _ *WorkflowArgs) *cobra.Command {
cmd := &cobra.Command{
Use: "list",
Short: "List running workflows",
Long: "List running workflows",
Example: "vela workflow list",
RunE: func(cmd *cobra.Command, args []string) error {
cli, err := c.GetClient()
if err != nil {
return err
}
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
if err != nil {
return err
}
if AllNamespace {
namespace = ""
}
ctx := context.Background()
return printWorkflowList(ctx, cli, namespace, ioStream)
},
}
cmd.Flags().BoolVarP(&AllNamespace, "all-namespaces", "A", false, "If true, check the specified action in all namespaces.")
addNamespaceAndEnvArg(cmd)
return cmd
}
func printWorkflowList(ctx context.Context, c client.Reader, namespace string, ioStream cmdutil.IOStreams) error {
table, err := buildWorkflowListTable(ctx, c, namespace)
if err != nil {
return err
}
ioStream.Info(table.String())
return nil
}
func buildWorkflowListTable(ctx context.Context, c client.Reader, namespace string) (*uitable.Table, error) {
table := newUITable()
header := []interface{}{"NAME", "TYPE", "PHASE", "START-TIME", "END-TIME"}
if AllNamespace {
header = append([]interface{}{"NAMESPACE"}, header...)
}
table.AddRow(header...)
applist := v1beta1.ApplicationList{}
if err := c.List(ctx, &applist, client.InNamespace(namespace)); err != nil {
return nil, errors.WithMessage(err, "unable to list application workflows")
}
for _, a := range applist.Items {
status := a.Status.Workflow
if a.Status.Workflow != nil {
if AllNamespace {
table.AddRow(a.Namespace, a.Name, "Application", status.Phase, status.StartTime, status.EndTime)
} else {
table.AddRow(a.Name, "Application", status.Phase, status.StartTime, status.EndTime)
}
}
}
wrList := workflowv1alpha1.WorkflowRunList{}
if err := c.List(ctx, &wrList, client.InNamespace(namespace)); err != nil {
return nil, errors.WithMessage(err, "unable to list workflowruns")
}
for _, w := range wrList.Items {
status := w.Status
if status.Phase != "" {
if AllNamespace {
table.AddRow(w.Namespace, w.Name, "WorkflowRun", status.Phase, status.StartTime, status.EndTime)
} else {
table.AddRow(w.Name, "WorkflowRun", status.Phase, status.StartTime, status.EndTime)
}
}
}
return table, nil
}
// WorkflowArgs is the args for workflow command
type WorkflowArgs struct {
Type string
Output string
ControllerLabels map[string]string
Operator wfUtils.WorkflowOperator
StepOperator wfUtils.WorkflowStepOperator
Writer io.Writer
Args common.Args
StepName string
StepID string
ErrMap map[string]string
App *v1beta1.Application
WorkflowRun *workflowv1alpha1.WorkflowRun
WorkflowInstance *wfTypes.WorkflowInstance
}
const (
instanceTypeApplication string = "app"
instanceTypeWorkflowRun string = "workflow"
)
func (w *WorkflowArgs) getWorkflowInstance(ctx context.Context, cmd *cobra.Command, args []string) error {
if len(args) < 1 {
return fmt.Errorf("please specify the name of application/workflow")
}
name := args[0]
namespace, err := GetFlagNamespaceOrEnv(cmd, w.Args)
if err != nil {
return err
}
cli, err := w.Args.GetClient()
if err != nil {
return err
}
config, err := w.Args.GetConfig()
if err != nil {
return err
}
config.Wrap(pkgmulticluster.NewTransportWrapper())
switch w.Type {
case "":
app := &v1beta1.Application{}
if err := cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, app); err == nil {
w.Type = instanceTypeApplication
w.App = app
} else {
wr := &workflowv1alpha1.WorkflowRun{}
if err := cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, wr); err == nil {
w.Type = instanceTypeWorkflowRun
w.WorkflowRun = wr
}
}
if w.Type == "" {
return fmt.Errorf("can't find application or workflowrun %s", name)
}
case instanceTypeApplication:
app := &v1beta1.Application{}
if err := cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, app); err != nil {
return err
}
w.App = app
case instanceTypeWorkflowRun:
wr := &workflowv1alpha1.WorkflowRun{}
if err := cli.Get(ctx, client.ObjectKey{Namespace: namespace, Name: name}, wr); err != nil {
return err
}
w.WorkflowRun = wr
default:
}
return w.generateWorkflowInstance(ctx, cli)
}
func (w *WorkflowArgs) generateWorkflowInstance(ctx context.Context, cli client.Client) error {
debug := false
switch w.Type {
case instanceTypeApplication:
if w.App.Status.Workflow == nil {
return fmt.Errorf("the workflow in application %s is not start", w.App.Name)
}
for _, policy := range w.App.Spec.Policies {
if policy.Type == v1alpha1.DebugPolicyType {
debug = true
break
}
}
status := w.App.Status.Workflow
w.WorkflowInstance = &wfTypes.WorkflowInstance{
WorkflowMeta: wfTypes.WorkflowMeta{
Name: w.App.Name,
Namespace: w.App.Namespace,
UID: w.App.UID,
},
Debug: debug,
Status: workflowv1alpha1.WorkflowRunStatus{
Phase: status.Phase,
Message: status.Message,
Suspend: status.Suspend,
SuspendState: status.SuspendState,
Terminated: status.Terminated,
Finished: status.Finished,
ContextBackend: status.ContextBackend,
Steps: status.Steps,
StartTime: status.StartTime,
EndTime: status.EndTime,
},
}
if w.App.Spec.Workflow != nil {
w.WorkflowInstance.Steps = w.App.Spec.Workflow.Steps
}
w.Operator = operation.NewApplicationWorkflowOperator(cli, w.Writer, w.App)
w.StepOperator = operation.NewApplicationWorkflowStepOperator(cli, w.Writer, w.App)
w.ControllerLabels = map[string]string{"app.kubernetes.io/name": "vela-core"}
case instanceTypeWorkflowRun:
var steps []workflowv1alpha1.WorkflowStep
if w.WorkflowRun.Spec.WorkflowRef != "" {
workflow := &workflowv1alpha1.Workflow{}
if err := cli.Get(ctx, client.ObjectKey{Namespace: w.WorkflowRun.Namespace, Name: w.WorkflowRun.Spec.WorkflowRef}, workflow); err != nil {
return err
}
steps = workflow.Steps
} else {
steps = w.WorkflowRun.Spec.WorkflowSpec.Steps
}
if w.WorkflowRun.Annotations != nil {
if d, ok := w.WorkflowRun.Annotations[wfTypes.AnnotationWorkflowRunDebug]; ok && d == "true" {
debug = true
}
}
w.WorkflowInstance = &wfTypes.WorkflowInstance{
WorkflowMeta: wfTypes.WorkflowMeta{
Name: w.WorkflowRun.Name,
Namespace: w.WorkflowRun.Namespace,
UID: w.WorkflowRun.UID,
},
Steps: steps,
Status: w.WorkflowRun.Status,
Debug: debug,
}
w.Operator = wfUtils.NewWorkflowRunOperator(cli, w.Writer, w.WorkflowRun)
w.StepOperator = wfUtils.NewWorkflowRunStepOperator(cli, w.Writer, w.WorkflowRun)
w.ControllerLabels = map[string]string{"app.kubernetes.io/name": "vela-workflow"}
default:
return fmt.Errorf("unknown workflow instance type %s", w.Type)
}
return nil
}
func (w *WorkflowArgs) printStepLogs(ctx context.Context, cli client.Client, ioStreams cmdutil.IOStreams) error {
if w.StepName == "" {
if err := w.selectWorkflowStep("Select a step to show logs:"); err != nil {
return err
}
}
if w.WorkflowInstance.Status.ContextBackend == nil {
return fmt.Errorf("the workflow context backend is not set")
}
logConfig, err := wfUtils.GetLogConfigFromStep(ctx, w.WorkflowInstance.Status.ContextBackend.Name, w.WorkflowInstance.Name, w.WorkflowInstance.Namespace, w.StepName)
if err != nil {
return errors.WithMessage(err, fmt.Sprintf("step [%s]", w.StepName))
}
if err := selectStepLogSource(logConfig); err != nil {
return err
}
switch {
case logConfig.Data:
return w.printResourceLogs(ctx, cli, ioStreams, []wfTypes.Resource{{
Namespace: types.DefaultKubeVelaNS,
LabelSelector: w.ControllerLabels,
}}, []string{fmt.Sprintf(`stepSessionID="%s"`, w.StepID), fmt.Sprintf("%s/%s", w.WorkflowInstance.Namespace, w.WorkflowInstance.Name), "cue logs"})
case logConfig.Source != nil:
if len(logConfig.Source.Resources) > 0 {
return w.printResourceLogs(ctx, cli, ioStreams, logConfig.Source.Resources, nil)
}
if logConfig.Source.URL != "" {
readCloser, err := wfUtils.GetLogsFromURL(ctx, logConfig.Source.URL)
if err != nil {
return err
}
//nolint:errcheck
defer readCloser.Close()
if _, err := io.Copy(ioStreams.Out, readCloser); err != nil {
return err
}
}
}
return nil
}
func (w *WorkflowArgs) getWorkflowSteps() []string {
if w.ErrMap == nil {
w.ErrMap = make(map[string]string)
}
stepsKey := make([]string, 0)
for _, step := range w.WorkflowInstance.Status.Steps {
stepsKey = append(stepsKey, wrapStepName(step.StepStatus))
if step.Phase == workflowv1alpha1.WorkflowStepPhaseFailed {
w.ErrMap[step.Name] = step.Message
}
for _, sub := range step.SubStepsStatus {
stepsKey = append(stepsKey, fmt.Sprintf(" %s", wrapStepName(sub)))
if sub.Phase == workflowv1alpha1.WorkflowStepPhaseFailed {
w.ErrMap[step.Name] = sub.Message
}
}
}
return stepsKey
}
func (w *WorkflowArgs) selectWorkflowStep(msg string) error {
stepsKey := w.getWorkflowSteps()
if len(stepsKey) == 0 {
return fmt.Errorf("workflow is not start")
}
prompt := &survey.Select{
Message: msg,
Options: stepsKey,
}
var stepName string
err := survey.AskOne(prompt, &stepName, survey.WithValidator(survey.Required))
if err != nil {
return fmt.Errorf("failed to select step %s: %w", unwrapStepName(w.StepName), err)
}
w.StepName = unwrapStepName(stepName)
w.StepID = unwrapStepID(stepName, w.WorkflowInstance)
return nil
}
func selectStepLogSource(logConfig *wfTypes.LogConfig) error {
var source string
if logConfig.Data && logConfig.Source != nil {
prompt := &survey.Select{
Message: "Select logs from data or source",
Options: []string{"data", "source"},
}
err := survey.AskOne(prompt, &source, survey.WithValidator(survey.Required))
if err != nil {
return fmt.Errorf("failed to select %s: %w", source, err)
}
if source != "data" {
logConfig.Data = false
}
}
return nil
}
func (w *WorkflowArgs) printResourceLogs(ctx context.Context, cli client.Client, ioStreams cmdutil.IOStreams, resources []wfTypes.Resource, filters []string) error {
pods, err := wfUtils.GetPodListFromResources(ctx, cli, resources)
if err != nil {
return err
}
podList := make([]querytypes.PodBase, 0)
for _, pod := range pods {
podBase := querytypes.PodBase{}
podBase.Metadata.Name = pod.Name
podBase.Metadata.Namespace = pod.Namespace
podList = append(podList, podBase)
}
if len(pods) == 0 {
return errors.New("no pod found")
}
var selectPod *querytypes.PodBase
if len(pods) > 1 {
selectPod, err = AskToChooseOnePod(podList)
if err != nil {
return err
}
} else {
// nolint:gosec
selectPod = &podList[0]
}
l := Args{
Args: w.Args,
Output: w.Output,
}
return l.printPodLogs(ctx, ioStreams, selectPod, filters)
}
func (w *WorkflowArgs) checkWorkflowNotComplete() func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, args []string) {
if err := w.getWorkflowInstance(context.Background(), cmd, args); err != nil {
return
}
if w.WorkflowInstance.Status.Phase == workflowv1alpha1.WorkflowStateSucceeded {
cmd.Printf("%s workflow not allowed because application %s is running\n", cmd.Use, args[0])
os.Exit(1)
}
}
}
func (w *WorkflowArgs) checkDebugMode() func(cmd *cobra.Command, args []string) {
return func(cmd *cobra.Command, args []string) {
if err := w.getWorkflowInstance(context.Background(), cmd, args); err != nil {
return
}
if !w.WorkflowInstance.Debug {
msg := ""
if w.Type == instanceTypeApplication {
msg = "please make sure your application have the debug policy, you can add the debug policy by using `vela up -f <app.yaml> --debug"
} else {
msg = "please make sure your workflow have the debug annotation [workflowrun.oam.dev/debug:true] then re-run the workflow"
}
cmd.Printf("workflow %s is not in debug mode, %s\n", w.WorkflowInstance.Name, msg)
os.Exit(1)
}
}
}