diff --git a/acceptance/experimental/air/register-image-no-secret-permission/output.txt b/acceptance/experimental/air/register-image-no-secret-permission/output.txt index 36a98a1fe76..98278254992 100644 --- a/acceptance/experimental/air/register-image-no-secret-permission/output.txt +++ b/acceptance/experimental/air/register-image-no-secret-permission/output.txt @@ -3,6 +3,9 @@ >>> [CLI] experimental air register-image nvcr.io/org/img:1.0 Error: image "nvcr.io/org/img:1.0" requires credentials, and the credentials found in your local Docker config could not be stored: creating secret scope "docker-credentials-[USERNAME]" was denied (user does not have permission to create secret scopes). Ask a workspace admin for permission to create secret scopes +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Host: [DATABRICKS_URL] Auth type: Personal Access Token (pat) diff --git a/acceptance/experimental/air/register-image/output.txt b/acceptance/experimental/air/register-image/output.txt index ebb73f8cba8..95f6dcf3de7 100644 --- a/acceptance/experimental/air/register-image/output.txt +++ b/acceptance/experimental/air/register-image/output.txt @@ -40,12 +40,18 @@ To use this image in your training config: >>> [CLI] experimental air register-image Error: IMAGE_URL cannot be empty +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === tag-policy auto is rejected >>> [CLI] experimental air register-image my-image:latest --tag-policy auto Error: --tag-policy auto is no longer supported: auto mode was removed and registration now always checks the source registry for the latest digest; omit the flag or use --tag-policy latest +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === invalid tag policy (json) @@ -67,6 +73,9 @@ Exit code: 1 >>> [CLI] experimental air register-image my-image:latest --timeout-minutes 0 Error: --timeout-minutes must be positive, got 0 +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air register-image … + Exit code: 1 === removed credential flags are unknown diff --git a/acceptance/experimental/air/run-submit-deps/output.txt b/acceptance/experimental/air/run-submit-deps/output.txt index 5425469f42f..761216b21be 100644 --- a/acceptance/experimental/air/run-submit-deps/output.txt +++ b/acceptance/experimental/air/run-submit-deps/output.txt @@ -63,3 +63,6 @@ Stream logs after submission using: === a requirements.yaml file path is rejected; deps must be inline >>> [CLI] experimental air run -f run-file.yaml Error: invalid config run-file.yaml: environment.dependencies must be a list of packages or reference a requirements.txt (see https://docs.databricks.com/aws/en/machine-learning/ai-runtime/cli/yaml-config#reference). A direct file reference is not supported + +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … diff --git a/acceptance/experimental/air/run/output.txt b/acceptance/experimental/air/run/output.txt index 18bf9526f31..de25a42433d 100644 --- a/acceptance/experimental/air/run/output.txt +++ b/acceptance/experimental/air/run/output.txt @@ -24,6 +24,9 @@ Dry run: configuration for "smoke-test" is valid; not submitting. >>> [CLI] experimental air run -f valid.yaml --dry-run --override bogus=1 Error: invalid --override "bogus": "bogus" is not a known field; available fields are: code_source, command, compute, env_variables, environment, experiment_name, idempotency_token, max_retries, mlflow_artifact_location, mlflow_experiment_directory, mlflow_run_name, parameters, permissions, secrets, timeout_minutes, usage_policy_id, usage_policy_name +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === override still runs schema validation @@ -31,6 +34,9 @@ Exit code: 1 Override: changing compute.num_accelerators from 1 to 0 Error: compute.num_accelerators must be positive, got 0 +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === watch is ignored with dry-run (nothing is submitted) @@ -45,12 +51,18 @@ Dry run: configuration for "smoke-test" is valid; not submitting. >>> [CLI] experimental air run -f git-remote.yaml --dry-run Error: git.remote is no longer supported: the snapshot archives your local copy, so a branch resolves to its local HEAD. To deploy a specific committed revision, use git.commit +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === invalid config is rejected >>> [CLI] experimental air run -f invalid.yaml --dry-run Error: invalid experiment_name "bad.name": only alphanumeric characters, hyphens (-), and underscores (_) are allowed +Tip: use the --debug flag to see more details and a trace of this error: + databricks --debug experimental air run … + Exit code: 1 === missing --file diff --git a/experimental/air/cmd/air.go b/experimental/air/cmd/air.go index 3ea28404b6b..a90773b284f 100644 --- a/experimental/air/cmd/air.go +++ b/experimental/air/cmd/air.go @@ -1,6 +1,9 @@ package aircmd import ( + "fmt" + "strings" + "github.com/spf13/cobra" ) @@ -15,13 +18,42 @@ This command set is the Go port of the standalone Python "air" CLI. It is experimental and may change in future versions.`, } - cmd.AddCommand(newRunCommand()) + runCommand := newRunCommand() + wrapRunErrorWithDebugTip(runCommand) + cmd.AddCommand(runCommand) cmd.AddCommand(newGetCommand()) cmd.AddCommand(newListCommand()) cmd.AddCommand(newLogsCommand()) cmd.AddCommand(newCancelCommand()) - cmd.AddCommand(newRegisterImageCommand()) + registerImageCommand := newRegisterImageCommand() + wrapRunErrorWithDebugTip(registerImageCommand) + cmd.AddCommand(registerImageCommand) cmd.AddCommand(newConvertToDabsCommand()) return cmd } + +func wrapRunErrorWithDebugTip(cmd *cobra.Command) { + runE := cmd.RunE + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return withDebugErrorTip(cmd, runE(cmd, args)) + } +} + +func withDebugErrorTip(cmd *cobra.Command, err error) error { + if err == nil { + return nil + } + debugFlag := cmd.Root().PersistentFlags().Lookup("debug") + if debugFlag != nil && debugFlag.Value.String() == "true" { + return err + } + + commandPrefix := cmd.Root().CommandPath() + command := commandPrefix + " --debug" + strings.TrimPrefix(cmd.CommandPath(), commandPrefix) + return fmt.Errorf( + "%w\n\nTip: use the --debug flag to see more details and a trace of this error:\n %s …", + err, + command, + ) +} diff --git a/experimental/air/cmd/air_test.go b/experimental/air/cmd/air_test.go index 1843acfe900..8717c293886 100644 --- a/experimental/air/cmd/air_test.go +++ b/experimental/air/cmd/air_test.go @@ -1,8 +1,10 @@ package aircmd import ( + "errors" "testing" + "github.com/spf13/cobra" "github.com/stretchr/testify/assert" ) @@ -20,3 +22,49 @@ func TestNewRegistersAllSubcommands(t *testing.T) { } assert.Len(t, registered, len(want), "unexpected number of subcommands") } + +func TestRunErrorIncludesDebugTip(t *testing.T) { + originalErr := errors.New("failed") + runCommand := &cobra.Command{ + Use: "run [arg]", + RunE: func(cmd *cobra.Command, args []string) error { + return originalErr + }, + } + airCommand := &cobra.Command{Use: "air"} + airCommand.AddCommand(runCommand) + wrapRunErrorWithDebugTip(runCommand) + experimentalCommand := &cobra.Command{Use: "experimental"} + experimentalCommand.AddCommand(airCommand) + rootCommand := &cobra.Command{Use: "databricks"} + rootCommand.PersistentFlags().Bool("debug", false, "") + rootCommand.AddCommand(experimentalCommand) + + err := runCommand.RunE(runCommand, []string{"secret-value"}) + + assert.ErrorIs(t, err, originalErr) + assert.Contains(t, err.Error(), "use the --debug flag") + assert.Contains(t, err.Error(), "databricks --debug experimental air run …") + assert.NotContains(t, err.Error(), "secret-value") +} + +func TestRunErrorOmitsDebugTipWhenDebugEnabled(t *testing.T) { + originalErr := errors.New("failed") + runCommand := &cobra.Command{ + Use: "run", + RunE: func(cmd *cobra.Command, args []string) error { + return originalErr + }, + } + airCommand := &cobra.Command{Use: "air"} + airCommand.AddCommand(runCommand) + wrapRunErrorWithDebugTip(runCommand) + rootCommand := &cobra.Command{Use: "databricks"} + rootCommand.PersistentFlags().Bool("debug", true, "") + rootCommand.AddCommand(airCommand) + + err := runCommand.RunE(runCommand, nil) + + assert.Same(t, originalErr, err) + assert.NotContains(t, err.Error(), "use the --debug flag") +}