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
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

### Bundles
* `bundle run` now prints the modern job run URL (`/jobs/<id>/runs/<id>`) so that non-admin users permitted to view the run are taken to the run instead of the workspace homepage.
* References to a registered model's `registered_model_id` now resolve under the direct engine, matching Terraform behavior ([#5621](https://github.com/databricks/cli/pull/5621)).
* Fix missing field descriptions in the bundle JSON schema for fields whose upstream API docs arrived after the field was first annotated (e.g. `vector_search_endpoints.*.target_qps`); stale placeholder markers no longer hide them ([#5588](https://github.com/databricks/cli/pull/5588)).

### Dependency updates
Expand Down
16 changes: 16 additions & 0 deletions acceptance/bundle/resource_deps/model_id_ref/databricks.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
bundle:
name: test-bundle

# A model's numeric ID is named registered_model_id in Terraform state and model_id in
# direct state. terraform_dabs_map bridges the two names, so a reference to
# registered_model_id resolves on both engines.
resources:
models:
my_model:
name: my-model
description: my model

jobs:
consumer:
name: consumer
description: model id is ${resources.models.my_model.registered_model_id}
3 changes: 3 additions & 0 deletions acceptance/bundle/resource_deps/model_id_ref/out.test.toml

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

42 changes: 42 additions & 0 deletions acceptance/bundle/resource_deps/model_id_ref/output.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@

=== the consumer job implicitly depends on the model
>>> [CLI] bundle plan
create jobs.consumer
create models.my_model

Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged

=== after deploy, registered_model_id is resolved to the model's numeric id
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> print_requests.py //mlflow/registered-models/create //jobs/create --sort
{
"method": "POST",
"path": "/api/2.0/mlflow/registered-models/create",
"body": {
"description": "my model",
"name": "my-model"
}
}
{
"method": "POST",
"path": "/api/2.2/jobs/create",
"body": {
"deployment": {
"kind": "BUNDLE",
"metadata_file_path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/metadata.json"
},
"description": "model id is [MY_MODEL_ID]",
"edit_mode": "UI_LOCKED",
"format": "MULTI_TASK",
"max_concurrent_runs": 1,
"name": "consumer",
"queue": {
"enabled": true
}
}
}
13 changes: 13 additions & 0 deletions acceptance/bundle/resource_deps/model_id_ref/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
title "the consumer job implicitly depends on the model"
trace $CLI bundle plan

title "after deploy, registered_model_id is resolved to the model's numeric id"
trace $CLI bundle deploy

# Bind the model's exact numeric id to [MY_MODEL_ID] so the resolved reference is matched
# precisely rather than by the broad [NUMID] pattern. The id comes from the API (independent
# of the deploy engine), so terraform and direct must both resolve to this exact value.
model_id=$($CLI model-registry get-model my-model | jq -r '.registered_model_databricks.id')
add_repl.py "$model_id" MY_MODEL_ID

trace print_requests.py //mlflow/registered-models/create //jobs/create --sort
68 changes: 60 additions & 8 deletions bundle/terraform_dabs_map/generate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,15 +74,25 @@ var tfKnownSegments = map[string]bool{
"provider_config": true, // Terraform provider metadata, not a DABs concept
}

// manualRenames maps DABs group → DABs field path → TF field path for renames the
// heuristic matcher cannot derive. These are state-computed fields that live in the
// RemoteType (not the config struct), whose DABs and TF names are semantically equivalent
// but lexically unrelated, so neither exact nor stemmed matching can connect them.
var manualRenames = map[string]map[string]string{
// models.model_id is the numeric model ID; TF names it registered_model_id.
"models": {"model_id": "registered_model_id"},
}

type groupResult struct {
group string
tfType string
hasTFType bool
renames map[string]string // TF path → DABs path (renamed fields only)
unwraps []string // TF paths that are structural wrappers (Unwrap: true)
dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent
tfOnly map[string]bool // TF clean paths with no DABs equivalent
matchCount int // used for stats output only, not written to generated.go
group string
tfType string
hasTFType bool
renames map[string]string // TF path → DABs path (renamed fields only)
unwraps []string // TF paths that are structural wrappers (Unwrap: true)
dabsOnly map[string]bool // DABs clean paths with no Terraform equivalent
tfOnly map[string]bool // TF clean paths with no DABs equivalent
matchCount int // used for stats output only, not written to generated.go
tfWrapperFirstSegs map[string]bool // first-level DABs field names that go under the wrapper (only set when unwraps is non-empty)
}

func buildAll() ([]groupResult, error) {
Expand Down Expand Up @@ -248,6 +258,32 @@ func buildGroup(group string, adapter *dresources.Adapter) (groupResult, error)
res.unwraps = append(res.unwraps, wrapper)
}

// Collect first-level DABs field names that go under the wrapper for groups with wrappers.
if len(res.unwraps) > 0 {
res.tfWrapperFirstSegs = make(map[string]bool)
for _, wrapper := range res.unwraps {
prefix := wrapper + "."
for tf := range tfFields {
if matchedTF[tf] {
if after, ok := strings.CutPrefix(tf, prefix); ok {
res.tfWrapperFirstSegs[topSegment(after)] = true
}
}
}
}
}

// Apply manual renames for fields the heuristic matcher cannot derive. These connect a
// state-computed DABs field to its differently-named TF counterpart; recording them as
// renames also marks the TF field matched so it does not surface as Terraform-only.
for dabsPath, tfPath := range manualRenames[group] {
if !tfFields[tfPath] {
return groupResult{}, fmt.Errorf("manual rename %s.%s: TF field %q not found", group, dabsPath, tfPath)
}
res.renames[tfPath] = dabsPath
matchedTF[tfPath] = true
}

// Step 4: remaining unmatched fields.
for dabs := range dabsFields {
if !matchedDABs[dabs] && !dabsKnownFields[topSegment(dabs)] {
Expand Down Expand Up @@ -463,6 +499,22 @@ func renderSource(results []groupResult) ([]byte, error) {
w("\t%q: %q,\n", r.group, wrapper)
}
}
w("}\n\n")

w("// DABsToTerraformWrapperFields maps DABs group name → first-level DABs field names that\n")
w("// live under the TF wrapper. For wrapper groups, a DABs path is prefixed with the wrapper\n")
w("// in DABsPathToTerraform only when its first segment appears here.\n")
w("var DABsToTerraformWrapperFields = map[string]FieldSet{\n")
for _, r := range results {
if !r.hasTFType || len(r.tfWrapperFirstSegs) == 0 {
continue
}
w("\t%q: {\n", r.group)
for _, key := range slices.Sorted(maps.Keys(r.tfWrapperFirstSegs)) {
w("\t\t%q: {},\n", key)
}
w("\t},\n")
}
w("}\n")

return format.Source([]byte(b.String()))
Expand Down
75 changes: 71 additions & 4 deletions bundle/terraform_dabs_map/generated.go

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

26 changes: 18 additions & 8 deletions bundle/terraform_dabs_map/translate.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,14 @@ import (
// DABsPathToTerraform translates a field path from DABs naming conventions
// to Terraform naming conventions for the given resource group.
//
// It is the inverse of TerraformPathToDABs. For groups whose TF schema wraps fields
// under a structural prefix (e.g. "spec"), that prefix is prepended to the result.
// Each field name segment is looked up in the DABsToTerraformRenameMap: when found the TF
// name is used and the tree descends for the remainder of the path. Array indices pass
// through unchanged without advancing the tree position. An unrecognised segment stops
// further renaming; remaining segments are kept as-is. Returns nil when path is nil.
// It is the inverse of TerraformPathToDABs. For groups whose TF schema wraps config fields
// under a structural prefix (e.g. "spec"), that prefix is prepended when the path's first
// segment is listed in DABsToTerraformWrapperFields. Root-level fields and unrecognised
// segments pass through without the wrapper. Each field name segment is looked up in the
// DABsToTerraformRenameMap: when found the TF name is used and the tree descends for the
// remainder of the path. Array indices pass through unchanged without advancing the tree
// position. An unrecognised segment stops further renaming; remaining segments are kept
// as-is. Returns nil when path is nil.
// Returns an error when path is a known DABs-only field with no Terraform equivalent.
//
// The path must be relative to the resource root (e.g. "tasks", not
Expand All @@ -28,10 +30,18 @@ func DABsPathToTerraform(group string, path *structpath.PathNode) (*structpath.P
return nil, fmt.Errorf("%s: %q is a DABs-only field with no Terraform equivalent", group, path)
}

// For groups with a TF wrapper (Unwrap inverse), prepend it as the first segment.
// For groups with a TF wrapper, prepend it only when the first segment is a known
// spec field. Unknown paths (root-level outputs, unrecognised segments) pass through unchanged.
var result *structpath.PathNode
if wrapper, ok := DABsToTerraformWrappers[group]; ok {
result = structpath.NewDotString(nil, wrapper)
segs := path.AsSlice()
if len(segs) > 0 {
if firstKey, ok := segs[0].StringKey(); ok {
if _, isWrapped := DABsToTerraformWrapperFields[group][firstKey]; isWrapped {
result = structpath.NewDotString(nil, wrapper)
}
}
}
}

tree := DABsToTerraformRenameMap[group]
Expand Down
Loading
Loading