1package main 2 3import ( 4 "fmt" 5 "os" 6 "strings" 7 8 "github.com/roboll/helmfile/pkg/app/version" 9 10 "github.com/roboll/helmfile/pkg/app" 11 "github.com/roboll/helmfile/pkg/helmexec" 12 "github.com/roboll/helmfile/pkg/maputil" 13 "github.com/roboll/helmfile/pkg/state" 14 "github.com/urfave/cli" 15 "go.uber.org/zap" 16) 17 18var logger *zap.SugaredLogger 19 20func configureLogging(c *cli.Context) error { 21 // Valid levels: 22 // https://github.com/uber-go/zap/blob/7e7e266a8dbce911a49554b945538c5b950196b8/zapcore/level.go#L126 23 logLevel := c.GlobalString("log-level") 24 if c.GlobalBool("debug") { 25 logLevel = "debug" 26 } else if c.GlobalBool("quiet") { 27 logLevel = "warn" 28 } 29 logger = helmexec.NewLogger(os.Stderr, logLevel) 30 if c.App.Metadata == nil { 31 // Auto-initialised in 1.19.0 32 // https://github.com/urfave/cli/blob/master/CHANGELOG.md#1190---2016-11-19 33 c.App.Metadata = make(map[string]interface{}) 34 } 35 c.App.Metadata["logger"] = logger 36 return nil 37} 38 39func main() { 40 41 cliApp := cli.NewApp() 42 cliApp.Name = "helmfile" 43 cliApp.Usage = "" 44 cliApp.Version = version.Version 45 cliApp.EnableBashCompletion = true 46 cliApp.Flags = []cli.Flag{ 47 cli.StringFlag{ 48 Name: "helm-binary, b", 49 Usage: "path to helm binary", 50 Value: app.DefaultHelmBinary, 51 }, 52 cli.StringFlag{ 53 Name: "file, f", 54 Usage: "load config from file or directory. defaults to `helmfile.yaml` or `helmfile.d`(means `helmfile.d/*.yaml`) in this preference", 55 }, 56 cli.StringFlag{ 57 Name: "environment, e", 58 Usage: `specify the environment name. defaults to "default"`, 59 }, 60 cli.StringSliceFlag{ 61 Name: "state-values-set", 62 Usage: "set state values on the command line (can specify multiple or separate values with commas: key1=val1,key2=val2)", 63 }, 64 cli.StringSliceFlag{ 65 Name: "state-values-file", 66 Usage: "specify state values in a YAML file", 67 }, 68 cli.BoolFlag{ 69 Name: "quiet, q", 70 Usage: "Silence output. Equivalent to log-level warn", 71 }, 72 cli.StringFlag{ 73 Name: "kube-context", 74 Usage: "Set kubectl context. Uses current context by default", 75 }, 76 cli.BoolFlag{ 77 Name: "debug", 78 Usage: "Enable verbose output for Helm and set log-level to debug, this disables --quiet/-q effect", 79 }, 80 cli.BoolFlag{ 81 Name: "no-color", 82 Usage: "Output without color", 83 }, 84 cli.StringFlag{ 85 Name: "log-level", 86 Usage: "Set log level, default info", 87 }, 88 cli.StringFlag{ 89 Name: "namespace, n", 90 Usage: "Set namespace. Uses the namespace set in the context by default, and is available in templates as {{ .Namespace }}", 91 }, 92 cli.StringSliceFlag{ 93 Name: "selector, l", 94 Usage: `Only run using the releases that match labels. Labels can take the form of foo=bar or foo!=bar. 95 A release must match all labels in a group in order to be used. Multiple groups can be specified at once. 96 --selector tier=frontend,tier!=proxy --selector tier=backend. Will match all frontend, non-proxy releases AND all backend releases. 97 The name of a release can be used as a label. --selector name=myrelease`, 98 }, 99 cli.BoolFlag{ 100 Name: "allow-no-matching-release", 101 Usage: `Do not exit with an error code if the provided selector has no matching releases.`, 102 }, 103 cli.BoolFlag{ 104 Name: "interactive, i", 105 Usage: "Request confirmation before attempting to modify clusters", 106 }, 107 } 108 109 cliApp.Before = configureLogging 110 cliApp.Commands = []cli.Command{ 111 { 112 Name: "deps", 113 Usage: "update charts based on their requirements", 114 Flags: []cli.Flag{ 115 cli.StringFlag{ 116 Name: "args", 117 Value: "", 118 Usage: "pass args to helm exec", 119 }, 120 cli.BoolFlag{ 121 Name: "skip-repos", 122 Usage: `skip running "helm repo update" before running "helm dependency build"`, 123 }, 124 }, 125 Action: action(func(run *app.App, c configImpl) error { 126 return run.Deps(c) 127 }), 128 }, 129 { 130 Name: "repos", 131 Usage: "sync repositories from state file (helm repo add && helm repo update)", 132 Flags: []cli.Flag{ 133 cli.StringFlag{ 134 Name: "args", 135 Value: "", 136 Usage: "pass args to helm exec", 137 }, 138 }, 139 Action: action(func(run *app.App, c configImpl) error { 140 return run.Repos(c) 141 }), 142 }, 143 { 144 Name: "charts", 145 Usage: "DEPRECATED: sync releases from state file (helm upgrade --install)", 146 Flags: []cli.Flag{ 147 cli.StringFlag{ 148 Name: "args", 149 Value: "", 150 Usage: "pass args to helm exec", 151 }, 152 cli.StringSliceFlag{ 153 Name: "set", 154 Usage: "additional values to be merged into the command", 155 }, 156 cli.StringSliceFlag{ 157 Name: "values", 158 Usage: "additional value files to be merged into the command", 159 }, 160 cli.IntFlag{ 161 Name: "concurrency", 162 Value: 0, 163 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 164 }, 165 }, 166 Action: action(func(run *app.App, c configImpl) error { 167 return run.DeprecatedSyncCharts(c) 168 }), 169 }, 170 { 171 Name: "diff", 172 Usage: "diff releases from state file against env (helm diff)", 173 Flags: []cli.Flag{ 174 cli.StringFlag{ 175 Name: "args", 176 Value: "", 177 Usage: "pass args to helm exec", 178 }, 179 cli.StringSliceFlag{ 180 Name: "set", 181 Usage: "additional values to be merged into the command", 182 }, 183 cli.StringSliceFlag{ 184 Name: "values", 185 Usage: "additional value files to be merged into the command", 186 }, 187 cli.BoolFlag{ 188 Name: "skip-deps", 189 Usage: `skip running "helm repo update" and "helm dependency build"`, 190 }, 191 cli.BoolFlag{ 192 Name: "detailed-exitcode", 193 Usage: "return a non-zero exit code when there are changes", 194 }, 195 cli.BoolFlag{ 196 Name: "include-tests", 197 Usage: "enable the diffing of the helm test hooks", 198 }, 199 cli.BoolFlag{ 200 Name: "suppress-secrets", 201 Usage: "suppress secrets in the output. highly recommended to specify on CI/CD use-cases", 202 }, 203 cli.IntFlag{ 204 Name: "concurrency", 205 Value: 0, 206 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 207 }, 208 cli.IntFlag{ 209 Name: "context", 210 Value: 0, 211 Usage: "output NUM lines of context around changes", 212 }, 213 }, 214 Action: action(func(run *app.App, c configImpl) error { 215 return run.Diff(c) 216 }), 217 }, 218 { 219 Name: "template", 220 Usage: "template releases from state file against env (helm template)", 221 Flags: []cli.Flag{ 222 cli.StringFlag{ 223 Name: "args", 224 Value: "", 225 Usage: "pass args to helm template", 226 }, 227 cli.StringSliceFlag{ 228 Name: "set", 229 Usage: "additional values to be merged into the command", 230 }, 231 cli.StringSliceFlag{ 232 Name: "values", 233 Usage: "additional value files to be merged into the command", 234 }, 235 cli.StringFlag{ 236 Name: "output-dir", 237 Usage: "output directory to pass to helm template (helm template --output-dir)", 238 }, 239 cli.StringFlag{ 240 Name: "output-dir-template", 241 Usage: "go text template for generating the output directory. Default: {{ .OutputDir }}/{{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}-{{ .Release.Name}}", 242 }, 243 cli.IntFlag{ 244 Name: "concurrency", 245 Value: 0, 246 Usage: "maximum number of concurrent downloads of release charts", 247 }, 248 cli.BoolFlag{ 249 Name: "validate", 250 Usage: "validate your manifests against the Kubernetes cluster you are currently pointing at", 251 }, 252 cli.BoolFlag{ 253 Name: "include-crds", 254 Usage: "include CRDs in the templated output", 255 }, 256 cli.BoolFlag{ 257 Name: "skip-deps", 258 Usage: `skip running "helm repo update" and "helm dependency build"`, 259 }, 260 cli.BoolFlag{ 261 Name: "skip-cleanup", 262 Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security", 263 }, 264 }, 265 Action: action(func(run *app.App, c configImpl) error { 266 return run.Template(c) 267 }), 268 }, 269 { 270 Name: "write-values", 271 Usage: "write values files for releases. Similar to `helmfile template`, write values files instead of manifests.", 272 Flags: []cli.Flag{ 273 cli.StringSliceFlag{ 274 Name: "set", 275 Usage: "additional values to be merged into the command", 276 }, 277 cli.StringSliceFlag{ 278 Name: "values", 279 Usage: "additional value files to be merged into the command", 280 }, 281 cli.StringFlag{ 282 Name: "output-file-template", 283 Usage: "go text template for generating the output file. Default: {{ .State.BaseName }}-{{ .State.AbsPathSHA1 }}/{{ .Release.Name}}.yaml", 284 }, 285 cli.IntFlag{ 286 Name: "concurrency", 287 Value: 0, 288 Usage: "maximum number of concurrent downloads of release charts", 289 }, 290 cli.BoolFlag{ 291 Name: "skip-deps", 292 Usage: `skip running "helm repo update" and "helm dependency build"`, 293 }, 294 }, 295 Action: action(func(run *app.App, c configImpl) error { 296 return run.WriteValues(c) 297 }), 298 }, 299 { 300 Name: "lint", 301 Usage: "lint charts from state file (helm lint)", 302 Flags: []cli.Flag{ 303 cli.StringFlag{ 304 Name: "args", 305 Value: "", 306 Usage: "pass args to helm exec", 307 }, 308 cli.StringSliceFlag{ 309 Name: "set", 310 Usage: "additional values to be merged into the command", 311 }, 312 cli.StringSliceFlag{ 313 Name: "values", 314 Usage: "additional value files to be merged into the command", 315 }, 316 cli.IntFlag{ 317 Name: "concurrency", 318 Value: 0, 319 Usage: "maximum number of concurrent downloads of release charts", 320 }, 321 cli.BoolFlag{ 322 Name: "skip-deps", 323 Usage: `skip running "helm repo update" and "helm dependency build"`, 324 }, 325 }, 326 Action: action(func(run *app.App, c configImpl) error { 327 return run.Lint(c) 328 }), 329 }, 330 { 331 Name: "sync", 332 Usage: "sync all resources from state file (repos, releases and chart deps)", 333 Flags: []cli.Flag{ 334 cli.StringSliceFlag{ 335 Name: "set", 336 Usage: "additional values to be merged into the command", 337 }, 338 cli.StringSliceFlag{ 339 Name: "values", 340 Usage: "additional value files to be merged into the command", 341 }, 342 cli.IntFlag{ 343 Name: "concurrency", 344 Value: 0, 345 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 346 }, 347 cli.StringFlag{ 348 Name: "args", 349 Value: "", 350 Usage: "pass args to helm exec", 351 }, 352 cli.BoolFlag{ 353 Name: "skip-deps", 354 Usage: `skip running "helm repo update" and "helm dependency build"`, 355 }, 356 cli.BoolFlag{ 357 Name: "wait", 358 Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`, 359 }, 360 }, 361 Action: action(func(run *app.App, c configImpl) error { 362 return run.Sync(c) 363 }), 364 }, 365 { 366 Name: "apply", 367 Usage: "apply all resources from state file only when there are changes", 368 Flags: []cli.Flag{ 369 cli.StringSliceFlag{ 370 Name: "set", 371 Usage: "additional values to be merged into the command", 372 }, 373 cli.StringSliceFlag{ 374 Name: "values", 375 Usage: "additional value files to be merged into the command", 376 }, 377 cli.IntFlag{ 378 Name: "concurrency", 379 Value: 0, 380 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 381 }, 382 cli.IntFlag{ 383 Name: "context", 384 Value: 0, 385 Usage: "output NUM lines of context around changes", 386 }, 387 cli.BoolFlag{ 388 Name: "detailed-exitcode", 389 Usage: "return a non-zero exit code 2 instead of 0 when there were changes detected AND the changes are synced successfully", 390 }, 391 cli.StringFlag{ 392 Name: "args", 393 Value: "", 394 Usage: "pass args to helm exec", 395 }, 396 cli.BoolFlag{ 397 Name: "retain-values-files", 398 Usage: "DEPRECATED: Use skip-cleanup instead", 399 }, 400 cli.BoolFlag{ 401 Name: "skip-cleanup", 402 Usage: "Stop cleaning up temporary values generated by helmfile and helm-secrets. Useful for debugging. Don't use in production for security", 403 }, 404 cli.BoolFlag{ 405 Name: "skip-diff-on-install", 406 Usage: "Skips running helm-diff on releases being newly installed on this apply. Useful when the release manifests are too huge to be reviewed, or it's too time-consuming to diff at all", 407 }, 408 cli.BoolFlag{ 409 Name: "include-tests", 410 Usage: "enable the diffing of the helm test hooks", 411 }, 412 cli.BoolFlag{ 413 Name: "suppress-secrets", 414 Usage: "suppress secrets in the diff output. highly recommended to specify on CI/CD use-cases", 415 }, 416 cli.BoolFlag{ 417 Name: "suppress-diff", 418 Usage: "suppress diff in the output. Usable in new installs", 419 }, 420 cli.BoolFlag{ 421 Name: "skip-deps", 422 Usage: `skip running "helm repo update" and "helm dependency build"`, 423 }, 424 cli.BoolFlag{ 425 Name: "wait", 426 Usage: `Override helmDefaults.wait setting "helm upgrade --install --wait"`, 427 }, 428 }, 429 Action: action(func(run *app.App, c configImpl) error { 430 return run.Apply(c) 431 }), 432 }, 433 { 434 Name: "status", 435 Usage: "retrieve status of releases in state file", 436 Flags: []cli.Flag{ 437 cli.IntFlag{ 438 Name: "concurrency", 439 Value: 0, 440 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 441 }, 442 cli.StringFlag{ 443 Name: "args", 444 Value: "", 445 Usage: "pass args to helm exec", 446 }, 447 }, 448 Action: action(func(run *app.App, c configImpl) error { 449 return run.Status(c) 450 }), 451 }, 452 { 453 Name: "delete", 454 Usage: "DEPRECATED: delete releases from state file (helm delete)", 455 Flags: []cli.Flag{ 456 cli.IntFlag{ 457 Name: "concurrency", 458 Value: 0, 459 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 460 }, 461 cli.StringFlag{ 462 Name: "args", 463 Value: "", 464 Usage: "pass args to helm exec", 465 }, 466 cli.BoolFlag{ 467 Name: "purge", 468 Usage: "purge releases i.e. free release names and histories", 469 }, 470 }, 471 Action: action(func(run *app.App, c configImpl) error { 472 return run.Delete(c) 473 }), 474 }, 475 { 476 Name: "destroy", 477 Usage: "deletes and then purges releases", 478 Flags: []cli.Flag{ 479 cli.IntFlag{ 480 Name: "concurrency", 481 Value: 0, 482 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 483 }, 484 cli.StringFlag{ 485 Name: "args", 486 Value: "", 487 Usage: "pass args to helm exec", 488 }, 489 }, 490 Action: action(func(run *app.App, c configImpl) error { 491 return run.Destroy(c) 492 }), 493 }, 494 { 495 Name: "test", 496 Usage: "test releases from state file (helm test)", 497 Flags: []cli.Flag{ 498 cli.BoolFlag{ 499 Name: "cleanup", 500 Usage: "delete test pods upon completion", 501 }, 502 cli.BoolFlag{ 503 Name: "logs", 504 Usage: "Dump the logs from test pods (this runs after all tests are complete, but before any cleanup)", 505 }, 506 cli.StringFlag{ 507 Name: "args", 508 Value: "", 509 Usage: "pass additional args to helm exec", 510 }, 511 cli.IntFlag{ 512 Name: "timeout", 513 Value: 300, 514 Usage: "maximum time for tests to run before being considered failed", 515 }, 516 cli.IntFlag{ 517 Name: "concurrency", 518 Value: 0, 519 Usage: "maximum number of concurrent helm processes to run, 0 is unlimited", 520 }, 521 }, 522 Action: action(func(run *app.App, c configImpl) error { 523 return run.Test(c) 524 }), 525 }, 526 { 527 Name: "build", 528 Usage: "output compiled helmfile state(s) as YAML", 529 Flags: []cli.Flag{ 530 cli.BoolFlag{ 531 Name: "embed-values", 532 Usage: "Read all the values files for every release and embed into the output helmfile.yaml", 533 }, 534 }, 535 Action: action(func(run *app.App, c configImpl) error { 536 return run.PrintState(c) 537 }), 538 }, 539 { 540 Name: "list", 541 Usage: "list releases defined in state file", 542 Flags: []cli.Flag{ 543 cli.StringFlag{ 544 Name: "output", 545 Value: "", 546 Usage: "output releases list as a json string", 547 }, 548 }, 549 Action: action(func(run *app.App, c configImpl) error { 550 return run.ListReleases(c) 551 }), 552 }, 553 { 554 Name: "version", 555 Usage: "Show the version for Helmfile.", 556 ArgsUsage: "[command]", 557 Action: func(c *cli.Context) error { 558 cli.ShowVersion(c) 559 return nil 560 }, 561 }, 562 } 563 564 err := cliApp.Run(os.Args) 565 if err != nil { 566 fmt.Fprintf(os.Stderr, "%v\n", err) 567 os.Exit(3) 568 } 569} 570 571type configImpl struct { 572 c *cli.Context 573 574 set map[string]interface{} 575} 576 577func NewUrfaveCliConfigImpl(c *cli.Context) (configImpl, error) { 578 if c.NArg() > 0 { 579 cli.ShowAppHelp(c) 580 return configImpl{}, fmt.Errorf("err: extraneous arguments: %s", strings.Join(c.Args(), ", ")) 581 } 582 583 conf := configImpl{ 584 c: c, 585 } 586 587 optsSet := c.GlobalStringSlice("state-values-set") 588 if len(optsSet) > 0 { 589 set := map[string]interface{}{} 590 for i := range optsSet { 591 ops := strings.Split(optsSet[i], ",") 592 for j := range ops { 593 op := strings.SplitN(ops[j], "=", 2) 594 k := maputil.ParseKey(op[0]) 595 v := op[1] 596 597 maputil.Set(set, k, v) 598 } 599 } 600 conf.set = set 601 } 602 603 return conf, nil 604} 605 606func (c configImpl) Set() []string { 607 return c.c.StringSlice("set") 608} 609 610func (c configImpl) SkipRepos() bool { 611 return c.c.Bool("skip-repos") 612} 613 614func (c configImpl) Wait() bool { 615 return c.c.Bool("wait") 616} 617 618func (c configImpl) Values() []string { 619 return c.c.StringSlice("values") 620} 621 622func (c configImpl) Args() string { 623 args := c.c.String("args") 624 enableHelmDebug := c.c.GlobalBool("debug") 625 626 if enableHelmDebug { 627 args = fmt.Sprintf("%s %s", args, "--debug") 628 } 629 return args 630} 631 632func (c configImpl) OutputDir() string { 633 return c.c.String("output-dir") 634} 635 636func (c configImpl) OutputDirTemplate() string { 637 return c.c.String("output-dir-template") 638} 639 640func (c configImpl) OutputFileTemplate() string { 641 return c.c.String("output-file-template") 642} 643 644func (c configImpl) Validate() bool { 645 return c.c.Bool("validate") 646} 647 648func (c configImpl) Concurrency() int { 649 return c.c.Int("concurrency") 650} 651 652func (c configImpl) HasCommandName(name string) bool { 653 return c.c.Command.HasName(name) 654} 655 656// DiffConfig 657 658func (c configImpl) SkipDeps() bool { 659 return c.c.Bool("skip-deps") 660} 661 662func (c configImpl) DetailedExitcode() bool { 663 return c.c.Bool("detailed-exitcode") 664} 665 666func (c configImpl) RetainValuesFiles() bool { 667 return c.c.Bool("retain-values-files") 668} 669 670func (c configImpl) IncludeTests() bool { 671 return c.c.Bool("include-tests") 672} 673 674func (c configImpl) SuppressSecrets() bool { 675 return c.c.Bool("suppress-secrets") 676} 677 678func (c configImpl) SuppressDiff() bool { 679 return c.c.Bool("suppress-diff") 680} 681 682// DeleteConfig 683 684func (c configImpl) Purge() bool { 685 return c.c.Bool("purge") 686} 687 688// TestConfig 689 690func (c configImpl) Cleanup() bool { 691 return c.c.Bool("cleanup") 692} 693 694func (c configImpl) Logs() bool { 695 return c.c.Bool("logs") 696} 697 698func (c configImpl) Timeout() int { 699 if !c.c.IsSet("timeout") { 700 return state.EmptyTimeout 701 } 702 return c.c.Int("timeout") 703} 704 705// ListConfig 706 707func (c configImpl) Output() string { 708 return c.c.String("output") 709} 710 711// GlobalConfig 712 713func (c configImpl) HelmBinary() string { 714 return c.c.GlobalString("helm-binary") 715} 716 717func (c configImpl) KubeContext() string { 718 return c.c.GlobalString("kube-context") 719} 720 721func (c configImpl) Namespace() string { 722 return c.c.GlobalString("namespace") 723} 724 725func (c configImpl) FileOrDir() string { 726 return c.c.GlobalString("file") 727} 728 729func (c configImpl) Selectors() []string { 730 return c.c.GlobalStringSlice("selector") 731} 732 733func (c configImpl) StateValuesSet() map[string]interface{} { 734 return c.set 735} 736 737func (c configImpl) StateValuesFiles() []string { 738 return c.c.GlobalStringSlice("state-values-file") 739} 740 741func (c configImpl) Interactive() bool { 742 return c.c.GlobalBool("interactive") 743} 744 745func (c configImpl) NoColor() bool { 746 return c.c.GlobalBool("no-color") 747} 748 749func (c configImpl) Context() int { 750 return c.c.Int("context") 751} 752 753func (c configImpl) SkipCleanup() bool { 754 return c.c.Bool("skip-cleanup") 755} 756 757func (c configImpl) SkipDiffOnInstall() bool { 758 return c.c.Bool("skip-diff-on-install") 759} 760 761func (c configImpl) EmbedValues() bool { 762 return c.c.Bool("embed-values") 763} 764 765func (c configImpl) IncludeCRDs() bool { 766 return c.c.Bool("include-crds") 767} 768 769func (c configImpl) Logger() *zap.SugaredLogger { 770 return c.c.App.Metadata["logger"].(*zap.SugaredLogger) 771} 772 773func (c configImpl) Env() string { 774 env := c.c.GlobalString("environment") 775 if env == "" { 776 env = os.Getenv("HELMFILE_ENVIRONMENT") 777 if env == "" { 778 env = state.DefaultEnv 779 } 780 } 781 return env 782} 783 784func action(do func(*app.App, configImpl) error) func(*cli.Context) error { 785 return func(implCtx *cli.Context) error { 786 conf, err := NewUrfaveCliConfigImpl(implCtx) 787 if err != nil { 788 return err 789 } 790 791 a := app.New(conf) 792 793 if err := do(a, conf); err != nil { 794 return toCliError(implCtx, err) 795 } 796 797 return nil 798 } 799} 800 801func toCliError(c *cli.Context, err error) error { 802 if err != nil { 803 switch e := err.(type) { 804 case *app.NoMatchingHelmfileError: 805 noMatchingExitCode := 3 806 if c.GlobalBool("allow-no-matching-release") { 807 noMatchingExitCode = 0 808 } 809 return cli.NewExitError(e.Error(), noMatchingExitCode) 810 case *app.Error: 811 return cli.NewExitError(e.Error(), e.Code()) 812 default: 813 panic(fmt.Errorf("BUG: please file an github issue for this unhandled error: %T: %v", e, e)) 814 } 815 } 816 return err 817} 818