Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions acceptance/experimental/air/unimplemented/out.test.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions acceptance/experimental/air/unimplemented/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@

=== run
>>> [CLI] experimental air run
Error: `air run` is not implemented yet

Exit code: 1

=== status
>>> [CLI] experimental air status 123
Error: `air status` is not implemented yet

Exit code: 1

=== list
>>> [CLI] experimental air list
Error: `air list` is not implemented yet

Exit code: 1

=== logs
>>> [CLI] experimental air logs 123
Error: `air logs` is not implemented yet

Exit code: 1

=== cancel
>>> [CLI] experimental air cancel 123
Error: `air cancel` is not implemented yet

Exit code: 1

=== register-image
>>> [CLI] experimental air register-image my-image:latest
Error: `air register-image` is not implemented yet

Exit code: 1
19 changes: 19 additions & 0 deletions acceptance/experimental/air/unimplemented/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# Each stub must fail with "not implemented"; errcode records the exit code.

title "run"
errcode trace $CLI experimental air run

title "status"
errcode trace $CLI experimental air status 123

title "list"
errcode trace $CLI experimental air list

title "logs"
errcode trace $CLI experimental air logs 123

title "cancel"
errcode trace $CLI experimental air cancel 123

title "register-image"
errcode trace $CLI experimental air register-image my-image:latest

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR's stated goal is making the command tree reviewable, so let's pin it in the acceptance output. Add a help case and the tree, short descriptions and flags all land in output.txt, then any future change shows up as a diff:

title "help"
trace $CLI experimental air --help

6 changes: 6 additions & 0 deletions acceptance/experimental/air/unimplemented/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Local = true

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Local = true and Cloud = false are already the defaults from the root acceptance/test.toml, and config is inherited (the genie test next door omits them). Keep just the [EnvMatrix] override and its comment.

Cloud = false

# Stubs fail locally before any API call, so no server stubs needed.
[EnvMatrix]
DATABRICKS_BUNDLE_ENGINE = []
36 changes: 36 additions & 0 deletions cmd/experimental/air/air.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package air

import (
"fmt"

"github.com/spf13/cobra"
)

// New returns the root command for the experimental AI runtime CLI.
//
// Milestone 0: scaffolds the command group with every subcommand registered as a
// stub (not yet implemented), pending the port from the Python `air` CLI.
func New() *cobra.Command {
cmd := &cobra.Command{
Use: "air",
Short: "Run and manage AI runtime training workloads",
Long: `Run and manage AI runtime training workloads on Databricks serverless GPU compute.

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())
cmd.AddCommand(newStatusCommand())
cmd.AddCommand(newListCommand())
cmd.AddCommand(newLogsCommand())
cmd.AddCommand(newCancelCommand())
cmd.AddCommand(newRegisterImageCommand())

return cmd
}

// notImplemented returns the placeholder error used by milestone-0 stubs.
func notImplemented(name string) error {
return fmt.Errorf("`air %s` is not implemented yet", name)
}
22 changes: 22 additions & 0 deletions cmd/experimental/air/air_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package air

import (
"testing"

"github.com/stretchr/testify/assert"
)

// TestNewRegistersAllSubcommands asserts the `ai` command wires up every

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: ai should be air.

// expected subcommand, so none is accidentally dropped from New.
func TestNewRegistersAllSubcommands(t *testing.T) {
registered := make(map[string]bool)
for _, c := range New().Commands() {
registered[c.Name()] = true
}

want := []string{"run", "status", "list", "logs", "cancel", "register-image"}
for _, name := range want {
assert.True(t, registered[name], "subcommand %q is not registered", name)
}
assert.Len(t, registered, len(want), "unexpected number of subcommands")
}
27 changes: 27 additions & 0 deletions cmd/experimental/air/cancel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package air

import (
"github.com/spf13/cobra"
)

func newCancelCommand() *cobra.Command {
var (
all bool
yes bool
)

cmd := &cobra.Command{
Use: "cancel [RUN_ID...]",
Args: cobra.ArbitraryArgs,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cobra.ArbitraryArgs accepts air cancel with no IDs and no --all, and also --all combined with explicit IDs. It doesn't matter while this is a stub, but the arg contract is part of what this PR puts up for review. Either add a small Args validator now (return root.InvalidArgsError like the helpers in cmd/root/args.go, so the usage string gets printed) or leave an explicit TODO for the implementation PR.

Short: "Cancel one or more runs",
Long: `Cancel one or more runs by ID, or cancel all of your active runs with --all.`,
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("cancel")
},
}

cmd.Flags().BoolVar(&all, "all", false, "Cancel all of your active runs")
cmd.Flags().BoolVarP(&yes, "yes", "y", false, "Skip the confirmation prompt")

return cmd
}
31 changes: 31 additions & 0 deletions cmd/experimental/air/list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package air

import (
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra"
)

func newListCommand() *cobra.Command {
var (
limit int
active bool
allUsers bool
filters []string
)

cmd := &cobra.Command{
Use: "list",
Args: root.NoArgs,
Short: "List recent runs",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("list")
},
}

cmd.Flags().IntVar(&limit, "limit", 20, "Maximum number of runs to show")
cmd.Flags().BoolVar(&active, "active", false, "Show only active runs")
cmd.Flags().BoolVar(&allUsers, "all-users", false, "Show runs from all users")
cmd.Flags().StringArrayVar(&filters, "filter", nil, "Filter runs, e.g. experiment=foo* (repeatable)")

return cmd
}
34 changes: 34 additions & 0 deletions cmd/experimental/air/logs.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package air

import (
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra"
)

func newLogsCommand() *cobra.Command {
var (
node int
lines int
retry int
downloadTo string
review bool
)

cmd := &cobra.Command{
Use: "logs RUN_ID",
Args: root.ExactArgs(1),
Short: "Stream or fetch logs for a run",
Long: `Stream logs from an active run, or fetch logs from a completed run.`,
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("logs")
},
}

cmd.Flags().IntVar(&node, "node", 0, "Fetch logs from this node")
cmd.Flags().IntVar(&lines, "lines", 10000, "For completed runs, print the last N lines")
cmd.Flags().IntVar(&retry, "retry", -1, "View logs from a specific retry attempt (default: latest)")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pflag appends its own marker for non-zero defaults, so the help renders a double default (checked against a local build):

--retry int   View logs from a specific retry attempt (default: latest) (default -1)

Reword to fold the sentinel in, something like "View logs from a specific retry attempt; -1 means latest".

cmd.Flags().StringVar(&downloadTo, "download-to", "", "Download all logs to this directory instead of printing")
cmd.Flags().BoolVar(&review, "review", false, "Download logs from all nodes and filter for error signatures")

return cmd
}
33 changes: 33 additions & 0 deletions cmd/experimental/air/register_image.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package air

import (
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra"
)

func newRegisterImageCommand() *cobra.Command {
var (
scope string
key string
interactiveAuth bool
tagPolicy string
timeoutMinutes int
)

cmd := &cobra.Command{
Use: "register-image IMAGE_URL",
Args: root.ExactArgs(1),
Short: "Mirror a Docker image into the workspace registry",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("register-image")
},
}

cmd.Flags().StringVar(&scope, "scope", "", "Databricks secret scope holding registry credentials")
cmd.Flags().StringVar(&key, "key", "", "Databricks secret key holding registry credentials")
cmd.Flags().BoolVar(&interactiveAuth, "interactive-authenticate", false, "Prompt for registry credentials and store them as a secret")
cmd.Flags().StringVar(&tagPolicy, "tag-policy", "auto", "Image resolution policy: auto or latest")
cmd.Flags().IntVar(&timeoutMinutes, "timeout-minutes", 60, "Timeout to wait for the image to become available")

return cmd
}
36 changes: 36 additions & 0 deletions cmd/experimental/air/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package air

import (
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra"
)

func newRunCommand() *cobra.Command {
var (
file string
watch bool
overrides []string
dryRun bool
idempotencyKey string
)

cmd := &cobra.Command{
Use: "run",
Args: root.NoArgs,
Short: "Submit a training workload from a YAML config",
Long: `Submit a training workload to Databricks serverless GPU compute.

The workload is described by a YAML config file (see --file).`,
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("run")
},
}

cmd.Flags().StringVarP(&file, "file", "f", "", "Path to the workload YAML config")
cmd.Flags().BoolVar(&watch, "watch", false, "Stream logs until the run completes")
cmd.Flags().StringArrayVar(&overrides, "override", nil, "Override a YAML field, e.g. compute.num_accelerators=8 (repeatable)")
cmd.Flags().BoolVar(&dryRun, "dry-run", false, "Validate the config without submitting")
cmd.Flags().StringVar(&idempotencyKey, "idempotency-key", "", "Return the existing run if this key was already used")

return cmd
}
19 changes: 19 additions & 0 deletions cmd/experimental/air/status.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package air

import (
"github.com/databricks/cli/cmd/root"
"github.com/spf13/cobra"
)

func newStatusCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "status RUN_ID",
Args: root.ExactArgs(1),
Short: "Show status and configuration for a run",
RunE: func(cmd *cobra.Command, args []string) error {
return notImplemented("status")
},
}

return cmd
}
30 changes: 30 additions & 0 deletions cmd/experimental/air/stubs_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package air

import (
"testing"

"github.com/spf13/cobra"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestStubCommandsReturnNotImplemented asserts each unimplemented subcommand
// fails with a "not implemented" error. Drop a command here once it lands.
func TestStubCommandsReturnNotImplemented(t *testing.T) {
stubs := map[string]*cobra.Command{
"run": newRunCommand(),
"status": newStatusCommand(),
"list": newListCommand(),
"logs": newLogsCommand(),
"cancel": newCancelCommand(),
"register-image": newRegisterImageCommand(),
}

for name, cmd := range stubs {
t.Run(name, func(t *testing.T) {
require.NotNil(t, cmd.RunE, "command should define RunE")
err := cmd.RunE(cmd, nil)
assert.ErrorContains(t, err, "not implemented")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This passes even if a stub returns the wrong name, i.e. if cancel.go copy-pasted notImplemented("list") this test wouldn't notice. You have the name as the map key, so assert the full message:

assert.EqualError(t, err, fmt.Sprintf("`air %s` is not implemented yet", name))

})
}
}
2 changes: 2 additions & 0 deletions cmd/experimental/experimental.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package experimental

import (
"github.com/databricks/cli/cmd/experimental/air"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The established layout for experimental features is experimental/<name>/cmd: that's where aitools, genie and postgres live, and cmd/experimental/ only owns the dispatcher plus the single-file workspace_open command. AIR is going to grow real implementation code over the next milestones, so please move this package to experimental/air/cmd (imported as aircmd here). Moving later only gets more expensive as milestones stack on top.

One catch to take care of in the same PR: TEST_PACKAGES in Taskfile.yml covers ./cmd/... but not ./experimental/... (only ssh), so add ./experimental/air/... to it, otherwise the unit tests silently stop running after the move. Update the go test path in the description's Tests section too.

aitoolscmd "github.com/databricks/cli/experimental/aitools/cmd"
geniecmd "github.com/databricks/cli/experimental/genie/cmd"
postgrescmd "github.com/databricks/cli/experimental/postgres/cmd"
Expand All @@ -22,6 +23,7 @@ These commands provide early access to new features that are still under
development. They may change or be removed in future versions without notice.`,
}

cmd.AddCommand(air.New())
cmd.AddCommand(aitoolscmd.NewAitoolsCmd())
cmd.AddCommand(geniecmd.NewGenieCmd())
cmd.AddCommand(postgrescmd.New())
Expand Down
2 changes: 1 addition & 1 deletion tools/list_embeds.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def main():
# files are silently missed until the file is staged or committed.
out = subprocess.check_output(
["git", "grep", "--no-color", "-E", "^" + EMBED, "--", "*.go"],
text=True,
universal_newlines=True,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change doesn't belong in this PR and the rationale doesn't hold up. This script isn't part of the acceptance harness, it's invoked by the Taskfile (EMBED_SOURCES var). The repo's Python floor is also way above 3.6: the tools/ scripts declare requires-python = ">=3.12", CI runs 3.13, and tools/check_deadcode.py plus several acceptance/bin/ scripts still use text=True, so this change alone wouldn't buy 3.6 compatibility anyway. universal_newlines is the legacy alias that text replaced in 3.7.

Please drop this file from the PR (one PR = one change). If ./task fails on your machine because of an old python3, the fix is upgrading your local Python.

)
paths = set()
for line in out.splitlines():
Expand Down
Loading