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
2 changes: 2 additions & 0 deletions acceptance/pipelines/describe/basic/out.test.toml

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

57 changes: 57 additions & 0 deletions acceptance/pipelines/describe/basic/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@

=== describe by pipeline id

>>> [CLI] pipelines describe [UUID]
Pipeline: My Pipeline
ID: [UUID]
State: IDLE
Health: HEALTHY
Creator: alice@example.com
Run as: alice@example.com
Target: main.sales
Mode: Triggered, Development
Compute: Serverless
Channel: CURRENT

Last run:
Update ID: upd-9
State: COMPLETED
Started: [TIMESTAMP]
Refreshed: [orders, customers]
Cause: API_CALL

=== json output

>>> [CLI] pipelines describe [UUID] --output json
{
"pipeline": {
"creator_user_name": "alice@example.com",
"health": "HEALTHY",
"latest_updates": [
{
"update_id": "upd-9"
}
],
"name": "My Pipeline",
"pipeline_id": "[UUID]",
"run_as_user_name": "alice@example.com",
"spec": {
"catalog": "main",
"channel": "CURRENT",
"development": true,
"schema": "sales",
"serverless": true
},
"state": "IDLE"
},
"last_update": {
"cause": "API_CALL",
"creation_time": [NUMID],
"refresh_selection": [
"orders",
"customers"
],
"state": "COMPLETED",
"update_id": "upd-9"
}
}
5 changes: 5 additions & 0 deletions acceptance/pipelines/describe/basic/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
title "describe by pipeline id\n"
trace $CLI pipelines describe 3fb8e5a1-0d2c-4a6b-9f1e-2c7d8e9f0a1b

title "json output\n"
trace $CLI pipelines describe 3fb8e5a1-0d2c-4a6b-9f1e-2c7d8e9f0a1b --output json
39 changes: 39 additions & 0 deletions acceptance/pipelines/describe/basic/test.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
Cloud = false

EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["direct"]

# describe by PIPELINE_ID fetches the pipeline, then its most recent update.
[[Server]]
Pattern = "GET /api/2.0/pipelines/{pipeline_id}"
Response.Body = '''
{
"pipeline_id": "3fb8e5a1-0d2c-4a6b-9f1e-2c7d8e9f0a1b",
"name": "My Pipeline",
"state": "IDLE",
"health": "HEALTHY",
"creator_user_name": "alice@example.com",
"run_as_user_name": "alice@example.com",
"latest_updates": [ { "update_id": "upd-9" } ],
"spec": {
"catalog": "main",
"schema": "sales",
"development": true,
"serverless": true,
"channel": "CURRENT"
}
}
'''

[[Server]]
Pattern = "GET /api/2.0/pipelines/{pipeline_id}/updates/{update_id}"
Response.Body = '''
{
"update": {
"update_id": "upd-9",
"state": "COMPLETED",
"creation_time": 1640995200000,
"refresh_selection": ["orders", "customers"],
"cause": "API_CALL"
}
}
'''
1 change: 1 addition & 0 deletions cmd/pipelines/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ func Commands() []*cobra.Command {
dryRunCommand(),
historyCommand(),
logsCommand(),
describeCommand(),
openCommand(),
showCommand(),
}
Expand Down
109 changes: 109 additions & 0 deletions cmd/pipelines/describe.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
package pipelines

import (
"context"
"fmt"
"regexp"

"github.com/databricks/cli/cmd/bundle/utils"
"github.com/databricks/cli/cmd/root"
"github.com/databricks/cli/libs/cmdctx"
"github.com/databricks/cli/libs/cmdio"
databricks "github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/pipelines"
"github.com/spf13/cobra"
)

var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)

// LooksLikeUUID reports whether s is a pipeline ID (UUID) rather than a bundle KEY.
func LooksLikeUUID(s string) bool {
return uuidRegex.MatchString(s)
}

// pipelineDescribeData is the payload rendered by `pipelines describe`: the
// pipeline definition and current state, plus its most recent update (if any).
type pipelineDescribeData struct {
Key string `json:"key,omitempty"`
Pipeline *pipelines.GetPipelineResponse `json:"pipeline"`
LastUpdate *pipelines.UpdateInfo `json:"last_update,omitempty"`
}

func describeCommand() *cobra.Command {
cmd := &cobra.Command{
Use: "describe [flags] KEY|PIPELINE_ID",
Args: root.MaximumNArgs(1),
Short: "Show a summary of a pipeline",
Long: `Show a pipeline's configuration and current state, plus the result of its
most recent update. Identify the pipeline by bundle KEY, or by PIPELINE_ID
(a UUID) to describe any pipeline in the workspace without a bundle.`,
// Hidden while the command is still limited to basic pipeline info;
// unhide once richer output (datasets, DAG, lineage) is available.
Hidden: true,
}

cmd.RunE = func(cmd *cobra.Command, args []string) error {
// A raw pipeline ID (UUID) is addressed directly, without a bundle, so
// describe works for pipelines not defined in the current bundle.
if len(args) == 1 && LooksLikeUUID(args[0]) {
if err := root.MustWorkspaceClient(cmd, args); err != nil {
return err
}
ctx := cmd.Context()
w := cmdctx.WorkspaceClient(ctx)
return describePipeline(ctx, w, "", args[0])
}

b, err := utils.ProcessBundle(cmd, utils.ProcessOptions{
InitIDs: true,
})
if err != nil {
return err
}
ctx := cmd.Context()

key, err := resolvePipelineArgument(ctx, b, args)
if err != nil {
return err
}

pipelineId, err := resolvePipelineIdFromKey(ctx, b, key)
if err != nil {
return err
}

w := b.WorkspaceClient(ctx)
return describePipeline(ctx, w, key, pipelineId)
}

return cmd
}

// describePipeline fetches the pipeline and its most recent update (if any) and
// renders the summary. key is the bundle KEY when addressed that way, or empty
// when addressed by pipeline ID.
func describePipeline(ctx context.Context, w *databricks.WorkspaceClient, key, pipelineId string) error {
pipeline, err := w.Pipelines.Get(ctx, pipelines.GetPipelineRequest{PipelineId: pipelineId})
if err != nil {
return fmt.Errorf("failed to get pipeline %s: %w", pipelineId, err)
}

// Enrich with the most recent update, when the pipeline has run before.
// LatestUpdates is ordered newest-first.
var lastUpdate *pipelines.UpdateInfo
if len(pipeline.LatestUpdates) > 0 {
updateId := pipeline.LatestUpdates[0].UpdateId
resp, err := w.Pipelines.GetUpdate(ctx, pipelines.GetUpdateRequest{PipelineId: pipelineId, UpdateId: updateId})
if err != nil {
return fmt.Errorf("failed to get latest update %s: %w", updateId, err)
}
lastUpdate = resp.Update
}

data := pipelineDescribeData{
Key: key,
Pipeline: pipeline,
LastUpdate: lastUpdate,
}
return cmdio.RenderWithTemplate(ctx, data, "", pipelineDescribeTemplate)
}
47 changes: 47 additions & 0 deletions cmd/pipelines/describe_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package pipelines

import (
"testing"

"github.com/databricks/cli/libs/cmdio"
"github.com/databricks/databricks-sdk-go/service/pipelines"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestLooksLikeUUID(t *testing.T) {
tests := []struct {
in string
want bool
}{
{"3fb8e5a1-0d2c-4a6b-9f1e-2c7d8e9f0a1b", true},
{"my_pipeline", false},
{"my-pipeline", false},
{"3FB8E5A1-0D2C-4A6B-9F1E-2C7D8E9F0A1B", false}, // uppercase is treated as a KEY
{"3fb8e5a1-0d2c-4a6b-9f1e-2c7d8e9f0a1", false}, // too short
{"", false},
}
for _, tt := range tests {
assert.Equal(t, tt.want, LooksLikeUUID(tt.in), "looksLikeUUID(%q)", tt.in)
}
}

// The populated render is golden-tested in acceptance/pipelines/describe/basic;
// this covers only the has-never-run branch.
func TestPipelineDescribeTemplateNoRuns(t *testing.T) {
data := pipelineDescribeData{
Pipeline: &pipelines.GetPipelineResponse{
Name: "Fresh Pipeline",
PipelineId: "def-456",
Spec: &pipelines.PipelineSpec{},
},
}

ctx, out := cmdio.NewTestContextWithStdout(t.Context())
require.NoError(t, cmdio.RenderWithTemplate(ctx, data, "", pipelineDescribeTemplate))

got := out.String()
assert.Contains(t, got, "Pipeline: Fresh Pipeline")
assert.Contains(t, got, "No runs yet.")
assert.NotContains(t, got, "Update ID:")
}
56 changes: 56 additions & 0 deletions cmd/pipelines/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,62 @@ Pipeline configurations for this update:
{{- end }}
`

// pipelineDescribeTemplate renders the `pipelines describe` summary: pipeline
// configuration and state, followed by the most recent update (if any).
const pipelineDescribeTemplate = `Pipeline: {{if .Pipeline.Name}}{{.Pipeline.Name}}{{else}}{{.Pipeline.PipelineId}}{{end}}
{{- if .Key}}
Key: {{.Key}}
{{- end}}
ID: {{.Pipeline.PipelineId}}
{{- if .Pipeline.State}}
State: {{.Pipeline.State}}
{{- end}}
{{- if .Pipeline.Health}}
Health: {{.Pipeline.Health}}
{{- end}}
{{- if .Pipeline.CreatorUserName}}
Creator: {{.Pipeline.CreatorUserName}}
{{- end}}
{{- if .Pipeline.RunAsUserName}}
Run as: {{.Pipeline.RunAsUserName}}
{{- end}}
{{- with .Pipeline.Spec}}
{{- if .Catalog}}
Target: {{.Catalog}}{{if .Schema}}.{{.Schema}}{{end}}
{{- end}}
Mode: {{if .Continuous}}Continuous{{else}}Triggered{{end}}, {{if .Development}}Development{{else}}Production{{end}}
Compute: {{if .Serverless}}Serverless{{else if $.Pipeline.ClusterId}}Classic ({{$.Pipeline.ClusterId}}){{else}}Classic{{end}}
{{- if .Channel}}
Channel: {{.Channel}}
{{- end}}
{{- end}}

Last run:
{{- if .LastUpdate}}
Update ID: {{.LastUpdate.UpdateId}}
{{- if .LastUpdate.State}}
State: {{.LastUpdate.State}}
{{- end}}
{{- if .LastUpdate.CreationTime}}
Started: {{.LastUpdate.CreationTime | pretty_UTC_date_from_millis}}
{{- end}}
{{- if .LastUpdate.FullRefresh}}
Full refresh: all tables
{{- end}}
{{- if .LastUpdate.RefreshSelection}}
Refreshed: [{{join .LastUpdate.RefreshSelection ", "}}]
{{- end}}
{{- if .LastUpdate.FullRefreshSelection}}
Full refreshed: [{{join .LastUpdate.FullRefreshSelection ", "}}]
{{- end}}
{{- if .LastUpdate.Cause}}
Cause: {{.LastUpdate.Cause}}
{{- end}}
{{- else}}
No runs yet.
{{- end}}
`

// progressEventsTemplate is the template for displaying progress events
const progressEventsTemplate = `{{- if .ProgressEvents }}
{{ printf "%-25s %s\n" "Run Phase" "Duration" }}
Expand Down
10 changes: 1 addition & 9 deletions cmd/workspace/pipelines/overrides.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package pipelines

import (
"regexp"
"slices"

pipelinesCli "github.com/databricks/cli/cmd/pipelines"
Expand Down Expand Up @@ -47,7 +46,7 @@ func init() {
originalRunE := cmd.RunE
cmd.RunE = func(cmd *cobra.Command, args []string) error {
// For compatibility, if argument looks like pipeline ID, use API
if len(args) > 0 && looksLikeUUID(args[0]) {
if len(args) > 0 && pipelinesCli.LooksLikeUUID(args[0]) {
return originalRunE(cmd, args)
}
// Looks like a bundle key or no args - use Lakeflow stop
Expand All @@ -70,10 +69,3 @@ If there is only one pipeline in the bundle, KEY is optional.
With a PIPELINE_ID: Stops the pipeline identified by the UUID using the API.`
})
}

var uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)

// looksLikeUUID checks if a string matches the UUID format with lowercase hex digits
func looksLikeUUID(s string) bool {
return uuidRegex.MatchString(s)
}
15 changes: 0 additions & 15 deletions cmd/workspace/pipelines/overrides_test.go

This file was deleted.

Loading