diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index db25ff3c12a..cd7926ccacd 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -12,6 +12,7 @@ * Set the default `data_security_mode` to `DATA_SECURITY_MODE_AUTO` in bundle templates ([#5452](https://github.com/databricks/cli/pull/5452)). * Mark vector search index index_subtype as backend_default to prevent drift after deployment ([#5454](https://github.com/databricks/cli/pull/5454)). * `bundle deployment migrate`: handle resources added to or removed from `databricks.yml` since the last Terraform deploy ([#5463](https://github.com/databricks/cli/pull/5463)). +* Fixed `bundle validate` creating the remote file path if it did not exist; validation no longer performs any write operations ([#5528](https://github.com/databricks/cli/pull/5528)). ### Dependency updates diff --git a/acceptance/.gitattributes b/acceptance/.gitattributes index 8d48122750e..3160a60475a 100644 --- a/acceptance/.gitattributes +++ b/acceptance/.gitattributes @@ -4,6 +4,7 @@ # uploading the file's content to a workspace. *.txt text eol=lf *.lvdash.json text eol=lf +*.py text eol=lf # The out.test.toml file is autogenerated based on the merged test.toml view. out.test.toml linguist-generated=true diff --git a/acceptance/bundle/resource_deps/remote_app_url/output.txt b/acceptance/bundle/resource_deps/remote_app_url/output.txt index 81b300ab11f..951bea9629f 100644 --- a/acceptance/bundle/resource_deps/remote_app_url/output.txt +++ b/acceptance/bundle/resource_deps/remote_app_url/output.txt @@ -15,13 +15,6 @@ create pipelines.mypipeline Plan: 2 to add, 0 to change, 0 to delete, 0 unchanged >>> print_requests.py ^//import-file/ -{ - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" - } -} >>> [CLI] bundle deploy Uploading bundle files to /Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files... @@ -45,6 +38,13 @@ Deployment complete! "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} { "method": "POST", "path": "/api/2.0/apps", diff --git a/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/default/out.requests.txt b/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/default/out.requests.txt index 0c815411ae9..aa0412db327 100644 --- a/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/default/out.requests.txt +++ b/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/default/out.requests.txt @@ -23,27 +23,3 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/pat/files" } } -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/pat/files" - } -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/pat/files" - } -} diff --git a/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/from_flag/out.requests.txt b/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/from_flag/out.requests.txt index 6ba1aad2544..6d221ca084f 100644 --- a/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/from_flag/out.requests.txt +++ b/acceptance/bundle/run/inline-script/databricks-cli/target-is-passed/from_flag/out.requests.txt @@ -51,27 +51,3 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/oauth/files" } } -{ - "headers": { - "Authorization": [ - "Bearer oauth-token" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/oauth/files" - } -} -{ - "headers": { - "Authorization": [ - "Bearer oauth-token" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/oauth/files" - } -} diff --git a/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/default/out.requests.txt b/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/default/out.requests.txt index 93542472ac5..e90d023a48b 100644 --- a/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/default/out.requests.txt +++ b/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/default/out.requests.txt @@ -36,27 +36,3 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/pat/files" } } -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/pat/files" - } -} -{ - "headers": { - "Authorization": [ - "Bearer [DATABRICKS_TOKEN]" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/pat/files" - } -} diff --git a/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/from_flag/out.requests.txt b/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/from_flag/out.requests.txt index 6ce820c124f..84276fce06d 100644 --- a/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/from_flag/out.requests.txt +++ b/acceptance/bundle/run/scripts/databricks-cli/target-is-passed/from_flag/out.requests.txt @@ -64,27 +64,3 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/oauth/files" } } -{ - "headers": { - "Authorization": [ - "Bearer oauth-token" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/oauth/files" - } -} -{ - "headers": { - "Authorization": [ - "Bearer oauth-token" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/foobar/oauth/files" - } -} diff --git a/acceptance/bundle/templates/default-python/classic/out.requests.dev.direct.txt b/acceptance/bundle/templates/default-python/classic/out.requests.dev.direct.txt index 60e8a1a0f34..8c3637d3bec 100644 --- a/acceptance/bundle/templates/default-python/classic/out.requests.dev.direct.txt +++ b/acceptance/bundle/templates/default-python/classic/out.requests.dev.direct.txt @@ -44,6 +44,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/classic/out.requests.dev.terraform.txt b/acceptance/bundle/templates/default-python/classic/out.requests.dev.terraform.txt index 173c894ac7f..1714d704159 100644 --- a/acceptance/bundle/templates/default-python/classic/out.requests.dev.terraform.txt +++ b/acceptance/bundle/templates/default-python/classic/out.requests.dev.terraform.txt @@ -44,6 +44,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/classic/out.requests.prod.direct.txt b/acceptance/bundle/templates/default-python/classic/out.requests.prod.direct.txt index 467ff7e75cd..118f384cabc 100644 --- a/acceptance/bundle/templates/default-python/classic/out.requests.prod.direct.txt +++ b/acceptance/bundle/templates/default-python/classic/out.requests.prod.direct.txt @@ -47,6 +47,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/classic/out.requests.prod.terraform.txt b/acceptance/bundle/templates/default-python/classic/out.requests.prod.terraform.txt index 1211c38babb..3bfc5ddc7d6 100644 --- a/acceptance/bundle/templates/default-python/classic/out.requests.prod.terraform.txt +++ b/acceptance/bundle/templates/default-python/classic/out.requests.prod.terraform.txt @@ -47,6 +47,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/serverless/out.requests.dev.direct.txt b/acceptance/bundle/templates/default-python/serverless/out.requests.dev.direct.txt index c5d9ae4c6b2..98e0d302f4a 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.requests.dev.direct.txt +++ b/acceptance/bundle/templates/default-python/serverless/out.requests.dev.direct.txt @@ -46,6 +46,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/serverless/out.requests.dev.terraform.txt b/acceptance/bundle/templates/default-python/serverless/out.requests.dev.terraform.txt index 4dfb549b054..eade8f9387e 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.requests.dev.terraform.txt +++ b/acceptance/bundle/templates/default-python/serverless/out.requests.dev.terraform.txt @@ -46,6 +46,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/dev/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/serverless/out.requests.prod.direct.txt b/acceptance/bundle/templates/default-python/serverless/out.requests.prod.direct.txt index d7a761ae4cc..c43bb77d488 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.requests.prod.direct.txt +++ b/acceptance/bundle/templates/default-python/serverless/out.requests.prod.direct.txt @@ -49,6 +49,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/templates/default-python/serverless/out.requests.prod.terraform.txt b/acceptance/bundle/templates/default-python/serverless/out.requests.prod.terraform.txt index ae401a3c97a..ff9f68a517f 100644 --- a/acceptance/bundle/templates/default-python/serverless/out.requests.prod.terraform.txt +++ b/acceptance/bundle/templates/default-python/serverless/out.requests.prod.terraform.txt @@ -49,6 +49,13 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/artifacts/.internal" } } +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/my_default_python/prod/files" + } +} { "method": "POST", "path": "/api/2.0/workspace/mkdirs", diff --git a/acceptance/bundle/user_agent/output.txt b/acceptance/bundle/user_agent/output.txt index b517b83889e..c8684c2e448 100644 --- a/acceptance/bundle/user_agent/output.txt +++ b/acceptance/bundle/user_agent/output.txt @@ -8,6 +8,7 @@ OK deploy.direct /api/2.0/workspace/get-status engine/direct OK deploy.direct /api/2.0/workspace/get-status engine/direct OK deploy.direct /api/2.0/workspace/get-status engine/direct OK deploy.direct /api/2.0/workspace/get-status engine/direct +OK deploy.direct /api/2.0/workspace/get-status engine/direct OK deploy.direct /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/empty.py engine/direct OK deploy.direct /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deploy.lock engine/direct OK deploy.direct /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deployment.json engine/direct @@ -16,6 +17,7 @@ OK deploy.direct /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAM OK deploy.direct /api/2.0/workspace/delete engine/direct OK deploy.direct /api/2.0/workspace/delete engine/direct OK deploy.direct /api/2.0/workspace/mkdirs engine/direct +OK deploy.direct /api/2.0/workspace/mkdirs engine/direct OK deploy.direct /api/2.1/unity-catalog/schemas engine/direct MISS deploy.direct /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS deploy.terraform /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_deploy cmd-exec-id/[UUID] interactive/none auth/pat' @@ -28,6 +30,7 @@ OK deploy.terraform /api/2.0/workspace/get-status engine/terraform OK deploy.terraform /api/2.0/workspace/get-status engine/terraform OK deploy.terraform /api/2.0/workspace/get-status engine/terraform OK deploy.terraform /api/2.0/workspace/get-status engine/terraform +OK deploy.terraform /api/2.0/workspace/get-status engine/terraform OK deploy.terraform /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files/empty.py engine/terraform OK deploy.terraform /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deploy.lock engine/terraform OK deploy.terraform /api/2.0/workspace-files/import-file/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/state/deployment.json engine/terraform @@ -36,6 +39,7 @@ OK deploy.terraform /api/2.0/workspace-files/import-file/Workspace/Users/[USER OK deploy.terraform /api/2.0/workspace/delete engine/terraform OK deploy.terraform /api/2.0/workspace/delete engine/terraform OK deploy.terraform /api/2.0/workspace/mkdirs engine/terraform +OK deploy.terraform /api/2.0/workspace/mkdirs engine/terraform MISS deploy.terraform /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS deploy.terraform /api/2.0/preview/scim/v2/Me 'databricks-tf-provider/1.117.0 databricks-sdk-go/[SDK_VERSION] go/1.25.8 os/[OS] cli/[DEV_VERSION] terraform/1.5.5 auth/pat' MISS deploy.terraform /api/2.0/preview/scim/v2/Me 'databricks-tf-provider/1.117.0 databricks-sdk-go/[SDK_VERSION] go/1.25.8 os/[OS] cli/[DEV_VERSION] terraform/1.5.5 auth/pat' @@ -132,11 +136,7 @@ MISS summary.terraform /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databric MISS summary.terraform /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS validate.direct /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' MISS validate.direct /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' -MISS validate.direct /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' -MISS validate.direct /api/2.0/workspace/mkdirs 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' MISS validate.direct /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' MISS validate.terraform /api/2.0/preview/scim/v2/Me 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' MISS validate.terraform /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' -MISS validate.terraform /api/2.0/workspace/get-status 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' -MISS validate.terraform /api/2.0/workspace/mkdirs 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat' MISS validate.terraform /.well-known/databricks-config 'cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS]' diff --git a/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json b/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json index cc39aad6e9b..a046d70fa98 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json +++ b/acceptance/bundle/user_agent/simple/out.requests.deploy.direct.json @@ -71,6 +71,18 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" } } +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_deploy cmd-exec-id/[UUID] interactive/none engine/direct auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/workspace/get-status", + "q": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} { "headers": { "User-Agent": [ @@ -274,6 +286,18 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" } } +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_deploy cmd-exec-id/[UUID] interactive/none engine/direct auth/pat" + ] + }, + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} { "headers": { "User-Agent": [ diff --git a/acceptance/bundle/user_agent/simple/out.requests.deploy.terraform.json b/acceptance/bundle/user_agent/simple/out.requests.deploy.terraform.json index 152e0040eaf..73fbe0acd22 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.deploy.terraform.json +++ b/acceptance/bundle/user_agent/simple/out.requests.deploy.terraform.json @@ -71,6 +71,18 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" } } +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_deploy cmd-exec-id/[UUID] interactive/none engine/terraform auth/pat" + ] + }, + "method": "GET", + "path": "/api/2.0/workspace/get-status", + "q": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} { "headers": { "User-Agent": [ @@ -300,6 +312,18 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/artifacts/.internal" } } +{ + "headers": { + "User-Agent": [ + "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_deploy cmd-exec-id/[UUID] interactive/none engine/terraform auth/pat" + ] + }, + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" + } +} { "headers": { "User-Agent": [ diff --git a/acceptance/bundle/user_agent/simple/out.requests.validate.direct.json b/acceptance/bundle/user_agent/simple/out.requests.validate.direct.json index 8c64d6f74a5..f85bf0ae0b1 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.validate.direct.json +++ b/acceptance/bundle/user_agent/simple/out.requests.validate.direct.json @@ -19,30 +19,6 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" } } -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" - } -} -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" - } -} { "headers": { "User-Agent": [ diff --git a/acceptance/bundle/user_agent/simple/out.requests.validate.terraform.json b/acceptance/bundle/user_agent/simple/out.requests.validate.terraform.json index 8c64d6f74a5..f85bf0ae0b1 100644 --- a/acceptance/bundle/user_agent/simple/out.requests.validate.terraform.json +++ b/acceptance/bundle/user_agent/simple/out.requests.validate.terraform.json @@ -19,30 +19,6 @@ "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" } } -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "GET", - "path": "/api/2.0/workspace/get-status", - "q": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" - } -} -{ - "headers": { - "User-Agent": [ - "cli/[DEV_VERSION] databricks-sdk-go/[SDK_VERSION] go/[GO_VERSION] os/[OS] cmd/bundle_validate cmd-exec-id/[UUID] interactive/none auth/pat" - ] - }, - "method": "POST", - "path": "/api/2.0/workspace/mkdirs", - "body": { - "path": "/Workspace/Users/[USERNAME]/.bundle/test-bundle/default/files" - } -} { "headers": { "User-Agent": [ diff --git a/acceptance/bundle/validate/no_writes/databricks.yml b/acceptance/bundle/validate/no_writes/databricks.yml new file mode 100644 index 00000000000..576d7a9ef25 --- /dev/null +++ b/acceptance/bundle/validate/no_writes/databricks.yml @@ -0,0 +1,2 @@ +bundle: + name: test-bundle diff --git a/acceptance/bundle/validate/no_writes/out.test.toml b/acceptance/bundle/validate/no_writes/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/bundle/validate/no_writes/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/bundle/validate/no_writes/output.txt b/acceptance/bundle/validate/no_writes/output.txt new file mode 100644 index 00000000000..a3d06b47348 --- /dev/null +++ b/acceptance/bundle/validate/no_writes/output.txt @@ -0,0 +1,11 @@ + +>>> [CLI] bundle validate +Name: test-bundle +Target: default +Workspace: + User: [USERNAME] + Path: /Workspace/Users/[USERNAME]/.bundle/test-bundle/default + +Validation OK! + +>>> print_requests.py --sort //api/ diff --git a/acceptance/bundle/validate/no_writes/script b/acceptance/bundle/validate/no_writes/script new file mode 100644 index 00000000000..35f0e48b61a --- /dev/null +++ b/acceptance/bundle/validate/no_writes/script @@ -0,0 +1,5 @@ +# Validate must not mutate the workspace: no write requests, e.g. mkdirs for +# a missing remote file path. print_requests.py prints all non-GET requests. +trace $CLI bundle validate + +trace print_requests.py --sort //api/ diff --git a/acceptance/bundle/validate/no_writes/test.toml b/acceptance/bundle/validate/no_writes/test.toml new file mode 100644 index 00000000000..159efe02696 --- /dev/null +++ b/acceptance/bundle/validate/no_writes/test.toml @@ -0,0 +1 @@ +RecordRequests = true diff --git a/acceptance/cmd/sync-dryrun-missing-remote/hello.py b/acceptance/cmd/sync-dryrun-missing-remote/hello.py new file mode 100644 index 00000000000..11b15b1a458 --- /dev/null +++ b/acceptance/cmd/sync-dryrun-missing-remote/hello.py @@ -0,0 +1 @@ +print("hello") diff --git a/acceptance/cmd/sync-dryrun-missing-remote/out.test.toml b/acceptance/cmd/sync-dryrun-missing-remote/out.test.toml new file mode 100644 index 00000000000..f784a183258 --- /dev/null +++ b/acceptance/cmd/sync-dryrun-missing-remote/out.test.toml @@ -0,0 +1,3 @@ +Local = true +Cloud = false +EnvMatrix.DATABRICKS_BUNDLE_ENGINE = ["terraform", "direct"] diff --git a/acceptance/cmd/sync-dryrun-missing-remote/output.txt b/acceptance/cmd/sync-dryrun-missing-remote/output.txt new file mode 100644 index 00000000000..5fd287c3e66 --- /dev/null +++ b/acceptance/cmd/sync-dryrun-missing-remote/output.txt @@ -0,0 +1,38 @@ + +>>> [CLI] sync . /Users/[USERNAME]/missing-dir --dry-run +Warn: Running in dry-run mode. No actual changes will be made. +Initial Sync Complete +Uploaded .gitignore +Uploaded hello.py + +>>> print_requests.py --sort //api/ + +>>> [CLI] sync . /Users/[USERNAME]/missing-dir +Initial Sync Complete +Uploaded .gitignore +Uploaded hello.py + +>>> print_requests.py --sort //api/ +{ + "method": "POST", + "path": "/api/2.0/workspace-files/import-file/Users/[USERNAME]/missing-dir/.gitignore", + "q": { + "overwrite": "true" + }, + "raw_body": ".databricks/\nscript\ntest.toml\noutput.txt\nout.requests.txt\nrepls.json\n" +} +{ + "method": "POST", + "path": "/api/2.0/workspace-files/import-file/Users/[USERNAME]/missing-dir/hello.py", + "q": { + "overwrite": "true" + }, + "raw_body": "print(\"hello\")\n" +} +{ + "method": "POST", + "path": "/api/2.0/workspace/mkdirs", + "body": { + "path": "/Users/[USERNAME]/missing-dir" + } +} diff --git a/acceptance/cmd/sync-dryrun-missing-remote/script b/acceptance/cmd/sync-dryrun-missing-remote/script new file mode 100644 index 00000000000..38c118bcd02 --- /dev/null +++ b/acceptance/cmd/sync-dryrun-missing-remote/script @@ -0,0 +1,25 @@ +mkdir .git +cat > .gitignore << EOF +.databricks/ +script +test.toml +output.txt +out.requests.txt +repls.json +EOF + +cleanup() { + rm -rf .git .gitignore .databricks +} +trap cleanup EXIT + +# A dry run must not create the missing remote dir or upload anything. +# Note: output line starting with Action lists files in non-deterministic order so we filter it out +# MSYS_NO_PATHCONV=1 prevents Git Bash on Windows from converting /Users/... to C:/Program Files/Git/Users/... +MSYS_NO_PATHCONV=1 trace $CLI sync . /Users/$CURRENT_USER_NAME/missing-dir --dry-run | grep -v "^Action" | sort + +trace print_requests.py --sort //api/ + +MSYS_NO_PATHCONV=1 trace $CLI sync . /Users/$CURRENT_USER_NAME/missing-dir | grep -v "^Action" | sort + +trace print_requests.py --sort //api/ diff --git a/acceptance/cmd/sync-dryrun-missing-remote/test.toml b/acceptance/cmd/sync-dryrun-missing-remote/test.toml new file mode 100644 index 00000000000..159efe02696 --- /dev/null +++ b/acceptance/cmd/sync-dryrun-missing-remote/test.toml @@ -0,0 +1 @@ +RecordRequests = true diff --git a/bundle/config/validate/files_to_sync.go b/bundle/config/validate/files_to_sync.go index aea78f7104b..1bd92413c11 100644 --- a/bundle/config/validate/files_to_sync.go +++ b/bundle/config/validate/files_to_sync.go @@ -7,6 +7,7 @@ import ( "github.com/databricks/cli/bundle/deploy/files" "github.com/databricks/cli/libs/diag" "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/sync" ) func FilesToSync() bundle.ReadOnlyMutator { @@ -26,12 +27,21 @@ func (v *filesToSync) Apply(ctx context.Context, b *bundle.Bundle) diag.Diagnost return nil } - sync, err := files.GetSync(ctx, b) + opts, err := files.GetSyncOptions(ctx, b) if err != nil { return diag.FromErr(err) } - fl, err := sync.GetFileList(ctx) + // Validation must not mutate the workspace; a dry-run sync skips creating + // a missing remote file path. + opts.DryRun = true + + s, err := sync.New(ctx, *opts) + if err != nil { + return diag.FromErr(err) + } + + fl, err := s.GetFileList(ctx) if err != nil { return diag.FromErr(err) } diff --git a/bundle/deploy/files/sync.go b/bundle/deploy/files/sync.go index 90f44a98d8b..e1116dd87a8 100644 --- a/bundle/deploy/files/sync.go +++ b/bundle/deploy/files/sync.go @@ -8,14 +8,6 @@ import ( "github.com/databricks/cli/libs/sync" ) -func GetSync(ctx context.Context, b *bundle.Bundle) (*sync.Sync, error) { - opts, err := GetSyncOptions(ctx, b) - if err != nil { - return nil, fmt.Errorf("cannot get sync options: %w", err) - } - return sync.New(ctx, *opts) -} - func GetSyncOptions(ctx context.Context, b *bundle.Bundle) (*sync.SyncOptions, error) { cacheDir, err := b.LocalStateDir(ctx) if err != nil { diff --git a/integration/cmd/sync/sync_test.go b/integration/cmd/sync/sync_test.go index 96f00284919..e170c6259bc 100644 --- a/integration/cmd/sync/sync_test.go +++ b/integration/cmd/sync/sync_test.go @@ -493,12 +493,12 @@ func TestSyncEnsureRemotePathIsUsableIfRepoDoesntExist(t *testing.T) { // Hypothetical repo path doesn't exist. nonExistingRepoPath := fmt.Sprintf("/Repos/%s/%s", me.UserName, testutil.RandomName("doesnt-exist-")) - err = sync.EnsureRemotePathIsUsable(ctx, wsc, nonExistingRepoPath, nil) + err = sync.EnsureRemotePathIsUsable(ctx, wsc, nonExistingRepoPath, nil, false) assert.ErrorContains(t, err, " does not exist; please create it first") // Paths nested under a hypothetical repo path should yield the same error. nestedPath := path.Join(nonExistingRepoPath, "nested/directory") - err = sync.EnsureRemotePathIsUsable(ctx, wsc, nestedPath, nil) + err = sync.EnsureRemotePathIsUsable(ctx, wsc, nestedPath, nil, false) assert.ErrorContains(t, err, " does not exist; please create it first") } @@ -509,12 +509,12 @@ func TestSyncEnsureRemotePathIsUsableIfRepoExists(t *testing.T) { _, remoteRepoPath := setupRepo(t, wsc, ctx) // Repo itself is usable. - err := sync.EnsureRemotePathIsUsable(ctx, wsc, remoteRepoPath, nil) + err := sync.EnsureRemotePathIsUsable(ctx, wsc, remoteRepoPath, nil, false) assert.NoError(t, err) // Path nested under repo path is usable. nestedPath := path.Join(remoteRepoPath, "nested/directory") - err = sync.EnsureRemotePathIsUsable(ctx, wsc, nestedPath, nil) + err = sync.EnsureRemotePathIsUsable(ctx, wsc, nestedPath, nil, false) assert.NoError(t, err) // Verify that the directory has been created. @@ -531,7 +531,7 @@ func TestSyncEnsureRemotePathIsUsableInWorkspace(t *testing.T) { require.NoError(t, err) remotePath := fmt.Sprintf("/Users/%s/%s", me.UserName, testutil.RandomName("ensure-path-exists-test-")) - err = sync.EnsureRemotePathIsUsable(ctx, wsc, remotePath, me) + err = sync.EnsureRemotePathIsUsable(ctx, wsc, remotePath, me, false) assert.NoError(t, err) // Clean up directory after test. diff --git a/libs/sync/path.go b/libs/sync/path.go index 6976fd8e0d6..f98099d2b81 100644 --- a/libs/sync/path.go +++ b/libs/sync/path.go @@ -24,7 +24,8 @@ func repoPathForPath(me *iam.User, remotePath string) string { // EnsureRemotePathIsUsable checks if the specified path is nested under // expected base paths and if it is a directory or repository. -func EnsureRemotePathIsUsable(ctx context.Context, wsc *databricks.WorkspaceClient, remotePath string, me *iam.User) error { +// If dryRun is set, a missing remote directory is not created. +func EnsureRemotePathIsUsable(ctx context.Context, wsc *databricks.WorkspaceClient, remotePath string, me *iam.User, dryRun bool) error { var err error // TODO: we should cache CurrentUser.Me at the SDK level @@ -55,6 +56,11 @@ func EnsureRemotePathIsUsable(ctx context.Context, wsc *databricks.WorkspaceClie } } + // A dry run must not create the missing directory; nothing left to validate. + if dryRun { + return nil + } + // The workspace path doesn't exist. Create it and try again. err = wsc.Workspace.MkdirsByPath(ctx, remotePath) //nolint:staticcheck // Deprecated in SDK v0.127.0. Migration to WorkspaceHierarchyService tracked separately. if err != nil { diff --git a/libs/sync/sync.go b/libs/sync/sync.go index c65b49eb775..6d7708c8b37 100644 --- a/libs/sync/sync.go +++ b/libs/sync/sync.go @@ -84,7 +84,7 @@ func New(ctx context.Context, opts SyncOptions) (*Sync, error) { } // Verify that the remote path we're about to synchronize to is valid and allowed. - err = EnsureRemotePathIsUsable(ctx, opts.WorkspaceClient, opts.RemotePath, opts.CurrentUser) + err = EnsureRemotePathIsUsable(ctx, opts.WorkspaceClient, opts.RemotePath, opts.CurrentUser, opts.DryRun) if err != nil { return nil, err } diff --git a/libs/sync/watchdog.go b/libs/sync/watchdog.go index 4a47acfb836..d3bb57662d9 100644 --- a/libs/sync/watchdog.go +++ b/libs/sync/watchdog.go @@ -63,14 +63,14 @@ func (s *Sync) applyMkdir(ctx context.Context, localName string) error { func (s *Sync) applyPut(ctx context.Context, localName string) error { s.notifyProgress(ctx, EventActionPut, localName, 0.0) - localFile, err := s.LocalRoot.Open(localName) - if err != nil { - return err - } + if !s.DryRun { + localFile, err := s.LocalRoot.Open(localName) + if err != nil { + return err + } - defer localFile.Close() + defer localFile.Close() - if !s.DryRun { opts := []filer.WriteMode{filer.CreateParentDirectories, filer.OverwriteIfExists} err = s.filer.Write(ctx, localName, localFile, opts...) if err != nil { diff --git a/libs/sync/watchdog_test.go b/libs/sync/watchdog_test.go new file mode 100644 index 00000000000..91378017fbc --- /dev/null +++ b/libs/sync/watchdog_test.go @@ -0,0 +1,22 @@ +package sync + +import ( + "testing" + + "github.com/databricks/cli/libs/vfs" + "github.com/stretchr/testify/assert" +) + +func TestApplyPutDryRunSkipsLocalRead(t *testing.T) { + s := &Sync{ + SyncOptions: &SyncOptions{ + LocalRoot: vfs.MustNew(t.TempDir()), + DryRun: true, + }, + notifier: &NopNotifier{}, + } + + // The missing file must not fail a dry run; the nil filer guarantees no write is attempted. + err := s.applyPut(t.Context(), "missing.txt") + assert.NoError(t, err) +}