Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Feature] Skip calling Read after Create/Update operations for notebooks #4173

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion exporter/importables.go
Original file line number Diff line number Diff line change
Expand Up @@ -1614,7 +1614,15 @@ var resourcesMap map[string]importable = map[string]importable{
ic.emitWorkspaceObjectParentDirectory(r)
return r.Data.Set("source", fileName)
},
ShouldOmitField: shouldOmitMd5Field,
ShouldOmitField: func(ic *importContext, pathString string, as *schema.Schema, d *schema.ResourceData) bool {
switch pathString {
case "language":
return d.Get("language") == ""
case "format":
return d.Get("format") == "SOURCE"
}
return shouldOmitMd5Field(ic, pathString, as, d)
},
Depends: []reference{
{Path: "source", File: true},
{Path: "path", Resource: "databricks_directory", MatchType: MatchLongestPrefix,
Expand Down
28 changes: 28 additions & 0 deletions internal/acceptance/notebook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,31 @@ func TestAccNotebookResourceScalability(t *testing.T) {
}`,
})
}

func TestAccNotebookResourceDbcUpdate(t *testing.T) {
WorkspaceLevel(t, Step{
Template: `resource "databricks_notebook" "this" {
source = "{var.CWD}/../../workspace/acceptance/testdata/acc-test-update1.dbc"
path = "/Shared/provider-test/dbc_{var.STICKY_RANDOM}"
}`,
}, Step{
Template: `resource "databricks_notebook" "this" {
source = "{var.CWD}/../../workspace/acceptance/testdata/acc-test-update2.dbc"
path = "/Shared/provider-test/dbc_{var.STICKY_RANDOM}"
}`,
})
}

func TestAccNotebookResourceJupiterUpdate(t *testing.T) {
WorkspaceLevel(t, Step{
Template: `resource "databricks_notebook" "this" {
source = "{var.CWD}/../../workspace/acceptance/testdata/acc-test-update1.ipynb"
path = "/Shared/provider-test/jupiter_{var.STICKY_RANDOM}"
}`,
}, Step{
Template: `resource "databricks_notebook" "this" {
source = "{var.CWD}/../../workspace/acceptance/testdata/acc-test-update2.ipynb"
path = "/Shared/provider-test/jupiter_{var.STICKY_RANDOM}"
}`,
})
}
Binary file not shown.
42 changes: 42 additions & 0 deletions workspace/acceptance/testdata/acc-test-update1.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a readme for the .dbc files? It's not clear what is in those.

"cells": [
{
"cell_type": "code",
"execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
"cellMetadata": {},
"inputWidgets": {},
"nuid": "b921d27d-a2bd-4ec8-bc2d-92ccc095246f",
"showTitle": false,
"tableResultSettingsMap": {},
"title": ""
}
},
"outputs": [],
"source": [
"print(\"Hello World\")"
]
}
],
"metadata": {
"application/vnd.databricks.v1+notebook": {
"dashboards": [],
"environmentMetadata": {
"base_environment": "",
"client": "1"
},
"language": "python",
"notebookMetadata": {
"pythonIndentUnit": 4
},
"notebookName": "1",
"widgets": {}
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
Binary file not shown.
60 changes: 60 additions & 0 deletions workspace/acceptance/testdata/acc-test-update2.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"cells": [
{
"cell_type": "code",
"execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
"cellMetadata": {},
"inputWidgets": {},
"nuid": "769d5d72-59f3-417b-9f8c-08785a9c4423",
"showTitle": false,
"tableResultSettingsMap": {},
"title": ""
}
},
"outputs": [],
"source": [
"print(\"First Hello\")"
]
},
{
"cell_type": "code",
"execution_count": 0,
"metadata": {
"application/vnd.databricks.v1+cell": {
"cellMetadata": {},
"inputWidgets": {},
"nuid": "b921d27d-a2bd-4ec8-bc2d-92ccc095246f",
"showTitle": false,
"tableResultSettingsMap": {},
"title": ""
}
},
"outputs": [],
"source": [
"print(\"Hello World\")"
]
}
],
"metadata": {
"application/vnd.databricks.v1+notebook": {
"dashboards": [],
"environmentMetadata": {
"base_environment": "",
"client": "1"
},
"language": "python",
"notebookMetadata": {
"pythonIndentUnit": 4
},
"notebookName": "1",
"widgets": {}
},
"language_info": {
"name": "python"
}
},
"nbformat": 4,
"nbformat_minor": 0
}
87 changes: 63 additions & 24 deletions workspace/resource_notebook.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,10 @@ type ObjectStatus struct {
Size int64 `json:"size,omitempty"`
}

type ImportResponse struct {
ObjectID int64 `json:"object_id,omitempty"`
}

// ExportPath contains the base64 content of the notebook
type ExportPath struct {
Content string `json:"content,omitempty"`
Expand Down Expand Up @@ -98,12 +102,14 @@ type NotebooksAPI struct {
var mtx = &sync.Mutex{}

// Create creates a notebook given the content and path
func (a NotebooksAPI) Create(r ImportPath) error {
func (a NotebooksAPI) Create(r ImportPath) (ImportResponse, error) {
if r.Format == "DBC" {
mtx.Lock()
defer mtx.Unlock()
}
return a.client.Post(a.context, "/workspace/import", r, nil)
var responce ImportResponse
err := a.client.Post(a.context, "/workspace/import", r, &responce)
return responce, err
alexott marked this conversation as resolved.
Show resolved Hide resolved
}

// Read returns the notebook metadata and not the contents
Expand Down Expand Up @@ -203,31 +209,29 @@ func (a NotebooksAPI) Delete(path string, recursive bool) error {
}, nil)
}

func setComputedProperties(d *schema.ResourceData, c *common.DatabricksClient) {
d.Set("url", c.FormatURL("#workspace", d.Id()))
d.Set("workspace_path", "/Workspace"+d.Id())
}

// ResourceNotebook manages notebooks
func ResourceNotebook() common.Resource {
s := FileContentSchema(map[string]*schema.Schema{
"language": {
Type: schema.TypeString,
Optional: true,
Computed: true, // we need it because it will be filled by the provider or backend
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 thank you for correcting this

ValidateFunc: validation.StringInSlice([]string{
Scala,
Python,
R,
SQL,
}, false),
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
source := d.Get("source").(string)
if source == "" {
return false
}
ext := strings.ToLower(filepath.Ext(source))
return old == extMap[ext].Language
},
},
"format": {
Type: schema.TypeString,
Optional: true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't need to change it in this PR, but is this really optional? From the docs, users can't specify this, and it seems like we always set it in Create based on the language/source rather than accepting the user-provided value.

Default: "SOURCE",
Computed: true,
ValidateFunc: validation.StringInSlice([]string{
"SOURCE",
"DBC",
Expand Down Expand Up @@ -258,6 +262,9 @@ func ResourceNotebook() common.Resource {
return common.Resource{
Schema: s,
SchemaVersion: 1,
CanSkipReadAfterCreateAndUpdate: func(d *schema.ResourceData) bool {
return d.Get("format").(string) == "SOURCE"
},
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
content, err := ReadContent(d)
if err != nil {
Expand All @@ -272,6 +279,10 @@ func ResourceNotebook() common.Resource {
Path: path,
Overwrite: true,
}
if createNotebook.Format == "" && createNotebook.Language != "" {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can users specify format? It's not documented on the public docs: https://registry.terraform.io/providers/databricks/databricks/1.61.0/docs/resources/notebook. If not, we can remove the first branch of the if since it is always true.

createNotebook.Format = "SOURCE"
d.Set("format", createNotebook.Format)
}
Comment on lines +282 to +285
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To make this easier to follow, it seems like we should do something like the following:

if createNotebook.Language == "" {
	// Users must not specify `content_base64` without a language, so
	// `source` must be set.
	ext := strings.ToLower(filepath.Ext(d.Get("source").(string)))
	createNotebook.Language = extMap[ext].Language
	createNotebook.Format = extMap[ext].Format
	// Overwrite cannot be used for Dbc format
	createNotebook.Overwrite = extMap[ext].Overwrite
	d.Set("language", createNotebook.Language)
} else {
	createNotebook.Format = "SOURCE"
}
d.Set("format", createNotebook.Format)

Is this logic correct?

if createNotebook.Language == "" {
// TODO: check what happens with empty source
ext := strings.ToLower(filepath.Ext(d.Get("source").(string)))
Expand All @@ -281,8 +292,9 @@ func ResourceNotebook() common.Resource {
createNotebook.Overwrite = extMap[ext].Overwrite
// by default it's SOURCE, but for DBC we have to change it
d.Set("format", createNotebook.Format)
d.Set("language", createNotebook.Language)
}
err = notebooksAPI.Create(createNotebook)
resp, err := notebooksAPI.Create(createNotebook)
if err != nil {
if isParentDoesntExistError(err) {
parent := filepath.ToSlash(filepath.Dir(path))
Expand All @@ -291,16 +303,32 @@ func ResourceNotebook() common.Resource {
if err != nil {
return err
}
err = notebooksAPI.Create(createNotebook)
resp, err = notebooksAPI.Create(createNotebook)
}
if err != nil {
return err
}
}
if d.Get("object_type").(string) == "" {
d.Set("object_type", Notebook)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we need to know the object type here? Isn't it always notebook for a databricks_notebook? Or is it because this can be a DBC file which can contain other stuff as well?

I am also missing when it would be set before (when this if block would be skipped). It's marked as deprecated and should always be notebook, I guess?

}
d.Set("object_id", resp.ObjectID)
d.SetId(path)
setComputedProperties(d, c)
return nil
},
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
oldFormat := d.Get("format").(string)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

To clarify, we are doing this in the Read() method for resource import? We should set these fields correctly on create/update definitely. IIUC we won't have access to the source field during import, so there will be a diff anyways after importing a notebook, and this would be set in the update anyways. So I'm wondering why we need to set these in Read() at all.

if oldFormat == "" {
source := d.Get("source").(string)
// check if `source` is set, and if it is, use file exension to determine format
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
// check if `source` is set, and if it is, use file exension to determine format
// check if `source` is set, and if it is, use file extension to determine format

if source != "" {
ext := strings.ToLower(filepath.Ext(source))
oldFormat = extMap[ext].Format
} else {
oldFormat = "SOURCE"
}
}
w, err := c.WorkspaceClient()
if err != nil {
return err
Expand All @@ -311,9 +339,12 @@ func ResourceNotebook() common.Resource {
if err != nil {
return err
}
d.Set("url", c.FormatURL("#workspace", d.Id()))
d.Set("workspace_path", "/Workspace"+objectStatus.Path)
return common.StructToData(objectStatus, s, d)
setComputedProperties(d, c)
err = common.StructToData(objectStatus, s, d)
if err != nil {
return err
}
return d.Set("format", oldFormat)
},
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
notebooksAPI := NewNotebooksAPI(ctx, c)
Expand All @@ -322,25 +353,33 @@ func ResourceNotebook() common.Resource {
return err
}
format := d.Get("format").(string)
var resp ImportResponse
if format == "DBC" {
// Overwrite cannot be used for source format when importing a folder
err = notebooksAPI.Delete(d.Id(), true)
if err != nil {
return err
}
return notebooksAPI.Create(ImportPath{
resp, err = notebooksAPI.Create(ImportPath{
Content: base64.StdEncoding.EncodeToString(content),
Format: format,
Path: d.Id(),
})
} else {
resp, err = notebooksAPI.Create(ImportPath{
Content: base64.StdEncoding.EncodeToString(content),
Language: d.Get("language").(string),
Format: format,
Overwrite: true,
Path: d.Id(),
})
}
return notebooksAPI.Create(ImportPath{
Content: base64.StdEncoding.EncodeToString(content),
Language: d.Get("language").(string),
Format: format,
Overwrite: true,
Path: d.Id(),
})
if err != nil {
return err
}
d.Set("object_id", resp.ObjectID)
setComputedProperties(d, c)
return nil
},
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
objType := d.Get("object_type")
Expand Down
Loading
Loading