Skip to content

Commit 1345e03

Browse files
authored
Added interactive interface to resource exporter (#1010)
1 parent d45e827 commit 1345e03

File tree

14 files changed

+477
-316
lines changed

14 files changed

+477
-316
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
* Added new `gcp_attributes` to `databricks_cluster` and `databricks_instance_pool` ([#1126](https://github.com/databrickslabs/terraform-provider-databricks/pull/1126)).
99
* Added exporter functionality for `databricks_ip_access_list` and `databricks_workspace_conf` ([#1125](https://github.com/databrickslabs/terraform-provider-databricks/pull/1125)).
1010
* Added `graviton` selector for `databricks_node_type` and `databricks_spark_version` data sources ([#1127](https://github.com/databrickslabs/terraform-provider-databricks/pull/1127)).
11+
* Added interactive mode to resource exporter ([#1010](https://github.com/databrickslabs/terraform-provider-databricks/pull/1010)).
1112

1213
## 0.4.9
1314

common/client.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -358,7 +358,7 @@ func (c *DatabricksClient) configureWithDatabricksCfg(ctx context.Context) (func
358358
_, err = os.Stat(configFile)
359359
if os.IsNotExist(err) {
360360
// early return for non-configured machines
361-
log.Printf("[INFO] %s not found on current host", configFile)
361+
log.Printf("[DEBUG] %s not found on current host", configFile)
362362
return nil, nil
363363
}
364364
cfg, err := ini.Load(configFile)

docs/guides/experimental-exporter.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@ Generates `*.tf` files for Databricks resources as well as `import.sh` to run im
1111

1212
## Example Usage
1313

14-
After downloading the [latest released binary](https://github.com/databrickslabs/terraform-provider-databricks/releases), unpack it and place it in the same folder. In fact, you may have already downloaded this binary - check `.terraform` folder of any state directory, where you've used `databricks` provider. It could also be in your plugin cache `~/.terraform.d/plugins/registry.terraform.io/databrickslabs/databricks/*/*/terraform-provider-databricks`.
14+
After downloading the [latest released binary](https://github.com/databrickslabs/terraform-provider-databricks/releases), unpack it and place it in the same folder. In fact, you may have already downloaded this binary - check `.terraform` folder of any state directory, where you've used `databricks` provider. It could also be in your plugin cache `~/.terraform.d/plugins/registry.terraform.io/databrickslabs/databricks/*/*/terraform-provider-databricks`. Here's the tool in action:
15+
16+
[![asciicast](https://asciinema.org/a/Rv8ZFJQpfrfp6ggWddjtyXaOy.svg)](https://asciinema.org/a/Rv8ZFJQpfrfp6ggWddjtyXaOy)
17+
18+
Exporter can also be used in a non-interactive mode:
1519

1620
```bash
1721
export DATABRICKS_HOST=...

exporter/command.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package exporter
22

33
import (
44
"flag"
5+
"fmt"
56
"log"
67
"os"
78
"strings"
@@ -43,6 +44,31 @@ func (ic *importContext) allServicesAndListing() (string, string) {
4344
return services, listing
4445
}
4546

47+
func (ic *importContext) interactivePrompts() {
48+
for ic.Client.Authenticate(ic.Context) != nil {
49+
ic.Client.Host = askFor("🔑 Databricks Workspace URL:")
50+
ic.Client.Token = askFor("🔑 Databricks Workspace PAT:")
51+
}
52+
ic.match = askFor("🔍 Match entity names (optional):")
53+
listing := ""
54+
for r, ir := range ic.Importables {
55+
if ir.List == nil {
56+
continue
57+
}
58+
if !askFlag(fmt.Sprintf("✅ Generate `%s` and related resources?", r)) {
59+
continue
60+
}
61+
if len(listing) > 0 {
62+
listing += ","
63+
}
64+
listing += ir.Service
65+
if ir.Service == "mounts" {
66+
ic.mounts = true
67+
}
68+
}
69+
ic.listing = listing
70+
}
71+
4672
// Run import according to flags
4773
func Run(args ...string) error {
4874
log.SetOutput(&logLevel)
@@ -59,14 +85,16 @@ func Run(args ...string) error {
5985
if err != nil {
6086
return err
6187
}
88+
var skipInteractive bool
89+
flags.BoolVar(&skipInteractive, "skip-interactive", false, "Skip interactive mode")
6290
flags.StringVar(&ic.Directory, "directory", cwd,
6391
"Directory to generate sources in. Defaults to current directory.")
6492
flags.Int64Var(&ic.lastActiveDays, "last-active-days", 3650,
6593
"Items with older than activity specified won't be imported.")
6694
flags.BoolVar(&ic.debug, "debug", false, "Print extra debug information.")
6795
flags.BoolVar(&ic.mounts, "mounts", false, "List DBFS mount points.")
6896
flags.BoolVar(&ic.generateDeclaration, "generateProviderDeclaration", true,
69-
"Generate Databricks provider declaration (for Terraform >= 0.13).")
97+
"Generate Databricks provider declaration.")
7098
services, listing := ic.allServicesAndListing()
7199
flags.StringVar(&ic.services, "services", services,
72100
"Comma-separated list of services to import. By default all services are imported.")
@@ -87,6 +115,9 @@ func Run(args ...string) error {
87115
if err != nil {
88116
return err
89117
}
118+
if !skipInteractive {
119+
ic.interactivePrompts()
120+
}
90121
if len(prefix) > 0 {
91122
ic.prefix = prefix + "_"
92123
}

exporter/command_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
package exporter
2+
3+
import (
4+
"bytes"
5+
"context"
6+
"testing"
7+
8+
"github.com/databrickslabs/terraform-provider-databricks/common"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
type dummyReader string
13+
14+
func (d dummyReader) Read(p []byte) (int, error) {
15+
n := copy(p, []byte(d))
16+
return n, nil
17+
}
18+
19+
func TestInteractivePrompts(t *testing.T) {
20+
cliInput = dummyReader("y\n")
21+
cliOutput = &bytes.Buffer{}
22+
ic := &importContext{
23+
Client: &common.DatabricksClient{},
24+
Context: context.Background(),
25+
Importables: map[string]importable{
26+
"x": {
27+
Service: "a",
28+
List: func(_ *importContext) error {
29+
return nil
30+
},
31+
},
32+
"y": {
33+
Service: "mounts",
34+
List: func(_ *importContext) error {
35+
return nil
36+
},
37+
},
38+
},
39+
}
40+
ic.interactivePrompts()
41+
assert.Equal(t, "a,mounts", ic.listing)
42+
assert.Equal(t, "y", ic.match)
43+
assert.True(t, ic.mounts)
44+
}

exporter/context.go

Lines changed: 71 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,16 @@ type mount struct {
8282
ClusterID string
8383
}
8484

85+
var nameFixes = []regexFix{
86+
{regexp.MustCompile(`[0-9a-f]{8}[_-][0-9a-f]{4}[_-][0-9a-f]{4}` +
87+
`[_-][0-9a-f]{4}[_-][0-9a-f]{12}[_-]`), ""},
88+
{regexp.MustCompile(`[_-][0-9]+[\._-][0-9]+[\._-].*\.([a-z0-9]{1,4})`), "_$1"},
89+
{regexp.MustCompile(`@.*$`), ""},
90+
{regexp.MustCompile(`[-\s\.\|]`), "_"},
91+
{regexp.MustCompile(`\W+`), ""},
92+
{regexp.MustCompile(`[_]{2,}`), "_"},
93+
}
94+
8595
func newImportContext(c *common.DatabricksClient) *importContext {
8696
p := provider.DatabricksProvider()
8797
p.TerraformVersion = "exporter"
@@ -103,16 +113,8 @@ func newImportContext(c *common.DatabricksClient) *importContext {
103113
Files: map[string]*hclwrite.File{},
104114
Scope: []*resource{},
105115
importing: map[string]bool{},
106-
nameFixes: []regexFix{
107-
{regexp.MustCompile(`[0-9a-f]{8}[_-][0-9a-f]{4}[_-][0-9a-f]{4}` +
108-
`[_-][0-9a-f]{4}[_-][0-9a-f]{12}[_-]`), ""},
109-
{regexp.MustCompile(`[_-][0-9]+[\._-][0-9]+[\._-].*\.([a-z0-9]{1,4})`), "_$1"},
110-
{regexp.MustCompile(`@.*$`), ""},
111-
{regexp.MustCompile(`[-\s\.\|]`), "_"},
112-
{regexp.MustCompile(`\W+`), ""},
113-
{regexp.MustCompile(`[_]{2,}`), "_"},
114-
},
115-
hclFixes: []regexFix{ // Be careful with that! it may break working code
116+
nameFixes: nameFixes,
117+
hclFixes: []regexFix{ // Be careful with that! it may break working code
116118
},
117119
allUsers: []scim.User{},
118120
variables: map[string]string{},
@@ -191,42 +193,7 @@ func (ic *importContext) Run() error {
191193
`)
192194
dcfile.Close()
193195
}
194-
195-
sort.Sort(ic.Scope)
196-
scopeSize := len(ic.Scope)
197-
log.Printf("[INFO] Generating configuration for %d resources", scopeSize)
198-
for i, r := range ic.Scope {
199-
ir := ic.Importables[r.Resource]
200-
f, ok := ic.Files[ir.Service]
201-
if !ok {
202-
f = hclwrite.NewEmptyFile()
203-
ic.Files[ir.Service] = f
204-
}
205-
if ir.Ignore != nil && ir.Ignore(ic, r) {
206-
continue
207-
}
208-
body := f.Body()
209-
if ir.Body != nil {
210-
err := ir.Body(ic, body, r)
211-
if err != nil {
212-
log.Printf("[ERROR] %s", err.Error())
213-
}
214-
} else {
215-
resourceBlock := body.AppendNewBlock("resource", []string{r.Resource, r.Name})
216-
err := ic.dataToHcl(ir, []string{}, ic.Resources[r.Resource],
217-
r.Data, resourceBlock.Body())
218-
if err != nil {
219-
log.Printf("[ERROR] %s", err.Error())
220-
}
221-
}
222-
if i%50 == 0 {
223-
log.Printf("[INFO] Generated %d of %d resources", i, scopeSize)
224-
}
225-
if r.Mode != "data" && ic.Resources[r.Resource].Importer != nil {
226-
// nolint
227-
sh.WriteString(r.ImportCommand(ic) + "\n")
228-
}
229-
}
196+
ic.generateHclForResources(sh)
230197
for service, f := range ic.Files {
231198
formatted := hclwrite.Format(f.Bytes())
232199
// fix some formatting in a hacky way instead of writing 100 lines
@@ -268,6 +235,44 @@ func (ic *importContext) Run() error {
268235
return nil
269236
}
270237

238+
func (ic *importContext) generateHclForResources(sh *os.File) {
239+
sort.Sort(ic.Scope)
240+
scopeSize := len(ic.Scope)
241+
log.Printf("[INFO] Generating configuration for %d resources", scopeSize)
242+
for i, r := range ic.Scope {
243+
ir := ic.Importables[r.Resource]
244+
f, ok := ic.Files[ir.Service]
245+
if !ok {
246+
f = hclwrite.NewEmptyFile()
247+
ic.Files[ir.Service] = f
248+
}
249+
if ir.Ignore != nil && ir.Ignore(ic, r) {
250+
continue
251+
}
252+
body := f.Body()
253+
if ir.Body != nil {
254+
err := ir.Body(ic, body, r)
255+
if err != nil {
256+
log.Printf("[ERROR] %s", err.Error())
257+
}
258+
} else {
259+
resourceBlock := body.AppendNewBlock("resource", []string{r.Resource, r.Name})
260+
err := ic.dataToHcl(ir, []string{}, ic.Resources[r.Resource],
261+
r.Data, resourceBlock.Body())
262+
if err != nil {
263+
log.Printf("[ERROR] %s", err.Error())
264+
}
265+
}
266+
if i%50 == 0 {
267+
log.Printf("[INFO] Generated %d of %d resources", i+1, scopeSize)
268+
}
269+
if r.Mode != "data" && ic.Resources[r.Resource].Importer != nil && sh != nil {
270+
// nolint
271+
sh.WriteString(r.ImportCommand(ic) + "\n")
272+
}
273+
}
274+
}
275+
271276
func (ic *importContext) MatchesName(n string) bool {
272277
if ic.match == "" {
273278
return true
@@ -465,6 +470,17 @@ func (ic *importContext) reference(i importable, path []string, value string) hc
465470
if d.Path != match {
466471
continue
467472
}
473+
if d.File {
474+
relativeFile := fmt.Sprintf("${path.module}/%s", value)
475+
return hclwrite.Tokens{
476+
&hclwrite.Token{Type: hclsyntax.TokenOQuote, Bytes: []byte{'"'}},
477+
&hclwrite.Token{Type: hclsyntax.TokenQuotedLit, Bytes: []byte(relativeFile)},
478+
&hclwrite.Token{Type: hclsyntax.TokenCQuote, Bytes: []byte{'"'}},
479+
}
480+
}
481+
if d.Variable {
482+
return ic.variable(fmt.Sprintf("%s_%s", path[0], value), "")
483+
}
468484
attr := "id"
469485
if d.Match != "" {
470486
attr = d.Match
@@ -512,7 +528,15 @@ func (ic *importContext) dataToHcl(i importable, path []string,
512528
if as.Computed {
513529
continue
514530
}
515-
raw, ok := d.GetOk(strings.Join(append(path, a), "."))
531+
pathString := strings.Join(append(path, a), ".")
532+
raw, ok := d.GetOk(pathString)
533+
for _, r := range i.Depends {
534+
if r.Path == pathString && r.Variable {
535+
// sensitive fields are moved to variable depends
536+
raw = i.Name(d)
537+
ok = true
538+
}
539+
}
516540
if !ok {
517541
continue
518542
}

0 commit comments

Comments
 (0)