mirror of
https://github.com/kubevela/kubevela.git
synced 2026-02-14 18:10:21 +00:00
Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com> Signed-off-by: FogDong <dongtianxin.tx@alibaba-inc.com>
482 lines
13 KiB
Go
482 lines
13 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"
|
||
"strings"
|
||
|
||
"cuelang.org/go/cue"
|
||
"github.com/AlecAivazis/survey/v2"
|
||
"github.com/FogDong/uitable"
|
||
"github.com/fatih/color"
|
||
"github.com/pkg/errors"
|
||
"github.com/spf13/cobra"
|
||
corev1 "k8s.io/api/core/v1"
|
||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||
"sigs.k8s.io/yaml"
|
||
|
||
workflowv1alpha1 "github.com/kubevela/workflow/api/v1alpha1"
|
||
"github.com/kubevela/workflow/pkg/cue/model/sets"
|
||
"github.com/kubevela/workflow/pkg/cue/model/value"
|
||
"github.com/kubevela/workflow/pkg/cue/packages"
|
||
"github.com/kubevela/workflow/pkg/debug"
|
||
"github.com/kubevela/workflow/pkg/tasks/custom"
|
||
|
||
"github.com/oam-dev/kubevela/apis/core.oam.dev/v1beta1"
|
||
"github.com/oam-dev/kubevela/apis/types"
|
||
"github.com/oam-dev/kubevela/pkg/appfile/dryrun"
|
||
"github.com/oam-dev/kubevela/pkg/oam"
|
||
"github.com/oam-dev/kubevela/pkg/oam/discoverymapper"
|
||
"github.com/oam-dev/kubevela/pkg/utils/common"
|
||
cmdutil "github.com/oam-dev/kubevela/pkg/utils/util"
|
||
"github.com/oam-dev/kubevela/references/appfile"
|
||
)
|
||
|
||
type debugOpts struct {
|
||
step string
|
||
focus string
|
||
errMsg string
|
||
// TODO: (fog) add watch flag
|
||
// watch bool
|
||
}
|
||
|
||
// NewDebugCommand create `debug` command
|
||
func NewDebugCommand(c common.Args, ioStreams cmdutil.IOStreams) *cobra.Command {
|
||
ctx := context.Background()
|
||
dOpts := &debugOpts{}
|
||
cmd := &cobra.Command{
|
||
Use: "debug",
|
||
Aliases: []string{"debug"},
|
||
Short: "Debug running application",
|
||
Long: "Debug running application with debug policy.",
|
||
Example: `vela debug <application-name>`,
|
||
RunE: func(cmd *cobra.Command, args []string) error {
|
||
if len(args) < 1 {
|
||
return fmt.Errorf("must specify application name")
|
||
}
|
||
namespace, err := GetFlagNamespaceOrEnv(cmd, c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
app, err := appfile.LoadApplication(namespace, args[0], c)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
return dOpts.debugApplication(ctx, c, app, ioStreams)
|
||
},
|
||
}
|
||
addNamespaceAndEnvArg(cmd)
|
||
cmd.Flags().StringVarP(&dOpts.step, "step", "s", "", "specify the step or component to debug")
|
||
cmd.Flags().StringVarP(&dOpts.focus, "focus", "f", "", "specify the focus value to debug, only valid for application with workflow")
|
||
return cmd
|
||
}
|
||
|
||
func (d *debugOpts) debugApplication(ctx context.Context, c common.Args, app *v1beta1.Application, ioStreams cmdutil.IOStreams) error {
|
||
cli, err := c.GetClient()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
config, err := c.GetConfig()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
pd, err := c.GetPackageDiscover()
|
||
if err != nil {
|
||
return err
|
||
}
|
||
|
||
s, opts, errMap := d.getDebugOptions(app)
|
||
if s == "workflow steps" {
|
||
if d.step == "" {
|
||
prompt := &survey.Select{
|
||
Message: fmt.Sprintf("Select the %s to debug:", s),
|
||
Options: opts,
|
||
}
|
||
var step string
|
||
err := survey.AskOne(prompt, &step, survey.WithValidator(survey.Required))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to select %s: %w", s, err)
|
||
}
|
||
d.step = unwrapStepName(step)
|
||
d.errMsg = errMap[d.step]
|
||
}
|
||
|
||
// debug workflow steps
|
||
rawValue, data, err := d.getDebugRawValue(ctx, cli, pd, app)
|
||
if err != nil {
|
||
if data != "" {
|
||
ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
|
||
ioStreams.Info(color.GreenString("Original Data in Debug:\n"), data)
|
||
return nil
|
||
}
|
||
return err
|
||
}
|
||
|
||
if err := d.handleCueSteps(rawValue, ioStreams); err != nil {
|
||
ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
|
||
ioStreams.Info(color.GreenString("Original Data in Debug:\n"), data)
|
||
return nil
|
||
}
|
||
} else {
|
||
// dry run components
|
||
dm, err := discoverymapper.New(config)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
dryRunOpt := dryrun.NewDryRunOption(cli, config, dm, pd, []oam.Object{}, false)
|
||
comps, _, err := dryRunOpt.ExecuteDryRun(ctx, app)
|
||
if err != nil {
|
||
ioStreams.Info(color.RedString("%s%s", emojiFail, err.Error()))
|
||
return nil
|
||
}
|
||
if err := d.debugComponents(opts, comps, ioStreams); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (d *debugOpts) debugComponents(compList []string, comps []*types.ComponentManifest, ioStreams cmdutil.IOStreams) error {
|
||
opts := compList
|
||
all := color.YellowString("all fields")
|
||
exit := color.CyanString("exit debug mode")
|
||
opts = append(opts, all, exit)
|
||
|
||
var components = make(map[string]*unstructured.Unstructured)
|
||
var traits = make(map[string][]*unstructured.Unstructured)
|
||
for _, comp := range comps {
|
||
components[comp.Name] = comp.StandardWorkload
|
||
traits[comp.Name] = comp.Traits
|
||
}
|
||
|
||
if d.step != "" {
|
||
return renderComponents(d.step, components[d.step], traits[d.step], ioStreams)
|
||
}
|
||
for {
|
||
prompt := &survey.Select{
|
||
Message: "Select the components to debug:",
|
||
Options: opts,
|
||
}
|
||
var step string
|
||
err := survey.AskOne(prompt, &step, survey.WithValidator(survey.Required))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to select components: %w", err)
|
||
}
|
||
|
||
if step == exit {
|
||
break
|
||
}
|
||
if step == all {
|
||
for _, step := range compList {
|
||
step = unwrapStepName(step)
|
||
if err := renderComponents(step, components[step], traits[step], ioStreams); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
continue
|
||
}
|
||
step = unwrapStepName(step)
|
||
if err := renderComponents(step, components[step], traits[step], ioStreams); err != nil {
|
||
return err
|
||
}
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func renderComponents(compName string, comp *unstructured.Unstructured, traits []*unstructured.Unstructured, ioStreams cmdutil.IOStreams) error {
|
||
ioStreams.Info(color.CyanString("\n▫️ %s", compName))
|
||
result, err := yaml.Marshal(comp)
|
||
if err != nil {
|
||
return errors.WithMessage(err, "marshal result for component "+compName+" object in yaml format")
|
||
}
|
||
ioStreams.Info(string(result), "\n")
|
||
for _, t := range traits {
|
||
result, err := yaml.Marshal(t)
|
||
if err != nil {
|
||
return errors.WithMessage(err, "marshal result for component "+compName+" object in yaml format")
|
||
}
|
||
ioStreams.Info(string(result), "\n")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (d *debugOpts) getDebugOptions(app *v1beta1.Application) (string, []string, map[string]string) {
|
||
s := "components"
|
||
stepList := make([]string, 0)
|
||
if app.Spec.Workflow != nil && len(app.Spec.Workflow.Steps) > 0 {
|
||
s = "workflow steps"
|
||
}
|
||
errMap := make(map[string]string)
|
||
switch {
|
||
case app.Status.Workflow != nil:
|
||
for _, step := range app.Status.Workflow.Steps {
|
||
stepName := wrapStepName(step.StepStatus)
|
||
if strings.HasPrefix(stepName, emojiFail) {
|
||
errMap[step.Name] = step.Message
|
||
}
|
||
stepList = append(stepList, stepName)
|
||
}
|
||
case app.Spec.Workflow != nil && len(app.Spec.Workflow.Steps) > 0:
|
||
for _, step := range app.Spec.Workflow.Steps {
|
||
stepList = append(stepList, step.Name)
|
||
}
|
||
default:
|
||
for _, c := range app.Spec.Components {
|
||
stepList = append(stepList, c.Name)
|
||
}
|
||
}
|
||
return s, stepList, errMap
|
||
}
|
||
|
||
func wrapStepName(step workflowv1alpha1.StepStatus) string {
|
||
var stepName string
|
||
switch step.Phase {
|
||
case workflowv1alpha1.WorkflowStepPhaseSucceeded:
|
||
stepName = emojiSucceed + step.Name
|
||
case workflowv1alpha1.WorkflowStepPhaseFailed:
|
||
stepName = emojiFail + step.Name
|
||
case workflowv1alpha1.WorkflowStepPhaseSkipped:
|
||
stepName = emojiSkip + step.Name
|
||
default:
|
||
stepName = emojiExecuting + step.Name
|
||
}
|
||
return stepName
|
||
}
|
||
|
||
func unwrapStepName(step string) string {
|
||
switch {
|
||
case strings.HasPrefix(step, emojiSucceed):
|
||
return strings.TrimPrefix(step, emojiSucceed)
|
||
case strings.HasPrefix(step, emojiFail):
|
||
return strings.TrimPrefix(step, emojiFail)
|
||
case strings.HasPrefix(step, emojiSkip):
|
||
return strings.TrimPrefix(step, emojiSkip)
|
||
case strings.HasPrefix(step, emojiExecuting):
|
||
return strings.TrimPrefix(step, emojiExecuting)
|
||
default:
|
||
return step
|
||
}
|
||
}
|
||
|
||
func (d *debugOpts) getDebugRawValue(ctx context.Context, cli client.Client, pd *packages.PackageDiscover, app *v1beta1.Application) (*value.Value, string, error) {
|
||
debugCM := &corev1.ConfigMap{}
|
||
if err := cli.Get(ctx, client.ObjectKey{Name: debug.GenerateContextName(app.Name, d.step), Namespace: app.Namespace}, debugCM); err != nil {
|
||
return nil, "", fmt.Errorf("failed to get debug configmap, please make sure your application have the debug policy, you can add the debug policy by using `vela up -f <app.yaml> --debug`: %w", err)
|
||
}
|
||
|
||
if debugCM.Data == nil || debugCM.Data["debug"] == "" {
|
||
return nil, "", fmt.Errorf("debug configmap is empty")
|
||
}
|
||
v, err := value.NewValue(debugCM.Data["debug"], pd, "")
|
||
if err != nil {
|
||
return nil, debugCM.Data["debug"], fmt.Errorf("failed to parse debug configmap: %w", err)
|
||
}
|
||
return v, debugCM.Data["debug"], nil
|
||
}
|
||
|
||
func (d *debugOpts) handleCueSteps(v *value.Value, ioStreams cmdutil.IOStreams) error {
|
||
if d.focus != "" {
|
||
f, err := v.LookupValue(strings.Split(d.focus, ".")...)
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ioStreams.Info(color.New(color.FgCyan).Sprint("\n", d.focus, "\n"))
|
||
rendered, err := renderFields(f, &renderOptions{})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ioStreams.Info(rendered, "\n")
|
||
return nil
|
||
}
|
||
|
||
if err := d.separateBySteps(v, ioStreams); err != nil {
|
||
return err
|
||
}
|
||
return nil
|
||
}
|
||
|
||
func (d *debugOpts) separateBySteps(v *value.Value, ioStreams cmdutil.IOStreams) error {
|
||
fieldMap := make(map[string]*value.Value)
|
||
fieldList := make([]string, 0)
|
||
if err := v.StepByFields(func(fieldName string, in *value.Value) (bool, error) {
|
||
if in.CueValue().IncompleteKind() == cue.BottomKind {
|
||
errInfo, err := sets.ToString(in.CueValue())
|
||
if err != nil {
|
||
errInfo = "value is _|_"
|
||
}
|
||
return true, errors.New(errInfo + "value is _|_ (bottom kind)")
|
||
}
|
||
fieldList = append(fieldList, fieldName)
|
||
fieldMap[fieldName] = in
|
||
return false, nil
|
||
}); err != nil {
|
||
return fmt.Errorf("failed to parse debug configmap by field: %w", err)
|
||
}
|
||
|
||
errStep := ""
|
||
if d.errMsg != "" {
|
||
s := strings.Split(d.errMsg, ":")
|
||
errStep = strings.TrimPrefix(s[0], "step ")
|
||
}
|
||
opts := make([]string, 0)
|
||
for _, field := range fieldList {
|
||
if field == errStep {
|
||
opts = append(opts, emojiFail+field)
|
||
} else {
|
||
opts = append(opts, emojiSucceed+field)
|
||
}
|
||
}
|
||
all := color.YellowString("all fields")
|
||
exit := color.CyanString("exit debug mode")
|
||
opts = append(opts, all, exit)
|
||
for {
|
||
prompt := &survey.Select{
|
||
Message: "Select the field to debug: ",
|
||
Options: opts,
|
||
}
|
||
var field string
|
||
err := survey.AskOne(prompt, &field, survey.WithValidator(survey.Required))
|
||
if err != nil {
|
||
return fmt.Errorf("failed to select: %w", err)
|
||
}
|
||
if field == exit {
|
||
break
|
||
}
|
||
if field == all {
|
||
for _, field := range fieldList {
|
||
ioStreams.Info(color.CyanString("\n▫️ %s", field))
|
||
rendered, err := renderFields(fieldMap[field], &renderOptions{})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ioStreams.Info(rendered, "\n")
|
||
}
|
||
continue
|
||
}
|
||
field = unwrapStepName(field)
|
||
ioStreams.Info(color.CyanString("\n▫️ %s", field))
|
||
rendered, err := renderFields(fieldMap[field], &renderOptions{})
|
||
if err != nil {
|
||
return err
|
||
}
|
||
ioStreams.Info(rendered, "\n")
|
||
}
|
||
return nil
|
||
}
|
||
|
||
type renderOptions struct {
|
||
hideIndex bool
|
||
filterFields []string
|
||
}
|
||
|
||
func renderFields(v *value.Value, opt *renderOptions) (string, error) {
|
||
table := uitable.New()
|
||
table.MaxColWidth = 200
|
||
table.Wrap = true
|
||
i := 0
|
||
|
||
if err := v.StepByFields(func(fieldName string, in *value.Value) (bool, error) {
|
||
key := ""
|
||
if custom.OpTpy(in) != "" {
|
||
rendered, err := renderFields(in, opt)
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
i++
|
||
if !opt.hideIndex {
|
||
key += fmt.Sprintf("%v.", i)
|
||
}
|
||
key += fieldName
|
||
if !strings.Contains(fieldName, "#") {
|
||
if err := v.FillObject(in, fieldName); err != nil {
|
||
renderValuesInRow(table, key, rendered, false)
|
||
return false, err
|
||
}
|
||
}
|
||
if len(opt.filterFields) > 0 {
|
||
for _, filter := range opt.filterFields {
|
||
if filter != fieldName {
|
||
renderValuesInRow(table, key, rendered, true)
|
||
}
|
||
}
|
||
} else {
|
||
renderValuesInRow(table, key, rendered, true)
|
||
}
|
||
return false, nil
|
||
}
|
||
|
||
vStr, err := in.String()
|
||
if err != nil {
|
||
return false, err
|
||
}
|
||
i++
|
||
if !opt.hideIndex {
|
||
key += fmt.Sprintf("%v.", i)
|
||
}
|
||
key += fieldName
|
||
if !strings.Contains(fieldName, "#") {
|
||
if err := v.FillObject(in, fieldName); err != nil {
|
||
renderValuesInRow(table, key, vStr, false)
|
||
return false, err
|
||
}
|
||
}
|
||
|
||
if len(opt.filterFields) > 0 {
|
||
for _, filter := range opt.filterFields {
|
||
if filter != fieldName {
|
||
renderValuesInRow(table, key, vStr, true)
|
||
}
|
||
}
|
||
} else {
|
||
renderValuesInRow(table, key, vStr, true)
|
||
}
|
||
return false, nil
|
||
}); err != nil {
|
||
vStr, serr := v.String()
|
||
if serr != nil {
|
||
return "", serr
|
||
}
|
||
if strings.Contains(err.Error(), "(type string) as struct") {
|
||
return strings.TrimSpace(vStr), nil
|
||
}
|
||
}
|
||
|
||
return table.String(), nil
|
||
}
|
||
|
||
func renderValuesInRow(table *uitable.Table, k, v string, isPass bool) {
|
||
v = strings.TrimSpace(v)
|
||
if isPass {
|
||
if strings.Contains(k, "#do") || strings.Contains(k, "#provider") {
|
||
k = color.YellowString("%s:", k)
|
||
} else {
|
||
k = color.GreenString("%s:", k)
|
||
}
|
||
} else {
|
||
k = color.RedString("%s:", k)
|
||
v = color.RedString("%s%s", emojiFail, v)
|
||
}
|
||
if v == `"steps"` {
|
||
v = color.BlueString(v)
|
||
}
|
||
|
||
table.AddRow(k, v)
|
||
}
|