Skip to content

Commit 3c2c2b5

Browse files
authored
Add databricks_workspace_file resource (#2266)
* Add `databricks_workspace_file` resource With recent release of the workspace file support it's now possible to store arbitrary files in the workspace, not only notebooks. It's also recommended way to store init scripts to avoid security problems associated with storing init scripts on DBFS. * Address review comments * swtich to SDK * One more fix
1 parent 51a98b8 commit 3c2c2b5

File tree

11 files changed

+603
-11
lines changed

11 files changed

+603
-11
lines changed

docs/resources/notebook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ In addition to all arguments above, the following attributes are exported:
6363

6464
## Access Control
6565

66-
* [databricks_permissions](permissions.md#Notebook-usage) can control which groups or individual users can access notebooks or folders.
66+
* [databricks_permissions](permissions.md#notebook-usage) can control which groups or individual users can access notebooks or folders.
6767

6868
## Import
6969

docs/resources/permissions.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -320,6 +320,45 @@ resource "databricks_permissions" "notebook_usage" {
320320
}
321321
```
322322

323+
## Workspace file usage
324+
325+
Valid permission levels for [databricks_workspace_file](workspace_file.md) are: `CAN_READ`, `CAN_RUN`, `CAN_EDIT`, and `CAN_MANAGE`.
326+
327+
```hcl
328+
resource "databricks_group" "auto" {
329+
display_name = "Automation"
330+
}
331+
332+
resource "databricks_group" "eng" {
333+
display_name = "Engineering"
334+
}
335+
336+
resource "databricks_workspace_file" "this" {
337+
content_base64 = base64encode("print('Hello World')")
338+
path = "/Production/ETL/Features.py"
339+
}
340+
341+
resource "databricks_permissions" "workspace_file_usage" {
342+
workspace_file_path = databricks_workspace_file.this.path
343+
344+
access_control {
345+
group_name = "users"
346+
permission_level = "CAN_READ"
347+
}
348+
349+
access_control {
350+
group_name = databricks_group.auto.display_name
351+
permission_level = "CAN_RUN"
352+
}
353+
354+
access_control {
355+
group_name = databricks_group.eng.display_name
356+
permission_level = "CAN_EDIT"
357+
}
358+
}
359+
```
360+
361+
323362
## Folder usage
324363

325364
Valid [permission levels](https://docs.databricks.com/security/access-control/workspace-acl.html#folder-permissions) for folders of [databricks_directory](directory.md) are: `CAN_READ`, `CAN_RUN`, `CAN_EDIT`, and `CAN_MANAGE`. Notebooks and experiments in a folder inherit all permissions settings of that folder. For example, a user (or service principal) that has `CAN_RUN` permission on a folder has `CAN_RUN` permission on the notebooks in that folder.

docs/resources/workspace_file.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
---
2+
subcategory: "Workspace"
3+
---
4+
# databricks_workspace_file Resource
5+
6+
This resource allows you to manage [Databricks Workspace Files](https://docs.databricks.com/files/workspace.html).
7+
8+
## Example Usage
9+
10+
You can declare Terraform-managed workspace file by specifying `source` attribute of corresponding local file.
11+
12+
```hcl
13+
data "databricks_current_user" "me" {
14+
}
15+
16+
resource "databricks_workspace_file" "module" {
17+
source = "${path.module}/module.py"
18+
path = "${data.databricks_current_user.me.home}/AA/BB/CC"
19+
}
20+
```
21+
22+
You can also create a managed workspace file with inline sources through `content_base64` attribute.
23+
24+
```hcl
25+
resource "databricks_workspace_file" "init_script" {
26+
content_base64 = base64encode(<<-EOT
27+
#!/bin/bash
28+
echo "Hello World"
29+
EOT
30+
)
31+
path = "/Shared/init-script.sh"
32+
}
33+
```
34+
35+
## Argument Reference
36+
37+
-> **Note** Files in Databricks workspace would only be changed, if Terraform stage did change. This means that any manual changes to managed workspace files won't be overwritten by Terraform, if there's no local change to file sources. Workspace files are identified by their path, so changing file's name manually on the workspace and then applying Terraform state would result in creation of workspace file from Terraform state.
38+
39+
The size of a workspace file source code must not exceed a few megabytes. The following arguments are supported:
40+
41+
* `path` - (Required) The absolute path of the workspace file, beginning with "/", e.g. "/Demo".
42+
* `source` - Path to file on local filesystem. Conflicts with `content_base64`.
43+
* `content_base64` - The base64-encoded file content. Conflicts with `source`. Use of `content_base64` is discouraged, as it's increasing memory footprint of Terraform state and should only be used in exceptional circumstances, like creating a workspace file with configuration properties for a data pipeline.
44+
45+
## Attribute Reference
46+
47+
In addition to all arguments above, the following attributes are exported:
48+
49+
* `id` - Path of workspace file
50+
* `url` - Routable URL of the workspace file
51+
* `object_id` - Unique identifier for a workspace file
52+
53+
## Access Control
54+
55+
* [databricks_permissions](permissions.md#workspace-file-usage) can control which groups or individual users can access workspace file.
56+
57+
## Import
58+
59+
The workspace file resource can be imported using workspace file path
60+
61+
```bash
62+
$ terraform import databricks_workspace_file.this /path/to/file
63+
```
64+
65+
## Related Resources
66+
67+
The following resources are often used in the same context:
68+
69+
* [End to end workspace management](../guides/workspace-management.md) guide.
70+
* [databricks_cluster](cluster.md) to create [Databricks Clusters](https://docs.databricks.com/clusters/index.html).
71+
* [databricks_directory](directory.md) to manage directories in [Databricks Workpace](https://docs.databricks.com/workspace/workspace-objects.html).
72+
* [databricks_job](job.md) to manage [Databricks Jobs](https://docs.databricks.com/jobs.html) to run non-interactive code in a [databricks_cluster](cluster.md).
73+
* [databricks_pipeline](pipeline.md) to deploy [Delta Live Tables](https://docs.databricks.com/data-engineering/delta-live-tables/index.html).
74+
* [databricks_repo](repo.md) to manage [Databricks Repos](https://docs.databricks.com/repos.html).
75+
* [databricks_secret](secret.md) to manage [secrets](https://docs.databricks.com/security/secrets/index.html#secrets-user-guide) in Databricks workspace.
76+
* [databricks_secret_acl](secret_acl.md) to manage access to [secrets](https://docs.databricks.com/security/secrets/index.html#secrets-user-guide) in Databricks workspace.
77+
* [databricks_secret_scope](secret_scope.md) to create [secret scopes](https://docs.databricks.com/security/secrets/index.html#secrets-user-guide) in Databricks workspace.
78+
* [databricks_user](user.md) to [manage users](https://docs.databricks.com/administration-guide/users-groups/users.html), that could be added to [databricks_group](group.md) within the workspace.
79+
* [databricks_user](../data-sources/user.md) data to retrieve information about [databricks_user](user.md).
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package acceptance
2+
3+
import (
4+
"testing"
5+
)
6+
7+
func TestAccWorkspaceFile(t *testing.T) {
8+
workspaceLevel(t, step{
9+
Template: `resource "databricks_workspace_file" "this" {
10+
source = "{var.CWD}/../../storage/testdata/tf-test-python.py"
11+
path = "/Shared/provider-test/xx_{var.RANDOM}"
12+
}`,
13+
}, step{
14+
Template: `resource "databricks_workspace_file" "this" {
15+
source = "{var.CWD}/../../storage/testdata/tf-test-python.py"
16+
path = "/Shared/provider-test/xx_{var.RANDOM}_renamed"
17+
}`,
18+
})
19+
}
20+
21+
func TestAccWorkspaceFileBase64(t *testing.T) {
22+
workspaceLevel(t, step{
23+
Template: `resource "databricks_workspace_file" "this2" {
24+
"content_base64": "YWJjCg==",
25+
path = "/Shared/provider-test/xx2_{var.RANDOM}"
26+
}`,
27+
}, step{
28+
Template: `resource "databricks_workspace_file" "this2" {
29+
"content_base64": "YWJjCg==",
30+
path = "/Shared/provider-test/xx2_{var.RANDOM}_renamed"
31+
}`,
32+
})
33+
}

permissions/resource_permissions.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,8 @@ func permissionsResourceIDFields() []permissionsIDFieldMapping {
296296
{"notebook_path", "notebook", "notebooks", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, PATH},
297297
{"directory_id", "directory", "directories", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, SIMPLE},
298298
{"directory_path", "directory", "directories", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, PATH},
299+
{"workspace_file_id", "file", "files", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, SIMPLE},
300+
{"workspace_file_path", "file", "files", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, PATH},
299301
{"repo_id", "repo", "repos", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, SIMPLE},
300302
{"repo_path", "repo", "repos", []string{"CAN_READ", "CAN_RUN", "CAN_EDIT", "CAN_MANAGE"}, PATH},
301303
{"authorization", "tokens", "authorization", []string{"CAN_USE"}, SIMPLE},
@@ -336,7 +338,12 @@ func (oa *ObjectACL) ToPermissionsEntity(d *schema.ResourceData, me string) (Per
336338
continue
337339
}
338340
entity.ObjectType = mapping.objectType
339-
pathVariant := d.Get(mapping.objectType + "_path")
341+
var pathVariant any
342+
if mapping.objectType == "file" {
343+
pathVariant = d.Get("workspace_file_path")
344+
} else {
345+
pathVariant = d.Get(mapping.objectType + "_path")
346+
}
340347
if pathVariant != nil && pathVariant.(string) != "" {
341348
// we're not importing and it's a path... it's set, so let's not re-set it
342349
return entity, nil

permissions/resource_permissions_test.go

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -973,6 +973,80 @@ func TestResourcePermissionsCreate_NotebookPath(t *testing.T) {
973973
assert.Equal(t, "CAN_READ", firstElem["permission_level"])
974974
}
975975

976+
func TestResourcePermissionsCreate_WorkspaceFilePath(t *testing.T) {
977+
d, err := qa.ResourceFixture{
978+
Fixtures: []qa.HTTPFixture{
979+
me,
980+
{
981+
Method: http.MethodGet,
982+
Resource: "/api/2.0/workspace/get-status?path=%2FDevelopment%2FInit",
983+
Response: workspace.ObjectStatus{
984+
ObjectID: 988765,
985+
ObjectType: workspace.File,
986+
},
987+
},
988+
{
989+
Method: http.MethodPut,
990+
Resource: "/api/2.0/permissions/files/988765",
991+
ExpectedRequest: AccessControlChangeList{
992+
AccessControlList: []AccessControlChange{
993+
{
994+
UserName: TestingUser,
995+
PermissionLevel: "CAN_READ",
996+
},
997+
},
998+
},
999+
},
1000+
{
1001+
Method: http.MethodGet,
1002+
Resource: "/api/2.0/permissions/files/988765",
1003+
Response: ObjectACL{
1004+
ObjectID: "/files/988765",
1005+
ObjectType: "file",
1006+
AccessControlList: []AccessControl{
1007+
{
1008+
UserName: TestingUser,
1009+
AllPermissions: []Permission{
1010+
{
1011+
PermissionLevel: "CAN_READ",
1012+
Inherited: false,
1013+
},
1014+
},
1015+
},
1016+
{
1017+
UserName: TestingAdminUser,
1018+
AllPermissions: []Permission{
1019+
{
1020+
PermissionLevel: "CAN_MANAGE",
1021+
Inherited: false,
1022+
},
1023+
},
1024+
},
1025+
},
1026+
},
1027+
},
1028+
},
1029+
Resource: ResourcePermissions(),
1030+
State: map[string]any{
1031+
"workspace_file_path": "/Development/Init",
1032+
"access_control": []any{
1033+
map[string]any{
1034+
"user_name": TestingUser,
1035+
"permission_level": "CAN_READ",
1036+
},
1037+
},
1038+
},
1039+
Create: true,
1040+
}.Apply(t)
1041+
1042+
assert.NoError(t, err)
1043+
ac := d.Get("access_control").(*schema.Set)
1044+
require.Equal(t, 1, len(ac.List()))
1045+
firstElem := ac.List()[0].(map[string]any)
1046+
assert.Equal(t, TestingUser, firstElem["user_name"])
1047+
assert.Equal(t, "CAN_READ", firstElem["permission_level"])
1048+
}
1049+
9761050
func TestResourcePermissionsCreate_error(t *testing.T) {
9771051
qa.ResourceFixture{
9781052
Fixtures: []qa.HTTPFixture{

provider/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,7 @@ func DatabricksProvider() *schema.Provider {
154154
"databricks_user_instance_profile": aws.ResourceUserInstanceProfile(),
155155
"databricks_user_role": aws.ResourceUserRole(),
156156
"databricks_workspace_conf": workspace.ResourceWorkspaceConf(),
157+
"databricks_workspace_file": workspace.ResourceWorkspaceFile(),
157158
},
158159
Schema: providerSchema(),
159160
}

workspace/data_notebook_paths.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,12 @@ func DataSourceNotebookPaths() *schema.Resource {
2020
d.SetId(path)
2121
var notebookPathList []map[string]string
2222
for _, v := range notebookList {
23-
notebookPathMap := map[string]string{}
24-
notebookPathMap["path"] = v.Path
25-
notebookPathMap["language"] = string(v.Language)
26-
notebookPathList = append(notebookPathList, notebookPathMap)
23+
if v.ObjectType == Notebook {
24+
notebookPathMap := map[string]string{}
25+
notebookPathMap["path"] = v.Path
26+
notebookPathMap["language"] = string(v.Language)
27+
notebookPathList = append(notebookPathList, notebookPathMap)
28+
}
2729
}
2830
// nolint
2931
d.Set("notebook_path_list", notebookPathList)

workspace/resource_notebook.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,14 @@ import (
1616
// ...
1717
const (
1818
Notebook string = "NOTEBOOK"
19+
File string = "FILE"
1920
Directory string = "DIRECTORY"
2021
Scala string = "SCALA"
2122
Python string = "PYTHON"
2223
SQL string = "SQL"
2324
R string = "R"
2425
Jupyter string = "JUPYTER"
26+
Auto string = "AUTO"
2527
)
2628

2729
type notebookLanguageFormat struct {
@@ -149,7 +151,7 @@ func (a NotebooksAPI) recursiveAddPaths(path string, pathList *[]ObjectStatus) e
149151
return err
150152
}
151153
for _, v := range notebookInfoList {
152-
if v.ObjectType == Notebook {
154+
if v.ObjectType == Notebook || v.ObjectType == File {
153155
*pathList = append(*pathList, v)
154156
} else if v.ObjectType == Directory {
155157
err := a.recursiveAddPaths(v.Path, pathList)
@@ -254,10 +256,9 @@ func ResourceNotebook() *schema.Resource {
254256
Deprecated: "Always is a notebook",
255257
},
256258
"object_id": {
257-
Type: schema.TypeInt,
258-
Optional: true,
259-
Computed: true,
260-
Deprecated: "Use id argument to retrieve object id",
259+
Type: schema.TypeInt,
260+
Optional: true,
261+
Computed: true,
261262
},
262263
})
263264
s["content_base64"].RequiredWith = []string{"language"}

0 commit comments

Comments
 (0)