diff --git a/exporter/importables.go b/exporter/importables.go index 04833df81..f208b9a3c 100644 --- a/exporter/importables.go +++ b/exporter/importables.go @@ -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, diff --git a/workspace/resource_notebook.go b/workspace/resource_notebook.go index deed2caa7..6e33ab86c 100644 --- a/workspace/resource_notebook.go +++ b/workspace/resource_notebook.go @@ -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"` @@ -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 } // Read returns the notebook metadata and not the contents @@ -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 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, - Default: "SOURCE", + Computed: true, ValidateFunc: validation.StringInSlice([]string{ "SOURCE", "DBC", @@ -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 { @@ -272,6 +279,10 @@ func ResourceNotebook() common.Resource { Path: path, Overwrite: true, } + if createNotebook.Format == "" && createNotebook.Language != "" { + createNotebook.Format = "SOURCE" + d.Set("format", createNotebook.Format) + } if createNotebook.Language == "" { // TODO: check what happens with empty source ext := strings.ToLower(filepath.Ext(d.Get("source").(string))) @@ -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)) @@ -291,16 +303,25 @@ 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) + } + 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) + if oldFormat == "" { + oldFormat = "SOURCE" + } w, err := c.WorkspaceClient() if err != nil { return err @@ -311,9 +332,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) @@ -322,25 +346,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") diff --git a/workspace/resource_notebook_test.go b/workspace/resource_notebook_test.go index 304ac55d8..005caeb5f 100644 --- a/workspace/resource_notebook_test.go +++ b/workspace/resource_notebook_test.go @@ -36,6 +36,42 @@ func TestResourceNotebookRead(t *testing.T) { "language": "PYTHON", "id": path, "workspace_path": "/Workspace" + path, + "format": "SOURCE", + }) +} + +func TestResourceNotebookReadWithState(t *testing.T) { + path := "/test/path.py" + objectID := 12345 + qa.ResourceFixture{ + Fixtures: []qa.HTTPFixture{ + { + Method: http.MethodGet, + Resource: "/api/2.0/workspace/get-status?path=%2Ftest%2Fpath.py", + Response: ObjectStatus{ + ObjectID: int64(objectID), + ObjectType: Notebook, + Path: path, + Language: "PYTHON", + }, + }, + }, + Resource: ResourceNotebook(), + Read: true, + New: true, + State: map[string]any{ + "source": "acceptance/testdata/tf-test-jupyter.ipynb", + "format": "JUPYTER", + "path": "/foo/path.py", + }, + ID: path, + }.ApplyAndExpectData(t, map[string]any{ + "path": path, + "object_id": objectID, + "language": "PYTHON", + "id": path, + "workspace_path": "/Workspace" + path, + "format": "JUPYTER", }) } @@ -119,22 +155,8 @@ func TestResourceNotebookCreate_DirectoryExist(t *testing.T) { Overwrite: true, Format: "SOURCE", }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/export?format=SOURCE&path=%2Ffoo%2Fpath.py", - Response: ExportPath{ - Content: "YWJjCg==", - }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/get-status?path=%2Ffoo%2Fpath.py", - Response: ObjectStatus{ - ObjectID: 4567, - ObjectType: "NOTEBOOK", - Path: "/foo/path.py", - Language: "PYTHON", + Response: ImportResponse{ + ObjectID: 12345, }, }, }, @@ -146,8 +168,10 @@ func TestResourceNotebookCreate_DirectoryExist(t *testing.T) { }, Create: true, }.ApplyAndExpectData(t, map[string]any{ - "path": "/foo/path.py", - "id": "/foo/path.py", + "path": "/foo/path.py", + "id": "/foo/path.py", + "object_id": 12345, + "format": "SOURCE", }) } @@ -187,22 +211,8 @@ func TestResourceNotebookCreate_DirectoryDoesntExist(t *testing.T) { Overwrite: true, Format: "SOURCE", }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/export?format=SOURCE&path=%2Ffoo%2Fpath.py", - Response: ExportPath{ - Content: "YWJjCg==", - }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/get-status?path=%2Ffoo%2Fpath.py", - Response: ObjectStatus{ - ObjectID: 4567, - ObjectType: "NOTEBOOK", - Path: "/foo/path.py", - Language: "PYTHON", + Response: ImportResponse{ + ObjectID: 12345, }, }, }, @@ -214,8 +224,9 @@ func TestResourceNotebookCreate_DirectoryDoesntExist(t *testing.T) { }, Create: true, }.ApplyAndExpectData(t, map[string]any{ - "path": "/foo/path.py", - "id": "/foo/path.py", + "path": "/foo/path.py", + "id": "/foo/path.py", + "object_id": 12345, }) } @@ -294,13 +305,17 @@ func TestResourceNotebookCreateSource_Jupyter(t *testing.T) { Overwrite: true, Format: "JUPYTER", }, + Response: ImportResponse{ + ObjectID: 12345, + }, }, { Method: http.MethodGet, Resource: "/api/2.0/workspace/get-status?path=%2FMars", Response: ObjectStatus{ - ObjectID: 4567, + ObjectID: 12345, ObjectType: "NOTEBOOK", + Language: "PYTHON", Path: "/Mars", }, }, @@ -312,7 +327,9 @@ func TestResourceNotebookCreateSource_Jupyter(t *testing.T) { }, Create: true, }.ApplyAndExpectData(t, map[string]any{ - "id": "/Mars", + "id": "/Mars", + "object_id": 12345, + "language": "PYTHON", }) } @@ -331,15 +348,8 @@ func TestResourceNotebookCreateSource(t *testing.T) { Overwrite: true, Format: "SOURCE", }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/get-status?path=%2FDashboard", - Response: ObjectStatus{ - ObjectID: 4567, - ObjectType: "NOTEBOOK", - Path: "/Dashboard", - Language: "SQL", + Response: ImportResponse{ + ObjectID: 12345, }, }, }, @@ -350,7 +360,10 @@ func TestResourceNotebookCreateSource(t *testing.T) { }, Create: true, }.ApplyAndExpectData(t, map[string]any{ - "id": "/Dashboard", + "id": "/Dashboard", + "object_id": 12345, + "language": "SQL", + "format": "SOURCE", }) } @@ -410,18 +423,11 @@ func TestResourceNotebookUpdate(t *testing.T) { Format: "SOURCE", Overwrite: true, Content: "YWJjCg==", - Path: "abc", + Path: "/path.py", Language: "R", }, - }, - { - Method: http.MethodGet, - Resource: "/api/2.0/workspace/get-status?path=abc", - Response: ObjectStatus{ - ObjectID: 4567, - ObjectType: "NOTEBOOK", - Path: "abc", - Language: "R", + Response: ImportResponse{ + ObjectID: 12345, }, }, }, @@ -430,11 +436,16 @@ func TestResourceNotebookUpdate(t *testing.T) { "content_base64": "YWJjCg==", "language": "R", "path": "/path.py", + "format": "SOURCE", }, - ID: "abc", + ID: "/path.py", RequiresNew: true, Update: true, - }.ApplyNoError(t) + }.ApplyAndExpectData(t, map[string]any{ + "id": "/path.py", + "object_id": 12345, + "language": "R", + }) } func TestResourceNotebookUpdate_DBC(t *testing.T) { @@ -445,7 +456,7 @@ func TestResourceNotebookUpdate_DBC(t *testing.T) { Resource: "/api/2.0/workspace/delete", ExpectedRequest: DeletePath{ Recursive: true, - Path: "abc", + Path: "/path.py", }, }, { @@ -454,38 +465,33 @@ func TestResourceNotebookUpdate_DBC(t *testing.T) { ExpectedRequest: ImportPath{ Format: "DBC", Content: "YWJjCg==", - Path: "abc", + Path: "/path.py", }, }, { Method: http.MethodGet, - Resource: "/api/2.0/workspace/get-status?path=abc", + Resource: "/api/2.0/workspace/get-status?path=%2Fpath.py", Response: ObjectStatus{ - ObjectID: 4567, + ObjectID: 12345, ObjectType: Directory, - Path: "abc", + Path: "/path.py", }, }, }, Resource: ResourceNotebook(), State: map[string]any{ "content_base64": "YWJjCg==", - // technically language is not needed, but makes the test simpler - "language": "PYTHON", - "format": "DBC", - "path": "/path.py", + "language": "PYTHON", + "format": "DBC", + "path": "/path.py", + "object_id": 45678, }, - ID: "abc", + ID: "/path.py", RequiresNew: true, Update: true, - }.ApplyNoError(t) -} - -func TestNotebookLanguageSuppressSourceDiff(t *testing.T) { - r := ResourceNotebook() - d := r.ToResource().TestResourceData() - d.Set("source", "this.PY") - suppress := r.Schema["language"].DiffSuppressFunc - assert.True(t, suppress("language", Python, Python, d)) + }.ApplyAndExpectData(t, map[string]any{ + "id": "/path.py", + "object_id": 12345, + }) }