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 @@ -7,6 +7,7 @@
### Bundles
* Modify grants to use SDK types ([#4666](https://github.com/databricks/cli/pull/4666))
* Modify permissions to use SDK types where available. This makes DABs validate permission levels, producing a warning on the unknown ones ([#4686](https://github.com/databricks/cli/pull/4686))
* direct: Sync registered model aliases via SetAlias/DeleteAlias ([#4637](https://github.com/databricks/cli/pull/4637))

### Dependency updates
* Bump databricks-sdk-go from v0.112.0 to v0.119.0 ([#4631](https://github.com/databricks/cli/pull/4631), [#4695](https://github.com/databricks/cli/pull/4695))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
bundle:
name: deploy-registered-models-aliases-$UNIQUE_NAME

resources:
registered_models:
my_registered_model:
name: $NAME
comment: $COMMENT
catalog_name: $CATALOG_NAME
schema_name: $SCHEMA_NAME

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

Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@

>>> export NAME=my-registered-model-[UNIQUE_NAME]

>>> export COMMENT=test model

>>> export CATALOG_NAME=mycatalog-[UNIQUE_NAME]

>>> export SCHEMA_NAME=myschema-[UNIQUE_NAME]

>>> [CLI] catalogs create mycatalog-[UNIQUE_NAME]
{
"full_name": "mycatalog-[UNIQUE_NAME]"
}

>>> [CLI] schemas create myschema-[UNIQUE_NAME] mycatalog-[UNIQUE_NAME]
{
"full_name": "mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME]"
}

=== create with aliases
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-aliases-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] registered-models get mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME].my-registered-model-[UNIQUE_NAME] --include-aliases
{
"aliases": [
{
"alias_name": "champion",
"version_num": 1
},
{
"alias_name": "staging",
"version_num": 2
}
]
}

=== redeploy unchanged (idempotence check)
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-aliases-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

=== update: modify champion version, remove staging, add latest
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-aliases-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] registered-models get mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME].my-registered-model-[UNIQUE_NAME] --include-aliases
{
"aliases": [
{
"alias_name": "champion",
"version_num": 5
},
{
"alias_name": "latest",
"version_num": 3
}
]
}

=== remove all aliases
>>> [CLI] bundle deploy
Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-aliases-[UNIQUE_NAME]/default/files...
Deploying resources...
Updating deployment state...
Deployment complete!

>>> [CLI] registered-models get mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME].my-registered-model-[UNIQUE_NAME] --include-aliases
{
"aliases": []
}

>>> [CLI] bundle destroy --auto-approve
The following resources will be deleted:
delete resources.registered_models.my_registered_model

All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/deploy-registered-models-aliases-[UNIQUE_NAME]/default

Deleting files...
Destroy complete!

>>> [CLI] schemas delete mycatalog-[UNIQUE_NAME].myschema-[UNIQUE_NAME] --force

>>> [CLI] catalogs delete mycatalog-[UNIQUE_NAME] --force
47 changes: 47 additions & 0 deletions acceptance/bundle/resources/registered_models/aliases/script
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
trace export NAME="my-registered-model-$UNIQUE_NAME"
trace export COMMENT="test model"
trace export CATALOG_NAME="mycatalog-${UNIQUE_NAME}"
trace export SCHEMA_NAME="myschema-${UNIQUE_NAME}"
envsubst < databricks.yml.tmpl > databricks.yml

trace $CLI catalogs create ${CATALOG_NAME} | jq '{full_name}'
trace $CLI schemas create ${SCHEMA_NAME} ${CATALOG_NAME} | jq '{full_name}'

# Append aliases to the config.
cat >> databricks.yml <<'YAML'
aliases:
- alias_name: champion
version_num: 1
- alias_name: staging
version_num: 2
YAML

cleanup() {
trace $CLI bundle destroy --auto-approve
trace $CLI schemas delete ${CATALOG_NAME}.${SCHEMA_NAME} --force
trace $CLI catalogs delete ${CATALOG_NAME} --force
}
trap cleanup EXIT

deploy_and_get_aliases() {
trace $CLI bundle deploy
registered_model_id=$($CLI bundle summary --output json | jq -r '.resources.registered_models.my_registered_model.id')
trace $CLI registered-models get "${registered_model_id}" --include-aliases | jq '{aliases: [.aliases[]? | {alias_name, version_num}] | sort_by(.alias_name)}'
}

title "create with aliases"
deploy_and_get_aliases

title "redeploy unchanged (idempotence check)"
trace $CLI bundle deploy

title "update: modify champion version, remove staging, add latest"
update_file.py databricks.yml "version_num: 1" "version_num: 5"
update_file.py databricks.yml "staging" "latest"
update_file.py databricks.yml "version_num: 2" "version_num: 3"
deploy_and_get_aliases

title "remove all aliases"
# Replace the aliases block with no aliases.
envsubst < databricks.yml.tmpl > databricks.yml
deploy_and_get_aliases
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
Cloud = true
Local = true
RecordRequests = false
RunsOnDbr = true
RequiresUnityCatalog = true
99 changes: 96 additions & 3 deletions bundle/direct/dresources/registered_model.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ import (
"context"

"github.com/databricks/cli/bundle/config/resources"
"github.com/databricks/cli/libs/structs/structpath"
"github.com/databricks/cli/libs/utils"
"github.com/databricks/databricks-sdk-go"
"github.com/databricks/databricks-sdk-go/service/catalog"
"golang.org/x/sync/errgroup"
)

// Precalculated paths for HasChange checks.
var pathAliases = structpath.MustParsePath("aliases")

type ResourceRegisteredModel struct {
client *databricks.WorkspaceClient
}
Expand All @@ -19,6 +24,16 @@ func (*ResourceRegisteredModel) New(client *databricks.WorkspaceClient) *Resourc
}
}

func getAliasKey(a catalog.RegisteredModelAlias) (string, string) {
return "alias_name", a.AliasName
}

func (*ResourceRegisteredModel) KeyedSlices() map[string]any {
return map[string]any{
"aliases": getAliasKey,
}
}

func (*ResourceRegisteredModel) PrepareState(input *resources.RegisteredModel) *catalog.CreateRegisteredModelRequest {
return &input.CreateRegisteredModelRequest
}
Expand Down Expand Up @@ -49,7 +64,7 @@ func (*ResourceRegisteredModel) RemapState(model *catalog.RegisteredModelInfo) *
func (r *ResourceRegisteredModel) DoRead(ctx context.Context, id string) (*catalog.RegisteredModelInfo, error) {
return r.client.RegisteredModels.Get(ctx, catalog.GetRegisteredModelRequest{
FullName: id,
IncludeAliases: false,
IncludeAliases: true,
IncludeBrowse: false,
ForceSendFields: nil,
})
Expand All @@ -64,7 +79,17 @@ func (r *ResourceRegisteredModel) DoCreate(ctx context.Context, config *catalog.
return response.FullName, response, nil
}

func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, config *catalog.CreateRegisteredModelRequest, _ Changes) (*catalog.RegisteredModelInfo, error) {
// WaitAfterCreate syncs aliases after the model is created and state is saved.
// The Create API does not apply aliases, so we sync them separately.
func (r *ResourceRegisteredModel) WaitAfterCreate(ctx context.Context, config *catalog.CreateRegisteredModelRequest) (*catalog.RegisteredModelInfo, error) {
fullName := config.CatalogName + "." + config.SchemaName + "." + config.Name
if err := r.syncAliases(ctx, fullName, config.Aliases, []catalog.RegisteredModelAlias{}); err != nil {
return nil, err
}
return nil, nil
}

func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, config *catalog.CreateRegisteredModelRequest, changes Changes) (*catalog.RegisteredModelInfo, error) {
updateRequest := catalog.UpdateRegisteredModelRequest{
FullName: id,
Comment: config.Comment,
Expand All @@ -77,7 +102,9 @@ func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, confi
// Note: TF also does not support changing name without a recreate so the current behavior matches TF.
NewName: "",

Aliases: config.Aliases,
// Aliases are synced separately via SetAlias/DeleteAlias calls because
// the Update API ignores the Aliases field.
Aliases: nil,
BrowseOnly: config.BrowseOnly,
CreatedAt: config.CreatedAt,
CreatedBy: config.CreatedBy,
Expand All @@ -95,6 +122,12 @@ func (r *ResourceRegisteredModel) DoUpdate(ctx context.Context, id string, confi
return nil, err
}

if changes.HasChange(pathAliases) {
if err := r.syncAliases(ctx, id, config.Aliases, nil); err != nil {
return nil, err
}
}

return response, nil
}

Expand All @@ -103,3 +136,63 @@ func (r *ResourceRegisteredModel) DoDelete(ctx context.Context, id string) error
FullName: id,
})
}

// syncAliases compares desired and current aliases and calls SetAlias/DeleteAlias
// APIs to reconcile the difference. The Update API ignores the Aliases field,
// so separate API calls are required.
// If current is nil, the current aliases are fetched from the remote.
func (r *ResourceRegisteredModel) syncAliases(ctx context.Context, fullName string, desired, current []catalog.RegisteredModelAlias) error {
if current == nil {
remote, err := r.client.RegisteredModels.Get(ctx, catalog.GetRegisteredModelRequest{
FullName: fullName,
IncludeAliases: true,
IncludeBrowse: false,
ForceSendFields: nil,
})
if err != nil {
return err
}
current = remote.Aliases
}

desiredByName := make(map[string]int, len(desired))
for _, a := range desired {
desiredByName[a.AliasName] = a.VersionNum
}

currentByName := make(map[string]int, len(current))
for _, a := range current {
currentByName[a.AliasName] = a.VersionNum
}

var eg errgroup.Group

// Set new or updated aliases.
for name, version := range desiredByName {
if v, ok := currentByName[name]; ok && v == version {
continue
}
eg.Go(func() error {
_, err := r.client.RegisteredModels.SetAlias(ctx, catalog.SetRegisteredModelAliasRequest{
FullName: fullName,
Alias: name,
VersionNum: version,
})
return err
})
}

// Delete removed aliases.
for name := range currentByName {
if _, ok := desiredByName[name]; !ok {
eg.Go(func() error {
return r.client.RegisteredModels.DeleteAlias(ctx, catalog.DeleteAliasRequest{
FullName: fullName,
Alias: name,
})
})
}
}

return eg.Wait()
}
Loading
Loading