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 }