Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 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
26 changes: 26 additions & 0 deletions .agent/rules/name-prefix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
---
description: Rules for resource name prefixing in apply_presets
globs:
- "bundle/config/mutator/resourcemutator/apply_presets.go"
- "bundle/config/mutator/resourcemutator/apply_target_mode.go"
- "bundle/config/mutator/resourcemutator/apply_target_mode_test.go"
- "bundle/direct/dresources/*.go"
paths:
- "bundle/config/mutator/resourcemutator/apply_presets.go"
- "bundle/config/mutator/resourcemutator/apply_target_mode.go"
- "bundle/config/mutator/resourcemutator/apply_target_mode_test.go"
- "bundle/direct/dresources/*.go"
---

# Rules for resource name prefixing

`presets.name_prefix` and dev-mode renaming live in `bundle/config/mutator/resourcemutator/apply_presets.go`. They prepend a string to a resource field so deployments from the same bundle don't collide across users or targets.

**RULE: Only prefix display-name fields. Never prefix a field the API treats as the primary key / object id.** Prefixing an identity-bearing field changes which remote resource the bundle addresses, not just how it's labeled. The user sees a different name in the UI; the deployment also points at a different object.

To check whether a field is identity-bearing, look at the matching `bundle/direct/dresources/<resource>.go`:

- If `DoCreate` returns the name as the deployment id (e.g. `id := config.Name`) and `DoRead`/`DoUpdate`/`DoDelete` look the resource up by that name, the name is the primary key — do not prefix it.
- If the name is purely cosmetic and the API addresses the resource by a separate id (numeric, UUID, etc.), prefixing is fine.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we can shorten this without any loss of signal or even drop this.

The agents are going to read apply_presets.go anyway and should be able figure out from the comments and existing resources what to do for a new one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed and dropped in 6d240dc — checked with both Claude and Cursor (GPT) for a second opinion on whether a rule like this would actually change agent behavior, and both said they would read apply_presets.go and the test first and that the in-code comment + notRenamedFields list with its primary-key rationale is sufficient evidence. The rule was a restatement that got in the way more than it helped.


When you decide a resource's name must not be prefixed, also add the resource type to `notRenamedFields` in `bundle/config/mutator/resourcemutator/apply_target_mode_test.go`. `TestAppropriateResourcesAreRenamed` enforces the invariant for every resource and will fail loudly if a future change reintroduces prefixing.
1 change: 1 addition & 0 deletions .cursor/rules/name-prefix.mdc
1 change: 1 addition & 0 deletions NEXT_CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
### CLI

### Bundles
* Stop applying `presets.name_prefix` (and the dev-mode `[dev <user>]` rename) to `vector_search_endpoints` ([#5209](https://github.com/databricks/cli/pull/5209)).

### Dependency updates
10 changes: 3 additions & 7 deletions bundle/config/mutator/resourcemutator/apply_presets.go
Original file line number Diff line number Diff line change
Expand Up @@ -290,13 +290,9 @@ func (m *applyPresets) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnos
}
}

// Vector Search Endpoints: Prefix
for _, e := range r.VectorSearchEndpoints {
if e == nil {
continue
}
e.Name = normalizePrefix(prefix) + e.Name
}
// Vector Search Endpoints: no prefix. The endpoint name is the primary key
// (it's what GET/UPDATE/DELETE address by), so prefixing it would change
// the resource's identity rather than just its display name.

return diags
}
Expand Down
71 changes: 42 additions & 29 deletions bundle/config/mutator/resourcemutator/apply_target_mode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -303,8 +303,8 @@ func TestProcessTargetModeDevelopment(t *testing.T) {
// Model serving endpoint 1
assert.Equal(t, "dev_lennart_servingendpoint1", b.Config.Resources.ModelServingEndpoints["servingendpoint1"].Name)

// Vector search endpoint 1
assert.Equal(t, "dev_lennart_vs_endpoint1", b.Config.Resources.VectorSearchEndpoints["vs_endpoint1"].Name)
// Vector search endpoint 1: name is the primary key, so it must not be prefixed.
assert.Equal(t, "vs_endpoint1", b.Config.Resources.VectorSearchEndpoints["vs_endpoint1"].Name)

// Registered model 1
assert.Equal(t, "dev_lennart_registeredmodel1", b.Config.Resources.RegisteredModels["registeredmodel1"].Name)
Expand Down Expand Up @@ -414,17 +414,34 @@ func TestAllResourcesMocked(t *testing.T) {
}
}

// Make sure that we at rename all non UC resources
func TestAllNonUcResourcesAreRenamed(t *testing.T) {
// TestAppropriateResourcesAreRenamed checks that every resource with a user-facing
// Name field is renamed by dev-mode / presets.name_prefix, except for an
// explicit carve-out list. The carve-out applies to resources whose Name is
// the API primary key / object id (not a display name) — prefixing those
// would change the resource's identity rather than its label. See
// .agent/rules/name-prefix.md for the principle.
func TestAppropriateResourcesAreRenamed(t *testing.T) {
b := mockBundle(config.Development)

// UC resources should not have a prefix added to their name. Right now
// this list only contains the Volume, Catalog, and ExternalLocation resources since we have yet to remove
// prefixing support for UC schemas and registered models.
ucFields := []reflect.Type{
notRenamedFields := []reflect.Type{
reflect.TypeFor[*resources.Catalog](),
reflect.TypeFor[*resources.ExternalLocation](),
reflect.TypeFor[*resources.Volume](),
reflect.TypeFor[*resources.VectorSearchEndpoint](),
}

// Resources whose Name is server-generated or otherwise not a user-facing
// label, so the rename matrix doesn't apply. Reflection still finds a
// Name field on these via embedded SDK types, hence the explicit skip.
notUserNamed := []string{
"Apps",
"SecretScopes",
"DatabaseInstances",
"DatabaseCatalogs",
"SyncedDatabaseTables",
"PostgresProjects",
"PostgresBranches",
"PostgresEndpoints",
}

diags := bundle.ApplySeq(t.Context(), b, ApplyTargetMode(), ApplyPresets())
Expand All @@ -433,28 +450,24 @@ func TestAllNonUcResourcesAreRenamed(t *testing.T) {
resources := reflect.ValueOf(b.Config.Resources)
for i := range resources.NumField() {
field := resources.Field(i)
if field.Kind() != reflect.Map {
continue
}
resourceType := resources.Type().Field(i).Name
if slices.Contains(notUserNamed, resourceType) {
continue
}
for _, key := range field.MapKeys() {
resource := field.MapIndex(key)
nameField := resource.Elem().FieldByName("Name")
if !nameField.IsValid() || nameField.Kind() != reflect.String {
continue
}

if field.Kind() == reflect.Map {
for _, key := range field.MapKeys() {
resource := field.MapIndex(key)
nameField := resource.Elem().FieldByName("Name")
resourceType := resources.Type().Field(i).Name

// Skip resources that are not renamed (either because they don't have a user-facing Name field,
// or because their Name is server-generated rather than user-specified)
if resourceType == "Apps" || resourceType == "SecretScopes" || resourceType == "DatabaseInstances" || resourceType == "DatabaseCatalogs" || resourceType == "SyncedDatabaseTables" || resourceType == "PostgresProjects" || resourceType == "PostgresBranches" || resourceType == "PostgresEndpoints" {
continue
}

if !nameField.IsValid() || nameField.Kind() != reflect.String {
continue
}

if slices.Contains(ucFields, resource.Type()) {
assert.NotContains(t, nameField.String(), "dev", "process_target_mode should not rename '%s' in '%s'", key, resources.Type().Field(i).Name)
} else {
assert.Contains(t, nameField.String(), "dev", "process_target_mode should rename '%s' in '%s'", key, resources.Type().Field(i).Name)
}
if slices.Contains(notRenamedFields, resource.Type()) {
assert.NotContains(t, nameField.String(), "dev", "process_target_mode should not rename '%s' in '%s'", key, resourceType)
} else {
assert.Contains(t, nameField.String(), "dev", "process_target_mode should rename '%s' in '%s'", key, resourceType)
}
}
}
Expand Down
Loading