Skip to content

Commit 6a00182

Browse files
authored
Exporter: generate references that are matching only to a prefix (#1890)
* Exporter: generate references that are matching only to a prefix There are cases when reference isn't a full match of a given value but a prefix of it. For example, user's notebook has a path of `/Users/user@domain/notebook`. To make it working correctly we need to emit user or service principal, and then make a correct reference with dependency on emitted user or notebook, like, `${databricks_user.user_domain.home}/notebook`. This PR adds a new field to dependency specification: `MatchType` that may have following values: - `prefix` - value of attribute of resource is a prefix of a given attribute - `exact` (or empty string) - must have exact matching (previous behaviour) this fixes #1777 * improving test coverage & emit user/sp for repos * Address PR feedback
1 parent 3ebd419 commit 6a00182

File tree

8 files changed

+240
-47
lines changed

8 files changed

+240
-47
lines changed

exporter/context.go

Lines changed: 57 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ func (ic *importContext) MatchesName(n string) bool {
301301
return strings.Contains(strings.ToLower(n), strings.ToLower(ic.match))
302302
}
303303

304-
func (ic *importContext) Find(r *resource, pick string) hcl.Traversal {
304+
func (ic *importContext) Find(r *resource, pick string, matchType MatchType) (string, hcl.Traversal) {
305305
for _, sr := range ic.State.Resources {
306306
if sr.Type != r.Resource {
307307
continue
@@ -313,24 +313,36 @@ func (ic *importContext) Find(r *resource, pick string) hcl.Traversal {
313313
r.Attribute, r.Resource, r.Name, r.ID)
314314
continue
315315
}
316-
if v.(string) == r.Value {
317-
if sr.Mode == "data" {
318-
return hcl.Traversal{
319-
hcl.TraverseRoot{Name: "data"},
320-
hcl.TraverseAttr{Name: sr.Type},
321-
hcl.TraverseAttr{Name: sr.Name},
322-
hcl.TraverseAttr{Name: pick},
323-
}
324-
}
325-
return hcl.Traversal{
326-
hcl.TraverseRoot{Name: sr.Type},
316+
strValue := v.(string)
317+
matched := false
318+
switch matchType {
319+
case MatchExact:
320+
matched = (strValue == r.Value)
321+
case MatchPrefix:
322+
matched = strings.HasPrefix(r.Value, strValue)
323+
default:
324+
log.Printf("[WARN] Unsupported match type: %s", matchType)
325+
}
326+
if !matched {
327+
continue
328+
}
329+
if sr.Mode == "data" {
330+
return strValue, hcl.Traversal{
331+
hcl.TraverseRoot{Name: "data"},
332+
hcl.TraverseAttr{Name: sr.Type},
327333
hcl.TraverseAttr{Name: sr.Name},
328334
hcl.TraverseAttr{Name: pick},
329335
}
330336
}
337+
return strValue, hcl.Traversal{
338+
hcl.TraverseRoot{Name: sr.Type},
339+
hcl.TraverseAttr{Name: sr.Name},
340+
hcl.TraverseAttr{Name: pick},
341+
}
342+
331343
}
332344
}
333-
return nil
345+
return "", nil
334346
}
335347

336348
func (ic *importContext) Has(r *resource) bool {
@@ -486,6 +498,35 @@ func (ic *importContext) Emit(r *resource) {
486498
ic.Add(r)
487499
}
488500

501+
func (ic *importContext) getTraversalTokens(ref reference, value string) hclwrite.Tokens {
502+
matchType := ref.MatchTypeValue()
503+
attr := ref.MatchAttribute()
504+
attrValue, traversal := ic.Find(&resource{
505+
Resource: ref.Resource,
506+
Attribute: attr,
507+
Value: value,
508+
}, attr, matchType)
509+
// at least one invocation of ic.Find will assign Nil to traversal if resource with value is not found
510+
if traversal == nil {
511+
return nil
512+
}
513+
switch matchType {
514+
case MatchExact:
515+
return hclwrite.TokensForTraversal(traversal)
516+
case MatchPrefix:
517+
rest := value[len(attrValue):]
518+
tokens := hclwrite.Tokens{&hclwrite.Token{Type: hclsyntax.TokenOQuote, Bytes: []byte{'"', '$', '{'}}}
519+
tokens = append(tokens, hclwrite.TokensForTraversal(traversal)...)
520+
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCQuote, Bytes: []byte{'}'}})
521+
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenQuotedLit, Bytes: []byte(rest)})
522+
tokens = append(tokens, &hclwrite.Token{Type: hclsyntax.TokenCQuote, Bytes: []byte{'"'}})
523+
return tokens
524+
default:
525+
log.Printf("[WARN] Unsupported match type: %s", ref.MatchType)
526+
}
527+
return nil
528+
}
529+
489530
// TODO: move to IC
490531
var dependsRe = regexp.MustCompile(`(\.[\d]+)`)
491532

@@ -506,18 +547,9 @@ func (ic *importContext) reference(i importable, path []string, value string) hc
506547
if d.Variable {
507548
return ic.variable(fmt.Sprintf("%s_%s", path[0], value), "")
508549
}
509-
attr := "id"
510-
if d.Match != "" {
511-
attr = d.Match
512-
}
513-
traversal := ic.Find(&resource{
514-
Resource: d.Resource,
515-
Attribute: attr,
516-
Value: value,
517-
}, attr)
518-
//at least one invocation of ic.Find will assign Nil to traversal if resource with value is not found
519-
if traversal != nil {
520-
return hclwrite.TokensForTraversal(traversal)
550+
551+
if tokens := ic.getTraversalTokens(d, value); tokens != nil {
552+
return tokens
521553
}
522554
}
523555
return hclwrite.TokensForValue(cty.StringVal(value))

exporter/context_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ func TestMatchesName(t *testing.T) {
1717
}
1818

1919
func TestImportContextFindSkips(t *testing.T) {
20-
assert.Nil(t, (&importContext{
20+
_, traversal := (&importContext{
2121
State: stateApproximation{
2222
Resources: []resourceApproximation{
2323
{
@@ -36,7 +36,8 @@ func TestImportContextFindSkips(t *testing.T) {
3636
Resource: "a",
3737
Attribute: "b",
3838
Name: "c",
39-
}, "x"))
39+
}, "x", "")
40+
assert.Nil(t, traversal)
4041
}
4142

4243
func TestImportContextHas(t *testing.T) {

exporter/exporter_test.go

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1533,6 +1533,51 @@ func TestImportingDLTPipelines(t *testing.T) {
15331533
},
15341534
},
15351535
},
1536+
{
1537+
Method: "GET",
1538+
Resource: "/api/2.0/permissions/repos/123",
1539+
Response: getJSONObject("test-data/get-repo-permissions.json"),
1540+
},
1541+
{
1542+
Method: "GET",
1543+
Resource: "/api/2.0/workspace/get-status?path=%2FRepos%2Fuser%40domain.com%2Frepo",
1544+
Response: workspace.ObjectStatus{
1545+
ObjectID: 123,
1546+
ObjectType: "REPO",
1547+
Path: "/Repos/[email protected]/repo",
1548+
},
1549+
},
1550+
{
1551+
Method: "GET",
1552+
Resource: "/api/2.0/repos/123",
1553+
Response: repos.ReposInformation{
1554+
ID: 123,
1555+
Url: "https://github.com/user/test.git",
1556+
Provider: "gitHub",
1557+
Path: "/Repos/[email protected]/repo",
1558+
HeadCommitID: "1124323423abc23424",
1559+
Branch: "releases",
1560+
},
1561+
ReuseRequest: true,
1562+
},
1563+
{
1564+
Method: "GET",
1565+
Resource: "/api/2.0/preview/scim/v2/Users?filter=userName%20eq%20%27user%40domain.com%27",
1566+
Response: scim.UserList{
1567+
Resources: []scim.User{
1568+
{ID: "123", DisplayName: "[email protected]", UserName: "[email protected]"},
1569+
},
1570+
StartIndex: 1,
1571+
TotalResults: 1,
1572+
ItemsPerPage: 1,
1573+
},
1574+
ReuseRequest: true,
1575+
},
1576+
{
1577+
Method: "GET",
1578+
Resource: "/api/2.0/preview/scim/v2/Users/123",
1579+
Response: scim.User{ID: "123", DisplayName: "[email protected]", UserName: "[email protected]"},
1580+
},
15361581
{
15371582
Method: "GET",
15381583
Resource: "/api/2.0/pipelines/123",
@@ -1597,7 +1642,7 @@ func TestImportingDLTPipelines(t *testing.T) {
15971642
ic := newImportContext(client)
15981643
ic.Directory = tmpDir
15991644
ic.listing = "dlt"
1600-
ic.services = "dlt,access,notebooks,secrets"
1645+
ic.services = "dlt,access,notebooks,users,repos,secrets"
16011646

16021647
err := ic.Run()
16031648
assert.NoError(t, err)

exporter/importables.go

Lines changed: 37 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ var (
4141
nameNormalizationRegex = regexp.MustCompile(`\W+`)
4242
jobClustersRegex = regexp.MustCompile(`^((job_cluster|task)\.[0-9]+\.new_cluster\.[0-9]+\.)`)
4343
dltClusterRegex = regexp.MustCompile(`^(cluster\.[0-9]+\.)`)
44+
uuidRegex = regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`)
4445
predefinedClusterPolicies = []string{"Personal Compute", "Job Compute", "Power User Compute", "Shared Compute"}
4546
secretPathRegex = regexp.MustCompile(`^\{\{secrets\/([^\/]+)\/([^}]+)\}\}$`)
4647
)
@@ -287,6 +288,7 @@ var resourcesMap map[string]importable = map[string]importable{
287288
{Path: "spark_python_task.parameters", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
288289
{Path: "spark_jar_task.jar_uri", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
289290
{Path: "notebook_task.notebook_path", Resource: "databricks_notebook"},
291+
{Path: "notebook_task.notebook_path", Resource: "databricks_repo", Match: "path", MatchType: MatchPrefix},
290292
{Path: "pipeline_task.pipeline_id", Resource: "databricks_pipeline"},
291293
{Path: "task.library.jar", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
292294
{Path: "task.library.whl", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
@@ -295,6 +297,7 @@ var resourcesMap map[string]importable = map[string]importable{
295297
{Path: "task.spark_python_task.parameters", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
296298
{Path: "task.spark_jar_task.jar_uri", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
297299
{Path: "task.notebook_task.notebook_path", Resource: "databricks_notebook"},
300+
{Path: "task.notebook_task.notebook_path", Resource: "databricks_repo", Match: "path", MatchType: MatchPrefix},
298301
{Path: "task.pipeline_task.pipeline_id", Resource: "databricks_pipeline"},
299302
{Path: "task.sql_task.query.query_id", Resource: "databricks_sql_query"},
300303
{Path: "task.sql_task.dashboard.dashboard_id", Resource: "databricks_sql_dashboard"},
@@ -359,10 +362,7 @@ var resourcesMap map[string]importable = map[string]importable{
359362
}
360363
}
361364
if job.NotebookTask != nil {
362-
ic.Emit(&resource{
363-
Resource: "databricks_notebook",
364-
ID: job.NotebookTask.NotebookPath,
365-
})
365+
ic.emitNotebookOrRepo(job.NotebookTask.NotebookPath)
366366
}
367367
if job.PipelineTask != nil {
368368
ic.Emit(&resource{
@@ -373,10 +373,7 @@ var resourcesMap map[string]importable = map[string]importable{
373373
// Support for multitask jobs
374374
for _, task := range job.Tasks {
375375
if task.NotebookTask != nil {
376-
ic.Emit(&resource{
377-
Resource: "databricks_notebook",
378-
ID: task.NotebookTask.NotebookPath,
379-
})
376+
ic.emitNotebookOrRepo(task.NotebookTask.NotebookPath)
380377
}
381378
if task.PipelineTask != nil {
382379
ic.Emit(&resource{
@@ -958,7 +955,21 @@ var resourcesMap map[string]importable = map[string]importable{
958955
if name == "" {
959956
return d.Id()
960957
}
961-
return strings.TrimPrefix(name, "/")
958+
return nameNormalizationRegex.ReplaceAllString(name[7:], "_")
959+
},
960+
Search: func(ic *importContext, r *resource) error {
961+
reposAPI := repos.NewReposAPI(ic.Context, ic.Client)
962+
notebooksAPI := workspace.NewNotebooksAPI(ic.Context, ic.Client)
963+
repoDir, err := notebooksAPI.Read(r.Value)
964+
if err != nil {
965+
return err
966+
}
967+
repo, err := reposAPI.Read(fmt.Sprintf("%d", repoDir.ObjectID))
968+
if err != nil {
969+
return err
970+
}
971+
r.ID = fmt.Sprintf("%d", repo.ID)
972+
return nil
962973
},
963974
List: func(ic *importContext) error {
964975
repoList, err := repos.NewReposAPI(ic.Context, ic.Client).ListAll()
@@ -977,6 +988,7 @@ var resourcesMap map[string]importable = map[string]importable{
977988
return nil
978989
},
979990
Import: func(ic *importContext, r *resource) error {
991+
ic.emitUserOrServicePrincipalForPath(r.Data.Get("path").(string), "/Repos")
980992
if ic.meAdmin {
981993
ic.Emit(&resource{
982994
Resource: "databricks_permissions",
@@ -986,6 +998,17 @@ var resourcesMap map[string]importable = map[string]importable{
986998
}
987999
return nil
9881000
},
1001+
ShouldOmitField: func(ic *importContext, pathString string, as *schema.Schema, d *schema.ResourceData) bool {
1002+
if pathString == "path" {
1003+
return false
1004+
}
1005+
return defaultShouldOmitFieldFunc(ic, pathString, as, d)
1006+
},
1007+
1008+
Depends: []reference{
1009+
{Path: "path", Resource: "databricks_user", Match: "repos", MatchType: MatchPrefix},
1010+
{Path: "path", Resource: "databricks_service_principal", Match: "repos", MatchType: MatchPrefix},
1011+
},
9891012
},
9901013
"databricks_workspace_conf": {
9911014
Service: "workspace",
@@ -1094,6 +1117,7 @@ var resourcesMap map[string]importable = map[string]importable{
10941117
return nil
10951118
},
10961119
Import: func(ic *importContext, r *resource) error {
1120+
ic.emitUserOrServicePrincipalForPath(r.ID, "/Users")
10971121
notebooksAPI := workspace.NewNotebooksAPI(ic.Context, ic.Client)
10981122
contentB64, err := notebooksAPI.Export(r.ID, "SOURCE")
10991123
if err != nil {
@@ -1118,6 +1142,8 @@ var resourcesMap map[string]importable = map[string]importable{
11181142
},
11191143
Depends: []reference{
11201144
{Path: "source", File: true},
1145+
{Path: "path", Resource: "databricks_user", Match: "home", MatchType: MatchPrefix},
1146+
{Path: "path", Resource: "databricks_service_principal", Match: "home", MatchType: MatchPrefix},
11211147
},
11221148
},
11231149
"databricks_sql_query": {
@@ -1391,10 +1417,7 @@ var resourcesMap map[string]importable = map[string]importable{
13911417
common.DataToStructPointer(r.Data, s, &pipeline)
13921418
for _, lib := range pipeline.Libraries {
13931419
if lib.Notebook != nil {
1394-
ic.Emit(&resource{
1395-
Resource: "databricks_notebook",
1396-
ID: lib.Notebook.Path,
1397-
})
1420+
ic.emitNotebookOrRepo(lib.Notebook.Path)
13981421
}
13991422
ic.emitIfDbfsFile(lib.Jar)
14001423
ic.emitIfDbfsFile(lib.Whl)
@@ -1452,6 +1475,7 @@ var resourcesMap map[string]importable = map[string]importable{
14521475
{Path: "cluster.instance_pool_id", Resource: "databricks_instance_pool"},
14531476
{Path: "cluster.driver_instance_pool_id", Resource: "databricks_instance_pool"},
14541477
{Path: "library.notebook.path", Resource: "databricks_notebook"},
1478+
{Path: "library.notebook.path", Resource: "databricks_repo", Match: "path", MatchType: MatchPrefix},
14551479
{Path: "library.jar", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
14561480
{Path: "library.whl", Resource: "databricks_dbfs_file", Match: "dbfs_path"},
14571481
},

exporter/model.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,36 @@ type importable struct {
5656
ApiVersion common.ApiVersion
5757
}
5858

59+
type MatchType string
60+
61+
const (
62+
// MatchExact is to specify that whole value should match
63+
MatchExact = "exact"
64+
// MatchPrefix is to specify that prefix of value should match
65+
MatchPrefix = "prefix"
66+
)
67+
5968
type reference struct {
60-
Path string
61-
Resource string
62-
Match string
63-
Variable bool
64-
File bool
69+
Path string
70+
Resource string
71+
Match string
72+
MatchType MatchType // type of match, `prefix` - reference is embedded into string, `` (or `exact`) - full match
73+
Variable bool
74+
File bool
75+
}
76+
77+
func (r reference) MatchAttribute() string {
78+
if r.Match != "" {
79+
return r.Match
80+
}
81+
return "id"
82+
}
83+
84+
func (r reference) MatchTypeValue() MatchType {
85+
if r.MatchType == "" {
86+
return MatchExact
87+
}
88+
return r.MatchType
6589
}
6690

6791
type resource struct {

exporter/test-data/get-dlt-pipeline.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@
4040
"notebook": {
4141
"path": "/Users/[email protected]/Test DLT"
4242
}
43+
},
44+
{
45+
"notebook": {
46+
"path": "/Repos/[email protected]/repo/Test DLT"
47+
}
4348
}
4449
],
4550
"name": "Test DLT",

0 commit comments

Comments
 (0)