From c65ab0bde40547745356a642604bf0e86ba5c828 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 02:21:25 +0200 Subject: [PATCH 01/22] Add support for experiments in direct deployment - Add ResourceExperiment adapter implementing CRUD operations for MLflow experiments - Add experiments to SupportedResources in direct deployment bundle - Add acceptance test for experiment deployment in both terraform and direct modes - Enable creating, updating, and deleting experiments via direct API calls This allows experiments to be deployed using DATABRICKS_BUNDLE_ENGINE=direct-exp, providing an alternative to the terraform-based deployment approach. --- .../experiments/basic/databricks.yml.tmpl | 8 ++ .../deploy/experiments/basic/output.txt | 30 +++++++ .../bundle/deploy/experiments/basic/script | 11 +++ .../bundle/deploy/experiments/basic/test.toml | 6 ++ bundle/direct/dresources/all.go | 1 + bundle/direct/dresources/experiment.go | 87 +++++++++++++++++++ 6 files changed, 143 insertions(+) create mode 100644 acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl create mode 100644 acceptance/bundle/deploy/experiments/basic/output.txt create mode 100755 acceptance/bundle/deploy/experiments/basic/script create mode 100644 acceptance/bundle/deploy/experiments/basic/test.toml create mode 100644 bundle/direct/dresources/experiment.go diff --git a/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl b/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl new file mode 100644 index 0000000000..1194d33bbc --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl @@ -0,0 +1,8 @@ +bundle: + name: experiment-basic-$UNIQUE_NAME + +resources: + experiments: + my_experiment: + name: "/Users/${CURRENT_USER_NAME}/test-experiment-${UNIQUE_NAME}" + description: "Test experiment for bundle deployment" diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt new file mode 100644 index 0000000000..4959343318 --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -0,0 +1,30 @@ +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] experiments get-experiment [NUMID] | jq '{name: .experiment.name, description: .experiment.description, lifecycle_stage: .experiment.lifecycle_stage}' +{ + "name": "/Users/[USERNAME]/test-experiment-[UNIQUE_NAME]", + "description": "Test experiment for bundle deployment", + "lifecycle_stage": "active" +} + +>>> [CLI] bundle destroy --auto-approve +The following resources will be deleted: + delete experiment my_experiment + +All files and directories at the following location will be deleted: /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default + +Deleting files... +Destroy complete! + +>>> [CLI] bundle summary +Name: experiment-basic-[UNIQUE_NAME] +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default + +Resources: diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script new file mode 100755 index 0000000000..2a6fb36bc3 --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -0,0 +1,11 @@ +envsubst < databricks.yml.tmpl > databricks.yml + +trace $CLI bundle deploy + +experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') + +trace $CLI experiments get-experiment $experiment_id | jq '{name: .experiment.name, description: .experiment.description, lifecycle_stage: .experiment.lifecycle_stage}' + +trace $CLI bundle destroy --auto-approve + +trace $CLI bundle summary diff --git a/acceptance/bundle/deploy/experiments/basic/test.toml b/acceptance/bundle/deploy/experiments/basic/test.toml new file mode 100644 index 0000000000..2fff4443ab --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/test.toml @@ -0,0 +1,6 @@ +Cloud = false +Local = false + +# Test both terraform and direct deployment engines +[EnvMatrix] +DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/bundle/direct/dresources/all.go b/bundle/direct/dresources/all.go index a93d723e01..37994aa723 100644 --- a/bundle/direct/dresources/all.go +++ b/bundle/direct/dresources/all.go @@ -9,6 +9,7 @@ import ( var SupportedResources = map[string]any{ "jobs": (*ResourceJob)(nil), "pipelines": (*ResourcePipeline)(nil), + "experiments": (*ResourceExperiment)(nil), "schemas": (*ResourceSchema)(nil), "volumes": (*ResourceVolume)(nil), "apps": (*ResourceApp)(nil), diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go new file mode 100644 index 0000000000..9147023667 --- /dev/null +++ b/bundle/direct/dresources/experiment.go @@ -0,0 +1,87 @@ +package dresources + +import ( + "context" + "fmt" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/deployplan" + "github.com/databricks/databricks-sdk-go" + "github.com/databricks/databricks-sdk-go/service/ml" +) + +type ResourceExperiment struct { + client *databricks.WorkspaceClient +} + +func (*ResourceExperiment) New(client *databricks.WorkspaceClient) *ResourceExperiment { + return &ResourceExperiment{ + client: client, + } +} + +func (*ResourceExperiment) PrepareState(input *resources.MlflowExperiment) *ml.CreateExperiment { + return &ml.CreateExperiment{ + Name: input.Name, + ArtifactLocation: input.ArtifactLocation, + Tags: input.Tags, + } +} + +func (*ResourceExperiment) RemapState(response *ml.GetExperimentResponse) *ml.CreateExperiment { + experiment := response.Experiment + return &ml.CreateExperiment{ + Name: experiment.Name, + ArtifactLocation: experiment.ArtifactLocation, + Tags: experiment.Tags, + } +} + +func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.GetExperimentResponse, error) { + result, err := r.client.Experiments.GetExperiment(ctx, ml.GetExperimentRequest{ + ExperimentId: id, + }) + if err != nil { + return nil, fmt.Errorf("failed to get experiment %s: %w", id, err) + } + return result, nil +} + +func (r *ResourceExperiment) DoCreate(ctx context.Context, state *ml.CreateExperiment) (string, error) { + result, err := r.client.Experiments.CreateExperiment(ctx, *state) + if err != nil { + return "", fmt.Errorf("failed to create experiment: %w", err) + } + return result.ExperimentId, nil +} + +func (r *ResourceExperiment) DoUpdate(ctx context.Context, id string, state *ml.CreateExperiment) error { + updateReq := ml.UpdateExperiment{ + ExperimentId: id, + NewName: state.Name, + } + + err := r.client.Experiments.UpdateExperiment(ctx, updateReq) + if err != nil { + return fmt.Errorf("failed to update experiment %s: %w", id, err) + } + return nil +} + +func (r *ResourceExperiment) DoDelete(ctx context.Context, id string) error { + err := r.client.Experiments.DeleteExperiment(ctx, ml.DeleteExperiment{ + ExperimentId: id, + }) + if err != nil { + return fmt.Errorf("failed to delete experiment %s: %w", id, err) + } + return nil +} + +func (*ResourceExperiment) FieldTriggers() map[string]deployplan.ActionType { + return map[string]deployplan.ActionType{ + "name": deployplan.ActionTypeUpdate, + "artifact_location": deployplan.ActionTypeRecreate, + "tags": deployplan.ActionTypeUpdate, + } +} From 69d0f4895ad0fac6e31676c325ef91459a23dff5 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 03:58:38 +0200 Subject: [PATCH 02/22] Add support for experiments in direct deployment --- .../experiments/basic/databricks.yml.tmpl | 8 - .../basic/out.additional_tag.direct-exp.txt | 1 + .../basic/out.additional_tag.terraform.txt | 3 + .../deploy/experiments/basic/out.test.toml | 5 + .../deploy/experiments/basic/output.txt | 178 ++++++++++++++++-- .../bundle/deploy/experiments/basic/script | 36 +++- .../experiments/basic/templates/one_tag.tmpl | 11 ++ .../experiments/basic/templates/two_tag.tmpl | 11 ++ .../bundle/deploy/experiments/basic/test.toml | 4 +- acceptance/bundle/refschema/out.fields.txt | 21 +++ bundle/direct/dresources/all_test.go | 13 ++ bundle/direct/dresources/experiment.go | 16 +- libs/testserver/experiments.go | 145 ++++++++++++++ libs/testserver/fake_workspace.go | 3 + libs/testserver/handlers.go | 18 ++ 15 files changed, 439 insertions(+), 34 deletions(-) delete mode 100644 acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl create mode 100644 acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt create mode 100644 acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt create mode 100644 acceptance/bundle/deploy/experiments/basic/out.test.toml create mode 100644 acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl create mode 100644 acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl create mode 100644 libs/testserver/experiments.go diff --git a/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl b/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl deleted file mode 100644 index 1194d33bbc..0000000000 --- a/acceptance/bundle/deploy/experiments/basic/databricks.yml.tmpl +++ /dev/null @@ -1,8 +0,0 @@ -bundle: - name: experiment-basic-$UNIQUE_NAME - -resources: - experiments: - my_experiment: - name: "/Users/${CURRENT_USER_NAME}/test-experiment-${UNIQUE_NAME}" - description: "Test experiment for bundle deployment" diff --git a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt new file mode 100644 index 0000000000..c54c9d511c --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt @@ -0,0 +1 @@ +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt new file mode 100644 index 0000000000..986d3cf68c --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt @@ -0,0 +1,3 @@ +update experiments.my_experiment + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/out.test.toml b/acceptance/bundle/deploy/experiments/basic/out.test.toml new file mode 100644 index 0000000000..43c8f792f5 --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/out.test.toml @@ -0,0 +1,5 @@ +Local = true +Cloud = true + +[EnvMatrix] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index 4959343318..a1595b2769 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -1,14 +1,175 @@ + +=== create the experiment +>>> [CLI] bundle plan +create experiments.my_experiment + +Plan: 1 to add, 0 to change, 0 to delete, 0 unchanged + >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... Deploying resources... Updating deployment state... Deployment complete! ->>> [CLI] experiments get-experiment [NUMID] | jq '{name: .experiment.name, description: .experiment.description, lifecycle_stage: .experiment.lifecycle_stage}' +>>> [CLI] experiments get-experiment 1 { - "name": "/Users/[USERNAME]/test-experiment-[UNIQUE_NAME]", - "description": "Test experiment for bundle deployment", - "lifecycle_stage": "active" + "name": "/Users/[USERNAME]/original-[UNIQUE_NAME]", + "artifact_location": "s3://original-[UNIQUE_NAME]", + "tags": [ + { + "key": "k1", + "value": "v1" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.experiment.sourceName", + "value": "/Users/[USERNAME]/original-[UNIQUE_NAME]" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.ownerEmail", + "value": "[USERNAME]" + }, + { + "key": "mlflow.experimentType", + "value": "MLFLOW_EXPERIMENT" + } + ] +} + +=== update the name +>>> [CLI] bundle plan +update experiments.my_experiment + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] experiments get-experiment 1 +{ + "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", + "artifact_location": "s3://original-[UNIQUE_NAME]", + "tags": [ + { + "key": "k1", + "value": "v1" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.experiment.sourceName", + "value": "/Users/[USERNAME]/original-[UNIQUE_NAME]" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.ownerEmail", + "value": "[USERNAME]" + }, + { + "key": "mlflow.experimentType", + "value": "MLFLOW_EXPERIMENT" + } + ] +} + +=== updating the artifact location should cause a recreation +>>> [CLI] bundle plan +recreate experiments.my_experiment + +Plan: 1 to add, 0 to change, 1 to delete, 0 unchanged + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] experiments get-experiment 1 +{ + "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", + "artifact_location": "s3://new-[UNIQUE_NAME]", + "tags": [ + { + "key": "k1", + "value": "v1" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.experiment.sourceName", + "value": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.ownerEmail", + "value": "[USERNAME]" + }, + { + "key": "mlflow.experimentType", + "value": "MLFLOW_EXPERIMENT" + } + ] +} + +=== adding a new tag should be a no-op for direct deployment. It is not for Terraform because of a persistent drift bug +>>> [CLI] bundle plan + +>>> [CLI] bundle deploy +Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... +Deploying resources... +Updating deployment state... +Deployment complete! + +>>> [CLI] experiments get-experiment 1 +{ + "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", + "artifact_location": "s3://new-[UNIQUE_NAME]", + "tags": [ + { + "key": "k1", + "value": "v1" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.experiment.sourceName", + "value": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]" + }, + { + "key": "mlflow.ownerId", + "value": "[USERID]" + }, + { + "key": "mlflow.ownerEmail", + "value": "[USERNAME]" + }, + { + "key": "mlflow.experimentType", + "value": "MLFLOW_EXPERIMENT" + } + ] } >>> [CLI] bundle destroy --auto-approve @@ -19,12 +180,3 @@ All files and directories at the following location will be deleted: /Workspace/ Deleting files... Destroy complete! - ->>> [CLI] bundle summary -Name: experiment-basic-[UNIQUE_NAME] -Target: default -Workspace: - User: [USERNAME] - Path: /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default - -Resources: diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index 2a6fb36bc3..ca285fd28b 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -1,11 +1,37 @@ -envsubst < databricks.yml.tmpl > databricks.yml +cleanup() { + trace $CLI bundle destroy --auto-approve +} -trace $CLI bundle deploy +trap cleanup EXIT +title "create the experiment" +export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" +export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" +envsubst < templates/one_tag.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') +trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' -trace $CLI experiments get-experiment $experiment_id | jq '{name: .experiment.name, description: .experiment.description, lifecycle_stage: .experiment.lifecycle_stage}' +title "update the name" +export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/new-name-${UNIQUE_NAME}" +envsubst < templates/one_tag.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy +experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') +trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' -trace $CLI bundle destroy --auto-approve +title "updating the artifact location should cause a recreation" +export EXPERIMENT_ARTIFACT_LOCATION="s3://new-${UNIQUE_NAME}" +envsubst < templates/one_tag.tmpl > databricks.yml +trace $CLI bundle plan +trace $CLI bundle deploy +experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') +trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' -trace $CLI bundle summary +title "adding a new tag should be a no-op for direct deployment. It is not for Terraform because of a persistent drift bug" +envsubst < templates/two_tag.tmpl > databricks.yml +trace $CLI bundle plan > out.additional_tag.$DATABRICKS_BUNDLE_ENGINE.txt +trace $CLI bundle deploy +experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') +trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' diff --git a/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl b/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl new file mode 100644 index 0000000000..139397bf41 --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl @@ -0,0 +1,11 @@ +bundle: + name: experiment-basic-$UNIQUE_NAME + +resources: + experiments: + my_experiment: + artifact_location: $EXPERIMENT_ARTIFACT_LOCATION + name: $EXPERIMENT_NAME + tags: + - key: "k1" + value: "v1" diff --git a/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl new file mode 100644 index 0000000000..139397bf41 --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl @@ -0,0 +1,11 @@ +bundle: + name: experiment-basic-$UNIQUE_NAME + +resources: + experiments: + my_experiment: + artifact_location: $EXPERIMENT_ARTIFACT_LOCATION + name: $EXPERIMENT_NAME + tags: + - key: "k1" + value: "v1" diff --git a/acceptance/bundle/deploy/experiments/basic/test.toml b/acceptance/bundle/deploy/experiments/basic/test.toml index 2fff4443ab..506fe817eb 100644 --- a/acceptance/bundle/deploy/experiments/basic/test.toml +++ b/acceptance/bundle/deploy/experiments/basic/test.toml @@ -1,5 +1,5 @@ -Cloud = false -Local = false +Cloud = true +Local = true # Test both terraform and direct deployment engines [EnvMatrix] diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 513ecea1f1..7954514f05 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -196,6 +196,27 @@ resources.database_instances.*.state database.DatabaseInstanceState ALL resources.database_instances.*.stopped bool ALL resources.database_instances.*.uid string ALL resources.database_instances.*.url string INPUT +resources.experiments.*.artifact_location string ALL +resources.experiments.*.creation_time int64 INPUT REMOTE +resources.experiments.*.experiment_id string INPUT REMOTE +resources.experiments.*.id string INPUT +resources.experiments.*.last_update_time int64 INPUT REMOTE +resources.experiments.*.lifecycle resources.Lifecycle INPUT +resources.experiments.*.lifecycle.prevent_destroy bool INPUT +resources.experiments.*.lifecycle_stage string INPUT REMOTE +resources.experiments.*.modified_status string INPUT +resources.experiments.*.name string ALL +resources.experiments.*.permissions []resources.MlflowExperimentPermission INPUT +resources.experiments.*.permissions[*] resources.MlflowExperimentPermission INPUT +resources.experiments.*.permissions[*].group_name string INPUT +resources.experiments.*.permissions[*].level resources.MlflowExperimentPermissionLevel INPUT +resources.experiments.*.permissions[*].service_principal_name string INPUT +resources.experiments.*.permissions[*].user_name string INPUT +resources.experiments.*.tags []ml.ExperimentTag ALL +resources.experiments.*.tags[*] ml.ExperimentTag ALL +resources.experiments.*.tags[*].key string ALL +resources.experiments.*.tags[*].value string ALL +resources.experiments.*.url string INPUT resources.jobs.*.budget_policy_id string INPUT STATE resources.jobs.*.continuous *jobs.Continuous INPUT STATE resources.jobs.*.continuous.pause_status jobs.PauseStatus INPUT STATE diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index 9ffd43329a..c161460806 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -16,6 +16,7 @@ import ( "github.com/databricks/databricks-sdk-go/service/apps" "github.com/databricks/databricks-sdk-go/service/catalog" "github.com/databricks/databricks-sdk-go/service/database" + "github.com/databricks/databricks-sdk-go/service/ml" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -60,6 +61,18 @@ var testConfig map[string]any = map[string]any{ Name: "main.myschema.my_synced_table", }, }, + "experiments": &resources.MlflowExperiment{ + Experiment: ml.Experiment{ + Name: "my-experiment", + Tags: []ml.ExperimentTag{ + { + Key: "my-tag", + Value: "my-value", + }, + }, + ArtifactLocation: "s3://my-bucket/my-experiment", + }, + }, } type prepareWorkspace func(client *databricks.WorkspaceClient) error diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index 9147023667..32d8ef9784 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -28,23 +28,23 @@ func (*ResourceExperiment) PrepareState(input *resources.MlflowExperiment) *ml.C } } -func (*ResourceExperiment) RemapState(response *ml.GetExperimentResponse) *ml.CreateExperiment { - experiment := response.Experiment +func (*ResourceExperiment) RemapState(experiment *ml.Experiment) *ml.CreateExperiment { return &ml.CreateExperiment{ Name: experiment.Name, ArtifactLocation: experiment.ArtifactLocation, Tags: experiment.Tags, + ForceSendFields: filterFields[ml.CreateExperiment](experiment.ForceSendFields), } } -func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.GetExperimentResponse, error) { +func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.Experiment, error) { result, err := r.client.Experiments.GetExperiment(ctx, ml.GetExperimentRequest{ ExperimentId: id, }) if err != nil { return nil, fmt.Errorf("failed to get experiment %s: %w", id, err) } - return result, nil + return result.Experiment, nil } func (r *ResourceExperiment) DoCreate(ctx context.Context, state *ml.CreateExperiment) (string, error) { @@ -80,8 +80,12 @@ func (r *ResourceExperiment) DoDelete(ctx context.Context, id string) error { func (*ResourceExperiment) FieldTriggers() map[string]deployplan.ActionType { return map[string]deployplan.ActionType{ - "name": deployplan.ActionTypeUpdate, + "name": deployplan.ActionTypeUpdate, + + // artifact_location is marked as suppress_diff in TF. This mirrors that behaviour. "artifact_location": deployplan.ActionTypeRecreate, - "tags": deployplan.ActionTypeUpdate, + + // Tags updates are not supported by TF. This mirrors that behaviour. + "tags": deployplan.ActionTypeSkip, } } diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go new file mode 100644 index 0000000000..87378df161 --- /dev/null +++ b/libs/testserver/experiments.go @@ -0,0 +1,145 @@ +package testserver + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/databricks/databricks-sdk-go/service/ml" +) + +func (s *FakeWorkspace) ExperimentCreate(req Request) Response { + defer s.LockUnlock()() + + var experiment ml.CreateExperiment + if err := json.Unmarshal(req.Body, &experiment); err != nil { + return Response{ + Body: fmt.Sprintf("request parsing error: %s", err), + StatusCode: http.StatusBadRequest, + } + } + + // TODO(verify):Server appends these tags automatically to experiments. + // replicate that behaviour in the test server as well. + appendTags := []ml.ExperimentTag{ + { + Key: "mlflow.ownerId", + Value: TestUser.Id, + }, + { + Key: "mlflow.experiment.sourceName", + Value: experiment.Name, + }, + { + Key: "mlflow.ownerId", + Value: TestUser.Id, + }, + { + Key: "mlflow.ownerEmail", + Value: TestUser.UserName, + }, + { + Key: "mlflow.experimentType", + Value: "MLFLOW_EXPERIMENT", + }, + } + + // Generate a new experiment ID + experimentId := fmt.Sprintf("%d", len(s.Experiments)+1) + + // Create the experiment + exp := ml.Experiment{ + ExperimentId: experimentId, + Name: experiment.Name, + ArtifactLocation: experiment.ArtifactLocation, + Tags: append(experiment.Tags, appendTags...), + LifecycleStage: "active", + } + + s.Experiments[experimentId] = exp + + return Response{ + Body: ml.CreateExperimentResponse{ + ExperimentId: experimentId, + }, + } +} + +func (s *FakeWorkspace) ExperimentGet(req Request) Response { + defer s.LockUnlock()() + + experimentId := req.URL.Query().Get("experiment_id") + if experimentId == "" { + return Response{ + StatusCode: http.StatusBadRequest, + Body: map[string]string{"message": "experiment_id is required"}, + } + } + + experiment, exists := s.Experiments[experimentId] + if !exists { + return Response{ + StatusCode: http.StatusNotFound, + Body: map[string]string{"message": fmt.Sprintf("Experiment %s not found", experimentId)}, + } + } + + return Response{ + Body: ml.GetExperimentResponse{ + Experiment: &experiment, + }, + } +} + +func (s *FakeWorkspace) ExperimentUpdate(req Request) Response { + defer s.LockUnlock()() + + var updateReq ml.UpdateExperiment + if err := json.Unmarshal(req.Body, &updateReq); err != nil { + return Response{ + Body: fmt.Sprintf("request parsing error: %s", err), + StatusCode: http.StatusBadRequest, + } + } + + experiment, exists := s.Experiments[updateReq.ExperimentId] + if !exists { + return Response{ + StatusCode: http.StatusNotFound, + Body: map[string]string{"message": fmt.Sprintf("Experiment %s not found", updateReq.ExperimentId)}, + } + } + + // Update the experiment + if updateReq.NewName != "" { + experiment.Name = updateReq.NewName + } + + s.Experiments[updateReq.ExperimentId] = experiment + + return Response{} +} + +func (s *FakeWorkspace) ExperimentDelete(req Request) Response { + defer s.LockUnlock()() + + var deleteReq ml.DeleteExperiment + if err := json.Unmarshal(req.Body, &deleteReq); err != nil { + return Response{ + Body: fmt.Sprintf("request parsing error: %s", err), + StatusCode: http.StatusBadRequest, + } + } + + _, exists := s.Experiments[deleteReq.ExperimentId] + if !exists { + return Response{ + StatusCode: http.StatusNotFound, + Body: map[string]string{"message": fmt.Sprintf("Experiment %s not found", deleteReq.ExperimentId)}, + } + } + + delete(s.Experiments, deleteReq.ExperimentId) + + return Response{} +} diff --git a/libs/testserver/fake_workspace.go b/libs/testserver/fake_workspace.go index 9918cc1888..5f662341fb 100644 --- a/libs/testserver/fake_workspace.go +++ b/libs/testserver/fake_workspace.go @@ -16,6 +16,7 @@ import ( "github.com/databricks/databricks-sdk-go/service/dashboards" "github.com/databricks/databricks-sdk-go/service/iam" "github.com/databricks/databricks-sdk-go/service/jobs" + "github.com/databricks/databricks-sdk-go/service/ml" "github.com/databricks/databricks-sdk-go/service/pipelines" "github.com/databricks/databricks-sdk-go/service/sql" "github.com/databricks/databricks-sdk-go/service/workspace" @@ -75,6 +76,7 @@ type FakeWorkspace struct { Dashboards map[string]dashboards.Dashboard SqlWarehouses map[string]sql.GetWarehouseResponse Alerts map[string]sql.AlertV2 + Experiments map[string]ml.Experiment Acls map[string][]workspace.AclItem @@ -172,6 +174,7 @@ func NewFakeWorkspace(url, token string) *FakeWorkspace { DatabaseCatalogs: map[string]database.DatabaseCatalog{}, SyncedDatabaseTables: map[string]database.SyncedDatabaseTable{}, Alerts: map[string]sql.AlertV2{}, + Experiments: map[string]ml.Experiment{}, } } diff --git a/libs/testserver/handlers.go b/libs/testserver/handlers.go index a0ab2070eb..218a498363 100644 --- a/libs/testserver/handlers.go +++ b/libs/testserver/handlers.go @@ -521,4 +521,22 @@ func AddDefaultHandlers(server *Server) { server.Handle("GET", "/api/2.0/permissions/jobs/{job_id}", func(req Request) any { return req.Workspace.JobsGetPermissions(req, req.Vars["job_id"]) }) + + // MLflow Experiments: + + server.Handle("GET", "/api/2.0/mlflow/experiments/get", func(req Request) any { + return req.Workspace.ExperimentGet(req) + }) + + server.Handle("POST", "/api/2.0/mlflow/experiments/create", func(req Request) any { + return req.Workspace.ExperimentCreate(req) + }) + + server.Handle("POST", "/api/2.0/mlflow/experiments/update", func(req Request) any { + return req.Workspace.ExperimentUpdate(req) + }) + + server.Handle("POST", "/api/2.0/mlflow/experiments/delete", func(req Request) any { + return req.Workspace.ExperimentDelete(req) + }) } From c6418065b4aff13f2a0354da0b1c3a197c631969 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:11:49 +0200 Subject: [PATCH 03/22] - --- acceptance/bundle/deploy/experiments/basic/output.txt | 8 ++++---- acceptance/bundle/deploy/experiments/basic/test.toml | 4 ++++ libs/testserver/experiments.go | 3 +-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index a1595b2769..0a3af33a93 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -11,7 +11,7 @@ Deploying resources... Updating deployment state... Deployment complete! ->>> [CLI] experiments get-experiment 1 +>>> [CLI] experiments get-experiment [NUMID] { "name": "/Users/[USERNAME]/original-[UNIQUE_NAME]", "artifact_location": "s3://original-[UNIQUE_NAME]", @@ -55,7 +55,7 @@ Deploying resources... Updating deployment state... Deployment complete! ->>> [CLI] experiments get-experiment 1 +>>> [CLI] experiments get-experiment [NUMID] { "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", "artifact_location": "s3://original-[UNIQUE_NAME]", @@ -99,7 +99,7 @@ Deploying resources... Updating deployment state... Deployment complete! ->>> [CLI] experiments get-experiment 1 +>>> [CLI] experiments get-experiment [NUMID] { "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", "artifact_location": "s3://new-[UNIQUE_NAME]", @@ -140,7 +140,7 @@ Deploying resources... Updating deployment state... Deployment complete! ->>> [CLI] experiments get-experiment 1 +>>> [CLI] experiments get-experiment [NUMID] { "name": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]", "artifact_location": "s3://new-[UNIQUE_NAME]", diff --git a/acceptance/bundle/deploy/experiments/basic/test.toml b/acceptance/bundle/deploy/experiments/basic/test.toml index 506fe817eb..c6c1abb2e6 100644 --- a/acceptance/bundle/deploy/experiments/basic/test.toml +++ b/acceptance/bundle/deploy/experiments/basic/test.toml @@ -1,6 +1,10 @@ Cloud = true Local = true +[[Repls]] +Old = '\d{3,}' +New = "[NUMID]" + # Test both terraform and direct deployment engines [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go index 87378df161..76b21f9f6a 100644 --- a/libs/testserver/experiments.go +++ b/libs/testserver/experiments.go @@ -44,8 +44,7 @@ func (s *FakeWorkspace) ExperimentCreate(req Request) Response { }, } - // Generate a new experiment ID - experimentId := fmt.Sprintf("%d", len(s.Experiments)+1) + experimentId := fmt.Sprintf("%d", len(s.Experiments)+1000) // Create the experiment exp := ml.Experiment{ From 9978371edaabf8e484eea50118ea90de4fc33766 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:18:43 +0200 Subject: [PATCH 04/22] add local test that works --- acceptance/bundle/deploy/experiments/basic/output.txt | 2 +- libs/testserver/experiments.go | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index 0a3af33a93..5ab6b1d21b 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -70,7 +70,7 @@ Deployment complete! }, { "key": "mlflow.experiment.sourceName", - "value": "/Users/[USERNAME]/original-[UNIQUE_NAME]" + "value": "/Users/[USERNAME]/new-name-[UNIQUE_NAME]" }, { "key": "mlflow.ownerId", diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go index 76b21f9f6a..b3a671517c 100644 --- a/libs/testserver/experiments.go +++ b/libs/testserver/experiments.go @@ -112,6 +112,14 @@ func (s *FakeWorkspace) ExperimentUpdate(req Request) Response { // Update the experiment if updateReq.NewName != "" { experiment.Name = updateReq.NewName + + // The server modifies the value of the tag as well. Mimic that behaviour + // in the test server as well. + for i := range experiment.Tags { + if experiment.Tags[i].Key == "mlflow.experiment.sourceName" { + experiment.Tags[i].Value = updateReq.NewName + } + } } s.Experiments[updateReq.ExperimentId] = experiment From 6950f618ee43a524f12e558da5103fc0ea5f6276 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:22:43 +0200 Subject: [PATCH 05/22] - --- acceptance/bundle/deploy/experiments/basic/output.txt | 2 +- acceptance/bundle/deploy/experiments/basic/script | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index 5ab6b1d21b..aea7c4da34 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -131,7 +131,7 @@ Deployment complete! ] } -=== adding a new tag should be a no-op for direct deployment. It is not for Terraform because of a persistent drift bug +=== adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated. >>> [CLI] bundle plan >>> [CLI] bundle deploy diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index ca285fd28b..e7bc28f890 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -29,7 +29,7 @@ trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' -title "adding a new tag should be a no-op for direct deployment. It is not for Terraform because of a persistent drift bug" +title "adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated." envsubst < templates/two_tag.tmpl > databricks.yml trace $CLI bundle plan > out.additional_tag.$DATABRICKS_BUNDLE_ENGINE.txt trace $CLI bundle deploy From 402f8201960aef72ba1bc48c96e0c7165f9069cc Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:24:15 +0200 Subject: [PATCH 06/22] - --- acceptance/bundle/deploy/experiments/basic/output.txt | 8 ++++++++ acceptance/bundle/deploy/experiments/basic/script | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index aea7c4da34..c69ff9cca2 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -1,5 +1,9 @@ === create the experiment +>>> export EXPERIMENT_ARTIFACT_LOCATION=s3://original-[UNIQUE_NAME] + +>>> export EXPERIMENT_NAME=/Users/[USERNAME]/original-[UNIQUE_NAME] + >>> [CLI] bundle plan create experiments.my_experiment @@ -44,6 +48,8 @@ Deployment complete! } === update the name +>>> export EXPERIMENT_NAME=/Users/[USERNAME]/new-name-[UNIQUE_NAME] + >>> [CLI] bundle plan update experiments.my_experiment @@ -88,6 +94,8 @@ Deployment complete! } === updating the artifact location should cause a recreation +>>> export EXPERIMENT_ARTIFACT_LOCATION=s3://new-[UNIQUE_NAME] + >>> [CLI] bundle plan recreate experiments.my_experiment diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index e7bc28f890..825a6996e1 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -5,8 +5,8 @@ cleanup() { trap cleanup EXIT title "create the experiment" -export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" -export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" +trace export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" +trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy @@ -14,7 +14,7 @@ experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiment trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' title "update the name" -export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/new-name-${UNIQUE_NAME}" +trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/new-name-${UNIQUE_NAME}" envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy @@ -22,7 +22,7 @@ experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiment trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' title "updating the artifact location should cause a recreation" -export EXPERIMENT_ARTIFACT_LOCATION="s3://new-${UNIQUE_NAME}" +trace export EXPERIMENT_ARTIFACT_LOCATION="s3://new-${UNIQUE_NAME}" envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy From d1599e54bbc2fd2a8a6caa261aa22a1cdc0b10c3 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:25:42 +0200 Subject: [PATCH 07/22] - --- acceptance/bundle/deploy/experiments/basic/output.txt | 8 ++++++++ acceptance/bundle/deploy/experiments/basic/script | 8 ++++---- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index c69ff9cca2..c06a6c786e 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -4,6 +4,8 @@ >>> export EXPERIMENT_NAME=/Users/[USERNAME]/original-[UNIQUE_NAME] +>>> envsubst + >>> [CLI] bundle plan create experiments.my_experiment @@ -50,6 +52,8 @@ Deployment complete! === update the name >>> export EXPERIMENT_NAME=/Users/[USERNAME]/new-name-[UNIQUE_NAME] +>>> envsubst + >>> [CLI] bundle plan update experiments.my_experiment @@ -96,6 +100,8 @@ Deployment complete! === updating the artifact location should cause a recreation >>> export EXPERIMENT_ARTIFACT_LOCATION=s3://new-[UNIQUE_NAME] +>>> envsubst + >>> [CLI] bundle plan recreate experiments.my_experiment @@ -140,6 +146,8 @@ Deployment complete! } === adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated. +>>> envsubst + >>> [CLI] bundle plan >>> [CLI] bundle deploy diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index 825a6996e1..f98d2a14ce 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -7,7 +7,7 @@ trap cleanup EXIT title "create the experiment" trace export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" -envsubst < templates/one_tag.tmpl > databricks.yml +trace envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') @@ -15,7 +15,7 @@ trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, title "update the name" trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/new-name-${UNIQUE_NAME}" -envsubst < templates/one_tag.tmpl > databricks.yml +trace envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') @@ -23,14 +23,14 @@ trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, title "updating the artifact location should cause a recreation" trace export EXPERIMENT_ARTIFACT_LOCATION="s3://new-${UNIQUE_NAME}" -envsubst < templates/one_tag.tmpl > databricks.yml +trace envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' title "adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated." -envsubst < templates/two_tag.tmpl > databricks.yml +trace envsubst < templates/two_tag.tmpl > databricks.yml trace $CLI bundle plan > out.additional_tag.$DATABRICKS_BUNDLE_ENGINE.txt trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') From cf0621d32772970268a7c808acc6fc45d070d8cd Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:27:40 +0200 Subject: [PATCH 08/22] fix two tag yml --- .../experiments/basic/out.additional_tag.direct-exp.txt | 4 +++- .../bundle/deploy/experiments/basic/templates/two_tag.tmpl | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt index c54c9d511c..986d3cf68c 100644 --- a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt +++ b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt @@ -1 +1,3 @@ -Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged +update experiments.my_experiment + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl index 139397bf41..83089014f6 100644 --- a/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl +++ b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl @@ -9,3 +9,5 @@ resources: tags: - key: "k1" value: "v1" + - key: "k2" + value: "v2" From efaac5e610a334f960d0d00df7257ad831a6a2d3 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:32:32 +0200 Subject: [PATCH 09/22] fix doc string --- bundle/direct/dresources/experiment.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index 32d8ef9784..c0b6c7454d 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -80,9 +80,7 @@ func (r *ResourceExperiment) DoDelete(ctx context.Context, id string) error { func (*ResourceExperiment) FieldTriggers() map[string]deployplan.ActionType { return map[string]deployplan.ActionType{ - "name": deployplan.ActionTypeUpdate, - - // artifact_location is marked as suppress_diff in TF. This mirrors that behaviour. + "name": deployplan.ActionTypeUpdate, "artifact_location": deployplan.ActionTypeRecreate, // Tags updates are not supported by TF. This mirrors that behaviour. From d152619f2e77cd6d0814368d203a5ea38d790895 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:40:52 +0200 Subject: [PATCH 10/22] comment --- libs/testserver/experiments.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go index b3a671517c..b691827a0b 100644 --- a/libs/testserver/experiments.go +++ b/libs/testserver/experiments.go @@ -19,8 +19,8 @@ func (s *FakeWorkspace) ExperimentCreate(req Request) Response { } } - // TODO(verify):Server appends these tags automatically to experiments. - // replicate that behaviour in the test server as well. + // Server appends these tags automatically to experiments. + // We replicate that behaviour in the test server as well. appendTags := []ml.ExperimentTag{ { Key: "mlflow.ownerId", From 314eaea6b52404e4e4548ea7fc30173f1a5e2a4c Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 14:50:10 +0200 Subject: [PATCH 11/22] fix test --- .../experiments/basic/out.additional_tag.direct-exp.txt | 3 --- .../experiments/basic/out.additional_tag.terraform.txt | 3 --- acceptance/bundle/deploy/experiments/basic/output.txt | 5 ++++- acceptance/bundle/deploy/experiments/basic/script | 4 ++-- 4 files changed, 6 insertions(+), 9 deletions(-) delete mode 100644 acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt delete mode 100644 acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt diff --git a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt deleted file mode 100644 index 986d3cf68c..0000000000 --- a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.direct-exp.txt +++ /dev/null @@ -1,3 +0,0 @@ -update experiments.my_experiment - -Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt b/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt deleted file mode 100644 index 986d3cf68c..0000000000 --- a/acceptance/bundle/deploy/experiments/basic/out.additional_tag.terraform.txt +++ /dev/null @@ -1,3 +0,0 @@ -update experiments.my_experiment - -Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index c06a6c786e..d96938c881 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -145,10 +145,13 @@ Deployment complete! ] } -=== adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated. +=== adding a new tag should be a no-op >>> envsubst >>> [CLI] bundle plan +update experiments.my_experiment + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index f98d2a14ce..9a3fd8626d 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -29,9 +29,9 @@ trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' -title "adding a new tag should be a no-op for direct deployment. It is a no-op as well for terraform, but shows up in the plan due to a persistent drift bug. The actual state of the experiment is not updated." +title "adding a new tag should be a no-op" trace envsubst < templates/two_tag.tmpl > databricks.yml -trace $CLI bundle plan > out.additional_tag.$DATABRICKS_BUNDLE_ENGINE.txt +trace $CLI bundle plan trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' From e89752c5725086f533cc1748c7ebc6d00361a899 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 15:00:15 +0200 Subject: [PATCH 12/22] lint --- bundle/direct/dresources/experiment.go | 8 +++++--- libs/testserver/experiments.go | 3 ++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index c0b6c7454d..2c00f42a6d 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -25,6 +25,7 @@ func (*ResourceExperiment) PrepareState(input *resources.MlflowExperiment) *ml.C Name: input.Name, ArtifactLocation: input.ArtifactLocation, Tags: input.Tags, + ForceSendFields: filterFields[ml.CreateExperiment](input.ForceSendFields), } } @@ -55,10 +56,11 @@ func (r *ResourceExperiment) DoCreate(ctx context.Context, state *ml.CreateExper return result.ExperimentId, nil } -func (r *ResourceExperiment) DoUpdate(ctx context.Context, id string, state *ml.CreateExperiment) error { +func (r *ResourceExperiment) DoUpdate(ctx context.Context, id string, config *ml.CreateExperiment) error { updateReq := ml.UpdateExperiment{ - ExperimentId: id, - NewName: state.Name, + ExperimentId: id, + NewName: config.Name, + ForceSendFields: filterFields[ml.UpdateExperiment](config.ForceSendFields), } err := r.client.Experiments.UpdateExperiment(ctx, updateReq) diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go index b691827a0b..305e8d86e2 100644 --- a/libs/testserver/experiments.go +++ b/libs/testserver/experiments.go @@ -4,6 +4,7 @@ import ( "encoding/json" "fmt" "net/http" + "strconv" "github.com/databricks/databricks-sdk-go/service/ml" ) @@ -44,7 +45,7 @@ func (s *FakeWorkspace) ExperimentCreate(req Request) Response { }, } - experimentId := fmt.Sprintf("%d", len(s.Experiments)+1000) + experimentId := strconv.Itoa(len(s.Experiments) + 1000) // Create the experiment exp := ml.Experiment{ From 340efaa7368c2f8fb381ba6dfc575f7807abcce6 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 15:03:27 +0200 Subject: [PATCH 13/22] - --- bundle/direct/dresources/experiment.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index 2c00f42a6d..c51a643b3f 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -48,8 +48,8 @@ func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.Expe return result.Experiment, nil } -func (r *ResourceExperiment) DoCreate(ctx context.Context, state *ml.CreateExperiment) (string, error) { - result, err := r.client.Experiments.CreateExperiment(ctx, *state) +func (r *ResourceExperiment) DoCreate(ctx context.Context, config *ml.CreateExperiment) (string, error) { + result, err := r.client.Experiments.CreateExperiment(ctx, *config) if err != nil { return "", fmt.Errorf("failed to create experiment: %w", err) } From d1ec3a98fd6c668cc41319d7ff45d3f1203f07e7 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 15:23:15 +0200 Subject: [PATCH 14/22] - --- acceptance/bundle/deploy/experiments/basic/test.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/acceptance/bundle/deploy/experiments/basic/test.toml b/acceptance/bundle/deploy/experiments/basic/test.toml index c6c1abb2e6..b0d56138da 100644 --- a/acceptance/bundle/deploy/experiments/basic/test.toml +++ b/acceptance/bundle/deploy/experiments/basic/test.toml @@ -5,6 +5,12 @@ Local = true Old = '\d{3,}' New = "[NUMID]" +[Env] +# MSYS2 automatically converts absolute paths like /Users/$username/$UNIQUE_NAME to +# C:/Program Files/Git/Users/$username/UNIQUE_NAME. +# Setting this environment variable prevents that conversion on windows. +MSYS_NO_PATHCONV = "1" + # Test both terraform and direct deployment engines [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] From 34d0aebc2eaeab0e85b6eadcd5e677d883e9e94e Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 15:35:32 +0200 Subject: [PATCH 15/22] - --- acceptance/bundle/deploy/experiments/basic/script | 4 ++++ acceptance/bundle/deploy/experiments/basic/test.toml | 6 ------ 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index 9a3fd8626d..9957afea63 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -4,6 +4,10 @@ cleanup() { trap cleanup EXIT +# disable conversion of absolute paths like /Users/$username/$UNIQUE_NAME to +# C:/Program Files/Git/Users/$username/UNIQUE_NAME on windows. +export MSYS_NO_PATHCONV=1 + title "create the experiment" trace export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" diff --git a/acceptance/bundle/deploy/experiments/basic/test.toml b/acceptance/bundle/deploy/experiments/basic/test.toml index b0d56138da..c6c1abb2e6 100644 --- a/acceptance/bundle/deploy/experiments/basic/test.toml +++ b/acceptance/bundle/deploy/experiments/basic/test.toml @@ -5,12 +5,6 @@ Local = true Old = '\d{3,}' New = "[NUMID]" -[Env] -# MSYS2 automatically converts absolute paths like /Users/$username/$UNIQUE_NAME to -# C:/Program Files/Git/Users/$username/UNIQUE_NAME. -# Setting this environment variable prevents that conversion on windows. -MSYS_NO_PATHCONV = "1" - # Test both terraform and direct deployment engines [EnvMatrix] DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] From b767f923f6da089fdccc914605746271f220b9e1 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 15:44:41 +0200 Subject: [PATCH 16/22] enable mlops-stacks test --- acceptance/bundle/deploy/mlops-stacks/out.test.toml | 2 +- acceptance/bundle/deploy/mlops-stacks/test.toml | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/acceptance/bundle/deploy/mlops-stacks/out.test.toml b/acceptance/bundle/deploy/mlops-stacks/out.test.toml index 3cdb920b67..c3a1b55592 100644 --- a/acceptance/bundle/deploy/mlops-stacks/out.test.toml +++ b/acceptance/bundle/deploy/mlops-stacks/out.test.toml @@ -2,4 +2,4 @@ Local = false Cloud = true [EnvMatrix] - DATABRICKS_BUNDLE_ENGINE = ["terraform"] + DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct-exp"] diff --git a/acceptance/bundle/deploy/mlops-stacks/test.toml b/acceptance/bundle/deploy/mlops-stacks/test.toml index ae16d8f388..98dbfa6435 100644 --- a/acceptance/bundle/deploy/mlops-stacks/test.toml +++ b/acceptance/bundle/deploy/mlops-stacks/test.toml @@ -3,8 +3,6 @@ Local=false Badness = "the newly initialized bundle from the 'mlops-stacks' template contains two validation warnings in the configuration" -EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform"] # uses 'experiments' resource - Ignore = [ "config.json" ] From 3ecaee2d2c10582bb122c843d308eaf5772f2ede Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 16:19:31 +0200 Subject: [PATCH 17/22] fix test on windows --- acceptance/bundle/deploy/experiments/basic/output.txt | 4 ++-- acceptance/bundle/deploy/experiments/basic/script | 8 ++------ .../deploy/experiments/basic/templates/one_tag.tmpl | 2 +- .../deploy/experiments/basic/templates/two_tag.tmpl | 2 +- 4 files changed, 6 insertions(+), 10 deletions(-) diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index d96938c881..1384eb84a8 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -2,7 +2,7 @@ === create the experiment >>> export EXPERIMENT_ARTIFACT_LOCATION=s3://original-[UNIQUE_NAME] ->>> export EXPERIMENT_NAME=/Users/[USERNAME]/original-[UNIQUE_NAME] +>>> export EXPERIMENT_NAME_PREFIX=original >>> envsubst @@ -50,7 +50,7 @@ Deployment complete! } === update the name ->>> export EXPERIMENT_NAME=/Users/[USERNAME]/new-name-[UNIQUE_NAME] +>>> export EXPERIMENT_NAME_PREFIX=new-name >>> envsubst diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index 9957afea63..1af65ff91f 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -4,13 +4,9 @@ cleanup() { trap cleanup EXIT -# disable conversion of absolute paths like /Users/$username/$UNIQUE_NAME to -# C:/Program Files/Git/Users/$username/UNIQUE_NAME on windows. -export MSYS_NO_PATHCONV=1 - title "create the experiment" trace export EXPERIMENT_ARTIFACT_LOCATION="s3://original-${UNIQUE_NAME}" -trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/original-${UNIQUE_NAME}" +trace export EXPERIMENT_NAME_PREFIX="original" trace envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy @@ -18,7 +14,7 @@ experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiment trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}' title "update the name" -trace export EXPERIMENT_NAME="/Users/${CURRENT_USER_NAME}/new-name-${UNIQUE_NAME}" +trace export EXPERIMENT_NAME_PREFIX="new-name" trace envsubst < templates/one_tag.tmpl > databricks.yml trace $CLI bundle plan trace $CLI bundle deploy diff --git a/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl b/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl index 139397bf41..3766a7c71b 100644 --- a/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl +++ b/acceptance/bundle/deploy/experiments/basic/templates/one_tag.tmpl @@ -5,7 +5,7 @@ resources: experiments: my_experiment: artifact_location: $EXPERIMENT_ARTIFACT_LOCATION - name: $EXPERIMENT_NAME + name: /Users/${CURRENT_USER_NAME}/${EXPERIMENT_NAME_PREFIX}-${UNIQUE_NAME} tags: - key: "k1" value: "v1" diff --git a/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl index 83089014f6..035f58e70e 100644 --- a/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl +++ b/acceptance/bundle/deploy/experiments/basic/templates/two_tag.tmpl @@ -5,7 +5,7 @@ resources: experiments: my_experiment: artifact_location: $EXPERIMENT_ARTIFACT_LOCATION - name: $EXPERIMENT_NAME + name: /Users/${CURRENT_USER_NAME}/${EXPERIMENT_NAME_PREFIX}-${UNIQUE_NAME} tags: - key: "k1" value: "v1" From e0d9a93d0918ba9dad7076ff8c44dd57bfb6c730 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 16:24:57 +0200 Subject: [PATCH 18/22] update ref schema --- acceptance/bundle/refschema/out.fields.txt | 8 ++++---- bundle/direct/dresources/all_test.go | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/acceptance/bundle/refschema/out.fields.txt b/acceptance/bundle/refschema/out.fields.txt index 4e6fea8e71..91ebbdc1ad 100644 --- a/acceptance/bundle/refschema/out.fields.txt +++ b/acceptance/bundle/refschema/out.fields.txt @@ -195,13 +195,13 @@ resources.database_instances.*.stopped bool ALL resources.database_instances.*.uid string ALL resources.database_instances.*.url string INPUT resources.experiments.*.artifact_location string ALL -resources.experiments.*.creation_time int64 INPUT REMOTE -resources.experiments.*.experiment_id string INPUT REMOTE +resources.experiments.*.creation_time int64 REMOTE +resources.experiments.*.experiment_id string REMOTE resources.experiments.*.id string INPUT -resources.experiments.*.last_update_time int64 INPUT REMOTE +resources.experiments.*.last_update_time int64 REMOTE resources.experiments.*.lifecycle resources.Lifecycle INPUT resources.experiments.*.lifecycle.prevent_destroy bool INPUT -resources.experiments.*.lifecycle_stage string INPUT REMOTE +resources.experiments.*.lifecycle_stage string REMOTE resources.experiments.*.modified_status string INPUT resources.experiments.*.name string ALL resources.experiments.*.permissions []resources.MlflowExperimentPermission INPUT diff --git a/bundle/direct/dresources/all_test.go b/bundle/direct/dresources/all_test.go index ca52a2d6f1..18116b5be1 100644 --- a/bundle/direct/dresources/all_test.go +++ b/bundle/direct/dresources/all_test.go @@ -62,7 +62,7 @@ var testConfig map[string]any = map[string]any{ }, }, "experiments": &resources.MlflowExperiment{ - Experiment: ml.Experiment{ + CreateExperiment: ml.CreateExperiment{ Name: "my-experiment", Tags: []ml.ExperimentTag{ { From fea04ea0294e3677ecf26ab1c5f6186ce67be7a3 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 16:59:05 +0200 Subject: [PATCH 19/22] remove error prefixes --- bundle/direct/dresources/experiment.go | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index c51a643b3f..9143944755 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -2,7 +2,6 @@ package dresources import ( "context" - "fmt" "github.com/databricks/cli/bundle/config/resources" "github.com/databricks/cli/bundle/deployplan" @@ -43,7 +42,7 @@ func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.Expe ExperimentId: id, }) if err != nil { - return nil, fmt.Errorf("failed to get experiment %s: %w", id, err) + return nil, err } return result.Experiment, nil } @@ -51,7 +50,7 @@ func (r *ResourceExperiment) DoRefresh(ctx context.Context, id string) (*ml.Expe func (r *ResourceExperiment) DoCreate(ctx context.Context, config *ml.CreateExperiment) (string, error) { result, err := r.client.Experiments.CreateExperiment(ctx, *config) if err != nil { - return "", fmt.Errorf("failed to create experiment: %w", err) + return "", err } return result.ExperimentId, nil } @@ -63,21 +62,13 @@ func (r *ResourceExperiment) DoUpdate(ctx context.Context, id string, config *ml ForceSendFields: filterFields[ml.UpdateExperiment](config.ForceSendFields), } - err := r.client.Experiments.UpdateExperiment(ctx, updateReq) - if err != nil { - return fmt.Errorf("failed to update experiment %s: %w", id, err) - } - return nil + return r.client.Experiments.UpdateExperiment(ctx, updateReq) } func (r *ResourceExperiment) DoDelete(ctx context.Context, id string) error { - err := r.client.Experiments.DeleteExperiment(ctx, ml.DeleteExperiment{ + return r.client.Experiments.DeleteExperiment(ctx, ml.DeleteExperiment{ ExperimentId: id, }) - if err != nil { - return fmt.Errorf("failed to delete experiment %s: %w", id, err) - } - return nil } func (*ResourceExperiment) FieldTriggers() map[string]deployplan.ActionType { From 2a8a63ec6550e562bc7ba7d95ded648bc5b4def0 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Mon, 29 Sep 2025 17:11:38 +0200 Subject: [PATCH 20/22] address comments --- libs/testserver/experiments.go | 52 +++++-------------------------- libs/testserver/fake_workspace.go | 4 +-- libs/testserver/handlers.go | 10 +++++- 3 files changed, 19 insertions(+), 47 deletions(-) diff --git a/libs/testserver/experiments.go b/libs/testserver/experiments.go index 305e8d86e2..9f7a335d9b 100644 --- a/libs/testserver/experiments.go +++ b/libs/testserver/experiments.go @@ -56,7 +56,9 @@ func (s *FakeWorkspace) ExperimentCreate(req Request) Response { LifecycleStage: "active", } - s.Experiments[experimentId] = exp + s.Experiments[experimentId] = ml.GetExperimentResponse{ + Experiment: &exp, + } return Response{ Body: ml.CreateExperimentResponse{ @@ -65,32 +67,6 @@ func (s *FakeWorkspace) ExperimentCreate(req Request) Response { } } -func (s *FakeWorkspace) ExperimentGet(req Request) Response { - defer s.LockUnlock()() - - experimentId := req.URL.Query().Get("experiment_id") - if experimentId == "" { - return Response{ - StatusCode: http.StatusBadRequest, - Body: map[string]string{"message": "experiment_id is required"}, - } - } - - experiment, exists := s.Experiments[experimentId] - if !exists { - return Response{ - StatusCode: http.StatusNotFound, - Body: map[string]string{"message": fmt.Sprintf("Experiment %s not found", experimentId)}, - } - } - - return Response{ - Body: ml.GetExperimentResponse{ - Experiment: &experiment, - }, - } -} - func (s *FakeWorkspace) ExperimentUpdate(req Request) Response { defer s.LockUnlock()() @@ -112,13 +88,13 @@ func (s *FakeWorkspace) ExperimentUpdate(req Request) Response { // Update the experiment if updateReq.NewName != "" { - experiment.Name = updateReq.NewName + experiment.Experiment.Name = updateReq.NewName // The server modifies the value of the tag as well. Mimic that behaviour // in the test server as well. - for i := range experiment.Tags { - if experiment.Tags[i].Key == "mlflow.experiment.sourceName" { - experiment.Tags[i].Value = updateReq.NewName + for i := range experiment.Experiment.Tags { + if experiment.Experiment.Tags[i].Key == "mlflow.experiment.sourceName" { + experiment.Experiment.Tags[i].Value = updateReq.NewName } } } @@ -129,8 +105,6 @@ func (s *FakeWorkspace) ExperimentUpdate(req Request) Response { } func (s *FakeWorkspace) ExperimentDelete(req Request) Response { - defer s.LockUnlock()() - var deleteReq ml.DeleteExperiment if err := json.Unmarshal(req.Body, &deleteReq); err != nil { return Response{ @@ -139,15 +113,5 @@ func (s *FakeWorkspace) ExperimentDelete(req Request) Response { } } - _, exists := s.Experiments[deleteReq.ExperimentId] - if !exists { - return Response{ - StatusCode: http.StatusNotFound, - Body: map[string]string{"message": fmt.Sprintf("Experiment %s not found", deleteReq.ExperimentId)}, - } - } - - delete(s.Experiments, deleteReq.ExperimentId) - - return Response{} + return MapDelete(s, s.Experiments, deleteReq.ExperimentId) } diff --git a/libs/testserver/fake_workspace.go b/libs/testserver/fake_workspace.go index 4d286683f3..ceeb1a5a26 100644 --- a/libs/testserver/fake_workspace.go +++ b/libs/testserver/fake_workspace.go @@ -76,7 +76,7 @@ type FakeWorkspace struct { Dashboards map[string]dashboards.Dashboard SqlWarehouses map[string]sql.GetWarehouseResponse Alerts map[string]sql.AlertV2 - Experiments map[string]ml.Experiment + Experiments map[string]ml.GetExperimentResponse ModelRegistryModels map[string]ml.Model Acls map[string][]workspace.AclItem @@ -175,7 +175,7 @@ func NewFakeWorkspace(url, token string) *FakeWorkspace { DatabaseCatalogs: map[string]database.DatabaseCatalog{}, SyncedDatabaseTables: map[string]database.SyncedDatabaseTable{}, Alerts: map[string]sql.AlertV2{}, - Experiments: map[string]ml.Experiment{}, + Experiments: map[string]ml.GetExperimentResponse{}, ModelRegistryModels: map[string]ml.Model{}, } } diff --git a/libs/testserver/handlers.go b/libs/testserver/handlers.go index 0de66c721c..ef31dde2f2 100644 --- a/libs/testserver/handlers.go +++ b/libs/testserver/handlers.go @@ -524,7 +524,15 @@ func AddDefaultHandlers(server *Server) { // MLflow Experiments: server.Handle("GET", "/api/2.0/mlflow/experiments/get", func(req Request) any { - return req.Workspace.ExperimentGet(req) + experimentId := req.URL.Query().Get("experiment_id") + if experimentId == "" { + return Response{ + StatusCode: http.StatusBadRequest, + Body: map[string]string{"message": "experiment_id is required"}, + } + } + + return MapGet(req.Workspace, req.Workspace.Experiments, experimentId) }) server.Handle("POST", "/api/2.0/mlflow/experiments/create", func(req Request) any { From f3ecb62e402bc9bd8edd51c7b5c0ea780177a683 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 1 Oct 2025 12:45:24 +0200 Subject: [PATCH 21/22] add TF impl link --- bundle/direct/dresources/experiment.go | 1 + 1 file changed, 1 insertion(+) diff --git a/bundle/direct/dresources/experiment.go b/bundle/direct/dresources/experiment.go index 9143944755..92e7baa8e6 100644 --- a/bundle/direct/dresources/experiment.go +++ b/bundle/direct/dresources/experiment.go @@ -72,6 +72,7 @@ func (r *ResourceExperiment) DoDelete(ctx context.Context, id string) error { } func (*ResourceExperiment) FieldTriggers() map[string]deployplan.ActionType { + // TF implementation: https://github.com/databricks/terraform-provider-databricks/blob/6c106e8e7052bb2726148d66309fd460ed444236/mlflow/resource_mlflow_experiment.go#L22 return map[string]deployplan.ActionType{ "name": deployplan.ActionTypeUpdate, "artifact_location": deployplan.ActionTypeRecreate, From fb4e095279a7115e73d28e31f0e26c43b9ae26f4 Mon Sep 17 00:00:00 2001 From: Shreyas Goenka Date: Wed, 1 Oct 2025 13:21:54 +0200 Subject: [PATCH 22/22] update tesT --- .../bundle/deploy/experiments/basic/out.plan.direct-exp.txt | 1 + .../bundle/deploy/experiments/basic/out.plan.terraform.txt | 3 +++ acceptance/bundle/deploy/experiments/basic/output.txt | 3 --- acceptance/bundle/deploy/experiments/basic/script | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 acceptance/bundle/deploy/experiments/basic/out.plan.direct-exp.txt create mode 100644 acceptance/bundle/deploy/experiments/basic/out.plan.terraform.txt diff --git a/acceptance/bundle/deploy/experiments/basic/out.plan.direct-exp.txt b/acceptance/bundle/deploy/experiments/basic/out.plan.direct-exp.txt new file mode 100644 index 0000000000..c54c9d511c --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/out.plan.direct-exp.txt @@ -0,0 +1 @@ +Plan: 0 to add, 0 to change, 0 to delete, 1 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/out.plan.terraform.txt b/acceptance/bundle/deploy/experiments/basic/out.plan.terraform.txt new file mode 100644 index 0000000000..986d3cf68c --- /dev/null +++ b/acceptance/bundle/deploy/experiments/basic/out.plan.terraform.txt @@ -0,0 +1,3 @@ +update experiments.my_experiment + +Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged diff --git a/acceptance/bundle/deploy/experiments/basic/output.txt b/acceptance/bundle/deploy/experiments/basic/output.txt index 1384eb84a8..6a97cdaf36 100644 --- a/acceptance/bundle/deploy/experiments/basic/output.txt +++ b/acceptance/bundle/deploy/experiments/basic/output.txt @@ -149,9 +149,6 @@ Deployment complete! >>> envsubst >>> [CLI] bundle plan -update experiments.my_experiment - -Plan: 0 to add, 1 to change, 0 to delete, 0 unchanged >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/experiment-basic-[UNIQUE_NAME]/default/files... diff --git a/acceptance/bundle/deploy/experiments/basic/script b/acceptance/bundle/deploy/experiments/basic/script index 1af65ff91f..6bb96db86d 100755 --- a/acceptance/bundle/deploy/experiments/basic/script +++ b/acceptance/bundle/deploy/experiments/basic/script @@ -31,7 +31,7 @@ trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, title "adding a new tag should be a no-op" trace envsubst < templates/two_tag.tmpl > databricks.yml -trace $CLI bundle plan +trace $CLI bundle plan > out.plan.$DATABRICKS_BUNDLE_ENGINE.txt trace $CLI bundle deploy experiment_id=$($CLI bundle summary --output json | jq -r '.resources.experiments.my_experiment.id') trace $CLI experiments get-experiment $experiment_id | jq '.experiment | {name, artifact_location, tags}'