github.com/oam-dev/kubevela@v1.9.11/references/cli/debug.go (about)

     1  /*
     2  Copyright 2021 The KubeVela Authors.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package cli
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"strings"
    23  
    24  	"cuelang.org/go/cue"
    25  	"github.com/AlecAivazis/survey/v2"
    26  	"github.com/FogDong/uitable"
    27  	"github.com/fatih/color"
    28  	"github.com/pkg/errors"
    29  	"github.com/spf13/cobra"
    30  	corev1 "k8s.io/api/core/v1"
    31  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    32  	"sigs.k8s.io/controller-runtime/pkg/client"
    33  	"sigs.k8s.io/yaml"
    34  
    35  	workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
    36  	"github.com/kubevela/workflow/pkg/cue/model/sets"
    37  	"github.com/kubevela/workflow/pkg/cue/model/value"
    38  	"github.com/kubevela/workflow/pkg/cue/packages"
    39  	"github.com/kubevela/workflow/pkg/debug"
    40  	"github.com/kubevela/workflow/pkg/tasks/custom"
    41  	wfTypes "github.com/kubevela/workflow/pkg/types"
    42  
    43  	"github.com/oam-dev/kubevela/apis/types"
    44  	"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
    45  	"github.com/oam-dev/kubevela/pkg/utils/common"
    46  	cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
    47  )
    48  
    49  type debugOpts struct {
    50  	step   string
    51  	focus  string
    52  	errMsg string
    53  	opts   []string
    54  	errMap map[string]string
    55  	// TODO: (fog) add watch flag
    56  	// watch bool
    57  }
    58  
    59  // NewDebugCommand create `debug` command
    60  func NewDebugCommand(c common.Args, order string, ioStreams cmdutil.IOStreams) *cobra.Command {
    61  	ctx := context.Background()
    62  	dOpts := &debugOpts{}
    63  	wargs := &WorkflowArgs{
    64  		Args:   c,
    65  		Writer: ioStreams.Out,
    66  	}
    67  	cmd := &cobra.Command{
    68  		Use:     "debug",
    69  		Aliases: []string{"debug"},
    70  		Short:   "Debug running application.",
    71  		Long:    "Debug running application with debug policy.",
    72  		Example: `vela debug <application-name>`,
    73  		Annotations: map[string]string{
    74  			types.TagCommandType:  types.TypeApp,
    75  			types.TagCommandOrder: order,
    76  		},
    77  		PreRun: wargs.checkDebugMode(),
    78  		RunE: func(cmd *cobra.Command, args []string) error {
    79  			if len(args) < 1 {
    80  				return fmt.Errorf("must specify application name")
    81  			}
    82  			if err := wargs.getWorkflowInstance(ctx, cmd, args); err != nil {
    83  				return err
    84  			}
    85  			if wargs.Type == instanceTypeWorkflowRun {
    86  				return fmt.Errorf("please use `vela workflow debug <name>` instead")
    87  			}
    88  			if wargs.App == nil {
    89  				return fmt.Errorf("application %s not found", args[0])
    90  			}
    91  
    92  			return dOpts.debugApplication(ctx, wargs, c, ioStreams)
    93  		},
    94  	}
    95  	addNamespaceAndEnvArg(cmd)
    96  	cmd.Flags().StringVarP(&dOpts.step, "step", "s", "", "specify the step or component to debug")
    97  	cmd.Flags().StringVarP(&dOpts.focus, "focus", "f", "", "specify the focus value to debug, only valid for application with workflow")
    98  	return cmd
    99  }
   100  
   101  func (d *debugOpts) debugApplication(ctx context.Context, wargs *WorkflowArgs, c common.Args, ioStreams cmdutil.IOStreams) error {
   102  	app := wargs.App
   103  	cli, err := c.GetClient()
   104  	if err != nil {
   105  		return err
   106  	}
   107  	config, err := c.GetConfig()
   108  	if err != nil {
   109  		return err
   110  	}
   111  	pd, err := c.GetPackageDiscover()
   112  	if err != nil {
   113  		return err
   114  	}
   115  	d.opts = wargs.getWorkflowSteps()
   116  	d.errMap = wargs.ErrMap
   117  	if app.Spec.Workflow != nil && len(app.Spec.Workflow.Steps) > 0 {
   118  		return d.debugWorkflow(ctx, wargs, cli, pd, ioStreams)
   119  	}
   120  
   121  	dryRunOpt := dryrun.NewDryRunOption(cli, config, pd, []*unstructured.Unstructured{}, false)
   122  	comps, _, err := dryRunOpt.ExecuteDryRun(ctx, app)
   123  	if err != nil {
   124  		ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
   125  		return nil
   126  	}
   127  	if err := d.debugComponents(comps, ioStreams); err != nil {
   128  		return err
   129  	}
   130  	return nil
   131  }
   132  
   133  func (d *debugOpts) debugWorkflow(ctx context.Context, wargs *WorkflowArgs, cli client.Client, pd *packages.PackageDiscover, ioStreams cmdutil.IOStreams) error {
   134  	if d.step == "" {
   135  		prompt := &survey.Select{
   136  			Message: "Select the workflow step to debug:",
   137  			Options: d.opts,
   138  		}
   139  		var step string
   140  		err := survey.AskOne(prompt, &step, survey.WithValidator(survey.Required))
   141  		if err != nil {
   142  			return fmt.Errorf("failed to select workflow step: %w", err)
   143  		}
   144  		d.step = unwrapStepID(step, wargs.WorkflowInstance)
   145  		d.errMsg = d.errMap[d.step]
   146  	} else {
   147  		d.step = unwrapStepID(d.step, wargs.WorkflowInstance)
   148  	}
   149  
   150  	// debug workflow steps
   151  	rawValue, data, err := d.getDebugRawValue(ctx, cli, pd, wargs.WorkflowInstance)
   152  	if err != nil {
   153  		if data != "" {
   154  			ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
   155  			ioStreams.Info(color.GreenString("Original Data in Debug:\n"), data)
   156  			return nil
   157  		}
   158  		return err
   159  	}
   160  
   161  	if err := d.handleCueSteps(rawValue, ioStreams); err != nil {
   162  		ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
   163  		ioStreams.Info(color.GreenString("Original Data in Debug:\n"), data)
   164  		return nil
   165  	}
   166  	return nil
   167  }
   168  
   169  func (d *debugOpts) debugComponents(comps []*types.ComponentManifest, ioStreams cmdutil.IOStreams) error {
   170  	opts := d.opts
   171  	all := color.YellowString("all fields")
   172  	exit := color.CyanString("exit debug mode")
   173  	opts = append(opts, all, exit)
   174  
   175  	var components = make(map[string]*unstructured.Unstructured)
   176  	var traits = make(map[string][]*unstructured.Unstructured)
   177  	for _, comp := range comps {
   178  		components[comp.Name] = comp.ComponentOutput
   179  		traits[comp.Name] = comp.ComponentOutputsAndTraits
   180  	}
   181  
   182  	if d.step != "" {
   183  		return renderComponents(d.step, components[d.step], traits[d.step], ioStreams)
   184  	}
   185  	for {
   186  		prompt := &survey.Select{
   187  			Message: "Select the components to debug:",
   188  			Options: opts,
   189  		}
   190  		var step string
   191  		err := survey.AskOne(prompt, &step, survey.WithValidator(survey.Required))
   192  		if err != nil {
   193  			return fmt.Errorf("failed to select components: %w", err)
   194  		}
   195  
   196  		if step == exit {
   197  			break
   198  		}
   199  		if step == all {
   200  			for _, step := range d.opts {
   201  				step = unwrapStepName(step)
   202  				if err := renderComponents(step, components[step], traits[step], ioStreams); err != nil {
   203  					return err
   204  				}
   205  			}
   206  			continue
   207  		}
   208  		step = unwrapStepName(step)
   209  		if err := renderComponents(step, components[step], traits[step], ioStreams); err != nil {
   210  			return err
   211  		}
   212  	}
   213  	return nil
   214  }
   215  
   216  func renderComponents(compName string, comp *unstructured.Unstructured, traits []*unstructured.Unstructured, ioStreams cmdutil.IOStreams) error {
   217  	ioStreams.Info(color.CyanString("\n▫️ %s", compName))
   218  	result, err := yaml.Marshal(comp)
   219  	if err != nil {
   220  		return errors.WithMessage(err, "marshal result for component "+compName+" object in yaml format")
   221  	}
   222  	ioStreams.Info(string(result), "\n")
   223  	for _, t := range traits {
   224  		result, err := yaml.Marshal(t)
   225  		if err != nil {
   226  			return errors.WithMessage(err, "marshal result for component "+compName+" object in yaml format")
   227  		}
   228  		ioStreams.Info(string(result), "\n")
   229  	}
   230  	return nil
   231  }
   232  
   233  func wrapStepName(step workflowv1alpha1.StepStatus) string {
   234  	var stepName string
   235  	switch step.Phase {
   236  	case workflowv1alpha1.WorkflowStepPhaseSucceeded:
   237  		stepName = emojiSucceed + step.Name
   238  	case workflowv1alpha1.WorkflowStepPhaseFailed:
   239  		stepName = emojiFail + step.Name
   240  	case workflowv1alpha1.WorkflowStepPhaseSkipped:
   241  		stepName = emojiSkip + step.Name
   242  	default:
   243  		stepName = emojiExecuting + step.Name
   244  	}
   245  	return stepName
   246  }
   247  
   248  func unwrapStepName(step string) string {
   249  	step = strings.TrimPrefix(step, "  ")
   250  	switch {
   251  	case strings.HasPrefix(step, emojiSucceed):
   252  		return strings.TrimPrefix(step, emojiSucceed)
   253  	case strings.HasPrefix(step, emojiFail):
   254  		return strings.TrimPrefix(step, emojiFail)
   255  	case strings.HasPrefix(step, emojiSkip):
   256  		return strings.TrimPrefix(step, emojiSkip)
   257  	case strings.HasPrefix(step, emojiExecuting):
   258  		return strings.TrimPrefix(step, emojiExecuting)
   259  	default:
   260  		return step
   261  	}
   262  }
   263  
   264  func unwrapStepID(step string, instance *wfTypes.WorkflowInstance) string {
   265  	step = unwrapStepName(step)
   266  	for _, status := range instance.Status.Steps {
   267  		if status.Name == step {
   268  			return status.ID
   269  		}
   270  		for _, sub := range status.SubStepsStatus {
   271  			if sub.Name == step {
   272  				return sub.ID
   273  			}
   274  		}
   275  	}
   276  	return step
   277  }
   278  
   279  func (d *debugOpts) getDebugRawValue(ctx context.Context, cli client.Client, pd *packages.PackageDiscover, instance *wfTypes.WorkflowInstance) (*value.Value, string, error) {
   280  	debugCM := &corev1.ConfigMap{}
   281  	if err := cli.Get(ctx, client.ObjectKey{Name: debug.GenerateContextName(instance.Name, d.step, string(instance.UID)), Namespace: instance.Namespace}, debugCM); err != nil {
   282  		for _, step := range instance.Status.Steps {
   283  			if step.Name == d.step && (step.Type == wfTypes.WorkflowStepTypeSuspend || step.Type == wfTypes.WorkflowStepTypeStepGroup) {
   284  				return nil, "", fmt.Errorf("no debug data for a suspend or step-group step, please choose another step")
   285  			}
   286  			for _, sub := range step.SubStepsStatus {
   287  				if sub.Name == d.step && sub.Type == wfTypes.WorkflowStepTypeSuspend {
   288  					return nil, "", fmt.Errorf("no debug data for a suspend step, please choose another step")
   289  				}
   290  			}
   291  		}
   292  		return nil, "", fmt.Errorf("failed to get debug configmap, please make sure the you're in the debug mode`: %w", err)
   293  	}
   294  
   295  	if debugCM.Data == nil || debugCM.Data["debug"] == "" {
   296  		return nil, "", fmt.Errorf("debug configmap is empty")
   297  	}
   298  	v, err := value.NewValue(debugCM.Data["debug"], pd, "")
   299  	if err != nil {
   300  		return nil, debugCM.Data["debug"], fmt.Errorf("failed to parse debug configmap: %w", err)
   301  	}
   302  	return v, debugCM.Data["debug"], nil
   303  }
   304  
   305  func (d *debugOpts) handleCueSteps(v *value.Value, ioStreams cmdutil.IOStreams) error {
   306  	if d.focus != "" {
   307  		f, err := v.LookupValue(strings.Split(d.focus, ".")...)
   308  		if err != nil {
   309  			return err
   310  		}
   311  		ioStreams.Info(color.New(color.FgCyan).Sprint("\n", d.focus, "\n"))
   312  		rendered, err := renderFields(f, &renderOptions{})
   313  		if err != nil {
   314  			return err
   315  		}
   316  		ioStreams.Info(rendered, "\n")
   317  		return nil
   318  	}
   319  
   320  	if err := d.separateBySteps(v, ioStreams); err != nil {
   321  		return err
   322  	}
   323  	return nil
   324  }
   325  
   326  func (d *debugOpts) separateBySteps(v *value.Value, ioStreams cmdutil.IOStreams) error {
   327  	fieldMap := make(map[string]*value.Value)
   328  	fieldList := make([]string, 0)
   329  	if err := v.StepByFields(func(fieldName string, in *value.Value) (bool, error) {
   330  		if in.CueValue().IncompleteKind() == cue.BottomKind {
   331  			errInfo, err := sets.ToString(in.CueValue())
   332  			if err != nil {
   333  				errInfo = "value is _|_"
   334  			}
   335  			return true, errors.New(errInfo + "value is _|_ (bottom kind)")
   336  		}
   337  		fieldList = append(fieldList, fieldName)
   338  		fieldMap[fieldName] = in
   339  		return false, nil
   340  	}); err != nil {
   341  		return fmt.Errorf("failed to parse debug configmap by field: %w", err)
   342  	}
   343  
   344  	errStep := ""
   345  	if d.errMsg != "" {
   346  		s := strings.Split(d.errMsg, ":")
   347  		errStep = strings.TrimPrefix(s[0], "step ")
   348  	}
   349  	opts := make([]string, 0)
   350  	for _, field := range fieldList {
   351  		if field == errStep {
   352  			opts = append(opts, emojiFail+field)
   353  		} else {
   354  			opts = append(opts, emojiSucceed+field)
   355  		}
   356  	}
   357  	all := color.YellowString("all fields")
   358  	exit := color.CyanString("exit debug mode")
   359  	opts = append(opts, all, exit)
   360  	for {
   361  		prompt := &survey.Select{
   362  			Message: "Select the field to debug: ",
   363  			Options: opts,
   364  		}
   365  		var field string
   366  		err := survey.AskOne(prompt, &field, survey.WithValidator(survey.Required))
   367  		if err != nil {
   368  			return fmt.Errorf("failed to select: %w", err)
   369  		}
   370  		if field == exit {
   371  			break
   372  		}
   373  		if field == all {
   374  			for _, field := range fieldList {
   375  				ioStreams.Info(color.CyanString("\n▫️ %s", field))
   376  				rendered, err := renderFields(fieldMap[field], &renderOptions{})
   377  				if err != nil {
   378  					return err
   379  				}
   380  				ioStreams.Info(rendered, "\n")
   381  			}
   382  			continue
   383  		}
   384  		field = unwrapStepName(field)
   385  		ioStreams.Info(color.CyanString("\n▫️ %s", field))
   386  		rendered, err := renderFields(fieldMap[field], &renderOptions{})
   387  		if err != nil {
   388  			return err
   389  		}
   390  		ioStreams.Info(rendered, "\n")
   391  	}
   392  	return nil
   393  }
   394  
   395  type renderOptions struct {
   396  	hideIndex    bool
   397  	filterFields []string
   398  }
   399  
   400  func renderFields(v *value.Value, opt *renderOptions) (string, error) {
   401  	table := uitable.New()
   402  	table.MaxColWidth = 200
   403  	table.Wrap = true
   404  	i := 0
   405  
   406  	if err := v.StepByFields(func(fieldName string, in *value.Value) (bool, error) {
   407  		key := ""
   408  		if custom.OpTpy(in) != "" {
   409  			rendered, err := renderFields(in, opt)
   410  			if err != nil {
   411  				return false, err
   412  			}
   413  			i++
   414  			if !opt.hideIndex {
   415  				key += fmt.Sprintf("%v.", i)
   416  			}
   417  			key += fieldName
   418  			if !strings.Contains(fieldName, "#") {
   419  				if err := v.FillObject(in, fieldName); err != nil {
   420  					renderValuesInRow(table, key, rendered, false)
   421  					return false, err
   422  				}
   423  			}
   424  			if len(opt.filterFields) > 0 {
   425  				for _, filter := range opt.filterFields {
   426  					if filter != fieldName {
   427  						renderValuesInRow(table, key, rendered, true)
   428  					}
   429  				}
   430  			} else {
   431  				renderValuesInRow(table, key, rendered, true)
   432  			}
   433  			return false, nil
   434  		}
   435  
   436  		vStr, err := in.String()
   437  		if err != nil {
   438  			return false, err
   439  		}
   440  		i++
   441  		if !opt.hideIndex {
   442  			key += fmt.Sprintf("%v.", i)
   443  		}
   444  		key += fieldName
   445  		if !strings.Contains(fieldName, "#") {
   446  			if err := v.FillObject(in, fieldName); err != nil {
   447  				renderValuesInRow(table, key, vStr, false)
   448  				return false, err
   449  			}
   450  		}
   451  
   452  		if len(opt.filterFields) > 0 {
   453  			for _, filter := range opt.filterFields {
   454  				if filter != fieldName {
   455  					renderValuesInRow(table, key, vStr, true)
   456  				}
   457  			}
   458  		} else {
   459  			renderValuesInRow(table, key, vStr, true)
   460  		}
   461  		return false, nil
   462  	}); err != nil {
   463  		vStr, serr := v.String()
   464  		if serr != nil {
   465  			return "", serr
   466  		}
   467  		if strings.Contains(err.Error(), "(type string) as struct") {
   468  			return strings.TrimSpace(vStr), nil
   469  		}
   470  	}
   471  
   472  	return table.String(), nil
   473  }
   474  
   475  func renderValuesInRow(table *uitable.Table, k, v string, isPass bool) {
   476  	v = strings.TrimSpace(v)
   477  	if isPass {
   478  		if strings.Contains(k, "#do") || strings.Contains(k, "#provider") {
   479  			k = color.YellowString("%s:", k)
   480  		} else {
   481  			k = color.GreenString("%s:", k)
   482  		}
   483  	} else {
   484  		k = color.RedString("%s:", k)
   485  		v = color.RedString("%s%s", emojiFail, v)
   486  	}
   487  	if v == `"steps"` {
   488  		v = color.BlueString(v)
   489  	}
   490  
   491  	table.AddRow(k, v)
   492  }