Skip to content

Immutable folder support in DABs#5254

Open
andrewnester wants to merge 17 commits into
mainfrom
demo-immutable
Open

Immutable folder support in DABs#5254
andrewnester wants to merge 17 commits into
mainfrom
demo-immutable

Conversation

@andrewnester

@andrewnester andrewnester commented May 15, 2026

Copy link
Copy Markdown
Contributor

Changes

Added support for deploying bundles to immutable folders in the workspace

Enabled by using

bundle:
  deployment: 
    immutable_folder: true

Why

Tests

Added an acceptance tests

@andrewnester andrewnester requested a review from pietern May 15, 2026 09:56
@eng-dev-ecosystem-bot

eng-dev-ecosystem-bot commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Integration test report

Commit: 9a6c898

Run: 27775865252

Env 🪲​BUG 🟨​KNOWN 💚​RECOVERED 🙈​SKIP ✅​pass 🙈​skip Time
🪲​ aws linux 3 7 13 264 1013 7:44
🪲​ aws windows 3 7 13 266 1011 13:29
🪲​ aws-ucws linux 3 1 6 13 360 927 8:04
🪲​ aws-ucws windows 3 1 6 13 362 925 12:33
🪲​ azure linux 3 1 15 267 1011 7:04
🪲​ azure windows 3 1 15 269 1009 11:06
🪲​ azure-ucws linux 3 1 15 365 923 8:52
🪲​ azure-ucws windows 3 1 15 367 921 12:04
🪲​ gcp linux 3 1 15 263 1014 7:30
🪲​ gcp windows 3 1 15 265 1012 11:00
23 interesting tests: 13 SKIP, 7 KNOWN, 3 BUG
Test Name aws linux aws windows aws-ucws linux aws-ucws windows azure linux azure windows azure-ucws linux azure-ucws windows gcp linux gcp windows
🟨​ TestAccept 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K 🟨​K
🪲​ TestAccept/bundle/deploy/immutable 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B
🪲​ TestAccept/bundle/deploy/immutable/DATABRICKS_BUNDLE_ENGINE=direct 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B
🪲​ TestAccept/bundle/deploy/immutable/DATABRICKS_BUNDLE_ENGINE=terraform 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B 🪲​B
🙈​ TestAccept/bundle/invariant/no_drift 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/permissions 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/with_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions 🟨​K 🟨​K 💚​R 💚​R 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=direct 🟨​K 🟨​K 💚​R 💚​R
🟨​ TestAccept/bundle/resources/permissions/jobs/destroy_without_mgmtperms/without_permissions/DATABRICKS_BUNDLE_ENGINE=terraform 🟨​K 🟨​K 💚​R 💚​R
🙈​ TestAccept/bundle/resources/postgres_branches/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/recreate 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/replace_existing 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/update_protected 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_branches/without_branch_id 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_endpoints/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/postgres_projects/update_display_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/synced_database_tables/basic 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_endpoints/drift/recreated_same_name 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/bundle/resources/vector_search_indexes/recreate/embedding_dimension 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
🙈​ TestAccept/ssh/connection 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S 🙈​S
Top 20 slowest tests (at least 2 minutes):
duration env testname
5:16 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
4:57 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
4:32 gcp linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
4:09 gcp windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:47 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:38 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:18 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
3:16 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:05 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
3:01 aws-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:54 azure-ucws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:54 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:53 azure windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:48 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:46 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=terraform
2:39 aws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:32 azure-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:31 azure linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:28 aws windows TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct
2:15 aws-ucws linux TestAccept/bundle/resources/apps/inline_config/DATABRICKS_BUNDLE_ENGINE=direct

@andrewnester andrewnester marked this pull request as ready for review June 1, 2026 11:34
@github-actions

github-actions Bot commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Approval status: pending

/acceptance/bundle/ - needs approval

23 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @shreyas-goenka, @lennartkats-db, @anton-107

/bundle/ - needs approval

19 files changed
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @shreyas-goenka, @lennartkats-db, @anton-107

/cmd/bundle/ - needs approval

Files: cmd/bundle/utils/process.go
Suggested: @denik
Also eligible: @pietern, @janniklasrose, @shreyas-goenka, @lennartkats-db, @anton-107

/libs/sync/ - needs approval

Files: libs/sync/sync.go
Suggested: @simonfaltum
Also eligible: @tanmay-db, @Divyansh-db, @renaudhartert-db, @parthban-db, @hectorcast-db, @tejaskochar-db, @mihaimitrea-db, @chrisst, @rauchy

General files (require maintainer)

Files: libs/testserver/handlers.go
Based on git history:

  • @denik -- recent work in bundle/phases/, bundle/config/, cmd/bundle/utils/

Any maintainer (@anton-107, @denik, @pietern, @shreyas-goenka, @simonfaltum, @renaudhartert-db) can approve all areas.
See OWNERS for ownership rules.

@andrewnester andrewnester requested a review from denik June 2, 2026 10:01

@shreyas-goenka shreyas-goenka left a comment

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.

Minor comments - other than the bit where we use metadata.json

Comment thread bundle/phases/deploy.go Outdated
uploadLibraries(ctx, b, libs)
if b.Config.Bundle.Immutable {
// Upload all source files and built artifacts as a single immutable snapshot.
// The API assigns a content-addressed path, so workspace.snapshot_path (and

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.

should we add a check explicitly disallowing workspace.snapshot_path references? We don't want users to accidentally rely on it and ensure that this is internal only.

@@ -15,6 +15,9 @@ type Bundle struct {

type Workspace struct {
FilePath string `json:"file_path"`
// SnapshotPath is the workspace path of the immutable snapshot uploaded
// during deployment. Only populated for bundles with bundle.immutable = true.
SnapshotPath string `json:"snapshot_path,omitempty"`

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.

Will the UI use the immutable snapshot path? In that case we'll need to add it to DMS as well.


cmdio.LogString(ctx, "Uploading immutable bundle snapshot...")

zipContent, err := BundleZip(ctx, b)

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.

Should we have a test that for a set of files the bundle zip remains identical across operating system (simple unit test should do it). Windows vs linux often have different new line characters.

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.

Yes, they likely will be different but what is the issue in this case?

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.

It's a performace optimization to avoid unnecessary updates if the snapshot did not change. The backend stores the sha on the ZIP. Should be a rare edge case so we can skip it.

Comment thread bundle/deploy/snapshot/client.go
Comment thread bundle/deploy/snapshot/path_test.go Outdated
Comment thread bundle/deploy/metadata/load.go Outdated
}

func (m *load) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
f, err := filer.NewWorkspaceFilesClient(b.WorkspaceClient(ctx), b.Config.Workspace.StatePath)

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.

metadata.json was only meant to be consumed by non DABs resources like jobs or the UI. We have deployment.json and resources.json. Maybe we should store the snapshot path there?

This is a new dep - before the CLI did not read metadata.json - and we are trying to get rid of this with DMS.

Comment thread bundle/config/mutator/translate_paths.go Outdated

// Perform resolution only if the path starts with one of the specified prefixes.
if slices.ContainsFunc(prefixes, path.HasPrefix) {
if slices.Contains(m.excludePaths, path.String()) {

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.

very nitpicky: Make this more robust? This matches substrings - so "abc" would match "abc" and "abcd". Normally in the codebase - paths refer to exact paths, not patterns or substrings.

Comment thread bundle/config/mutator/resourcemutator/process_static_resources.go Outdated
Deployment complete!

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/test-bundle-immutable-[UNIQUE_NAME]/11f80ca6d8923bf75b57e475d4ca9ba4bb1d6d48c58aace8d3f2a1289b51c6e0/src/files/src/main.py"

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 guess this test covers that the SHA remains identical for windows and linux?

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.

Why would it matter though? Practically they might be different, especially wheels built

@pietern pietern left a comment

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 see the existing tests pass against cloud, but recommend including a testserver implementation already. Makes it easier to iterate.

Comment thread bundle/phases/build.go
if b.Config.Bundle.Deployment.ImmutableFolder {
return nil
}

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.

Is it possible we always delay this until the deploy phase? Keeps things simpler.

Comment thread bundle/deploy/snapshot/path.go Outdated
}
return 0
})
return files, nil

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.

The logic that collects the list of files looks very similar to what we do in libs/sync. Any chance we can let that pkg take care of building the list and we refer to it here? This set of files is also configured as b.Files for telemetry. If we chase the path that sets it we might be able to reuse it?

}

return &SnapshotInfo{Path: resp.Snapshot.Path}, nil
}

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.

This doesn't seem related to the other filers or the filer interface.

It seems logical to colocate this client with the code that takes a couple of []file.FileSet and performs all the zipping and uploading.

}

// The real API uses the workspace user UUID (not email) in the snapshot path,
// matching service-principal identities used in cloud acceptance tests.

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.

optional: Should we also use UUIDs here for better fidelity? The benefits are minor if we have cloud coverage. The difference being users have CAN_MANAGE always on their home directory but that's likely not true for /Users/userId?

// Apply implements bundle.Mutator.
func (*workspaceRootPermissions) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
// If the bundle is immutable, we don't need to apply any permissions to the workspace root.
if b.Config.Bundle.Deployment.ImmutableFolder {

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.

is this safe to do? Given that state management and resource CRUD (workspace.state_path and workspace.resource_path ) both need to have ACLs applied for shared deployments to work.

Whether to fail on active runs. If this is set to true a deployment that is running can be interrupted.
"immutable_folder":
"description": |-
Whether to upload bundle files and artifacts as a single immutable snapshot. When true, all files are packaged into a zip and uploaded via the snapshot API, and workspace.file_path and workspace.artifact_path are set to the returned content-addressed path. The validate and plan commands make no mutative API calls when this is enabled.

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.

Snapshots API is internal. We should not refer to this in the docs.

Comment thread bundle/deploy/snapshot/translate_paths.go
func (m *translateResourcePaths) Name() string { return "snapshot.TranslateResourcePaths" }

func (m *translateResourcePaths) Apply(_ context.Context, b *bundle.Bundle) diag.Diagnostics {
localPrefix := b.SyncRootPath + "/"

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.

Maybe strings.TrimSuffix("/") as well to account for trailing /? Or is that not possible?

func (s *loadState) Name() string { return "snapshot.LoadState" }

func (s *saveState) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnostics {
if b.Config.Workspace.SnapshotPath == "" {

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.

Do we not need to store this remotely? The code reads like it only does local storage but we need it in remote as well.

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.

This sounds like something we can add to resources.json


// SaveState writes the snapshot path to the local deployment state directory
// so it can be recovered during destroy without reading metadata.json.
func SaveState() bundle.Mutator {

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.

Consider integrating this with deployment WAL? If we can record the snapshot upload event we can avoid reuploading it on a subsequent deployment since it already exists.

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.

We can't really, can we? The deployment WAL calculated before the execution of deployment as part of plan but we build and upload later in the phase

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 assume during deployments we write to WAL so surely we should be able to do that during / after file upload? Otherwise do how do we capture if the plan was partially applied.

@shreyas-goenka shreyas-goenka Jun 19, 2026

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 could be misunderstanding the WAL - I'm not familiar with it (will look into it) - I just assumed it records actions as we do them.

>>> [CLI] jobs get [NUMID]
"/Workspace/Users/[UUID]/.snapshots/test-bundle-immutable-no-artifacts-[UNIQUE_NAME]/[SNAPSHOT_HASH]/src/files/src/notebook"

>>> [CLI] bundle destroy --auto-approve

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.

can we assert that destroy deletes the snapshot? Even when .databricks is removed?

// Updates (dynamic): resources.* (strings) (resolves variable references to their actual values)
// Resolves variable references in 'resources' using bundle, workspace, and variables prefixes
mutator.ResolveVariableReferencesOnlyResources(),
resourceResolver,

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.

why this diff?

@@ -0,0 +1,16 @@
Local = false

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.

we can run locally as well? As long as we fix the snapshot API impl in test server?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants