diff --git a/bundle/config/mutator/translate_paths_dashboards.go b/bundle/config/mutator/translate_paths_dashboards.go index 341156163..2ead527c7 100644 --- a/bundle/config/mutator/translate_paths_dashboards.go +++ b/bundle/config/mutator/translate_paths_dashboards.go @@ -22,7 +22,7 @@ func (t *translateContext) dashboardRewritePatterns() []dashboardRewritePattern // Compile list of configuration paths to rewrite. return []dashboardRewritePattern{ { - base.Append(dyn.Key("definition_path")), + base.Append(dyn.Key("file_path")), t.retainLocalAbsoluteFilePath, }, } diff --git a/bundle/config/resources/dashboard.go b/bundle/config/resources/dashboard.go index 20acc9b07..93474e7a7 100644 --- a/bundle/config/resources/dashboard.go +++ b/bundle/config/resources/dashboard.go @@ -47,7 +47,6 @@ type Dashboard struct { // =========================== // FilePath points to the local `.lvdash.json` file containing the dashboard definition. - // If specified, it will populate the `SerializedDashboard` field. FilePath string `json:"file_path,omitempty"` } diff --git a/bundle/deploy/terraform/tfdyn/convert_dashboard.go b/bundle/deploy/terraform/tfdyn/convert_dashboard.go index b173c14ed..13be530b8 100644 --- a/bundle/deploy/terraform/tfdyn/convert_dashboard.go +++ b/bundle/deploy/terraform/tfdyn/convert_dashboard.go @@ -19,20 +19,48 @@ func convertDashboardResource(ctx context.Context, vin dyn.Value) (dyn.Value, er log.Debugf(ctx, "dashboard normalization diagnostic: %s", diag.Summary) } - // Include "serialized_dashboard" field if "definition_path" is set. - if path, ok := vin.Get("definition_path").AsString(); ok { + // Include "serialized_dashboard" field if "file_path" is set. + // Note: the Terraform resource supports "file_path" natively, but its + // change detection mechanism doesn't work as expected at the time of writing (Sep 30). + if path, ok := vout.Get("file_path").AsString(); ok { vout, err = dyn.Set(vout, "serialized_dashboard", dyn.V(fmt.Sprintf("${file(\"%s\")}", path))) if err != nil { return dyn.InvalidValue, fmt.Errorf("failed to set serialized_dashboard: %w", err) } + // Drop the "file_path" field. It is mutually exclusive with "serialized_dashboard". + vout, err = dyn.Walk(vout, func(p dyn.Path, v dyn.Value) (dyn.Value, error) { + switch len(p) { + case 0: + return v, nil + case 1: + if p[0] == dyn.Key("file_path") { + return v, dyn.ErrDrop + } + } + + // Skip everything else. + return v, dyn.ErrSkip + }) + if err != nil { + return dyn.InvalidValue, fmt.Errorf("failed to drop file_path: %w", err) + } + } + + // Default the "embed_credentials" field to "false", if not already set. + // This is different from the behavior in the Terraform provider, so we make it explicit. + if _, ok := vout.Get("embed_credentials").AsBool(); !ok { + vout, err = dyn.Set(vout, "embed_credentials", dyn.V(false)) + if err != nil { + return dyn.InvalidValue, fmt.Errorf("failed to set embed_credentials: %w", err) + } } return vout, nil } -type DashboardConverter struct{} +type dashboardConverter struct{} -func (DashboardConverter) Convert(ctx context.Context, key string, vin dyn.Value, out *schema.Resources) error { +func (dashboardConverter) Convert(ctx context.Context, key string, vin dyn.Value, out *schema.Resources) error { vout, err := convertDashboardResource(ctx, vin) if err != nil { return err @@ -51,5 +79,5 @@ func (DashboardConverter) Convert(ctx context.Context, key string, vin dyn.Value } func init() { - registerConverter("dashboards", DashboardConverter{}) + registerConverter("dashboards", dashboardConverter{}) } diff --git a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go index 2c84967ae..886be16f1 100644 --- a/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go +++ b/bundle/deploy/terraform/tfdyn/convert_dashboard_test.go @@ -1,7 +1,115 @@ package tfdyn -import "testing" +import ( + "context" + "fmt" + "testing" + + "github.com/databricks/cli/bundle/config/resources" + "github.com/databricks/cli/bundle/internal/tf/schema" + "github.com/databricks/cli/libs/dyn" + "github.com/databricks/cli/libs/dyn/convert" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) func TestConvertDashboard(t *testing.T) { + var src = resources.Dashboard{ + DisplayName: "my dashboard", + WarehouseID: "f00dcafe", + ParentPath: "/some/path", + EmbedCredentials: true, + Permissions: []resources.Permission{ + { + Level: "CAN_VIEW", + UserName: "jane@doe.com", + }, + }, + } + + vin, err := convert.FromTyped(src, dyn.NilValue) + require.NoError(t, err) + + ctx := context.Background() + out := schema.NewResources() + err = dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out) + require.NoError(t, err) + + // Assert equality on the job + assert.Equal(t, map[string]any{ + "display_name": "my dashboard", + "warehouse_id": "f00dcafe", + "parent_path": "/some/path", + "embed_credentials": true, + }, out.Dashboard["my_dashboard"]) + + // Assert equality on the permissions + assert.Equal(t, &schema.ResourcePermissions{ + DashboardId: "${databricks_dashboard.my_dashboard.id}", + AccessControl: []schema.ResourcePermissionsAccessControl{ + { + PermissionLevel: "CAN_VIEW", + UserName: "jane@doe.com", + }, + }, + }, out.Permissions["dashboard_my_dashboard"]) +} + +func TestConvertDashboardFilePath(t *testing.T) { + var src = resources.Dashboard{ + FilePath: "some/path", + } + + vin, err := convert.FromTyped(src, dyn.NilValue) + require.NoError(t, err) + + ctx := context.Background() + out := schema.NewResources() + err = dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out) + require.NoError(t, err) + + // Assert that the "serialized_dashboard" is included. + assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{ + "serialized_dashboard": "${file(\"some/path\")}", + }) + + // Assert that the "file_path" doesn't carry over. + assert.NotSubset(t, out.Dashboard["my_dashboard"], map[string]any{ + "file_path": "some/path", + }) +} + +func TestConvertDashboardEmbedCredentialsPassthrough(t *testing.T) { + for _, v := range []bool{true, false} { + t.Run(fmt.Sprintf("set to %v", v), func(t *testing.T) { + vin := dyn.V(map[string]dyn.Value{ + "embed_credentials": dyn.V(v), + }) + + ctx := context.Background() + out := schema.NewResources() + err := dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out) + require.NoError(t, err) + + // Assert that the "embed_credentials" is set as configured. + assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{ + "embed_credentials": v, + }) + }) + } +} + +func TestConvertDashboardEmbedCredentialsDefault(t *testing.T) { + vin := dyn.V(map[string]dyn.Value{}) + + ctx := context.Background() + out := schema.NewResources() + err := dashboardConverter{}.Convert(ctx, "my_dashboard", vin, out) + require.NoError(t, err) + + // Assert that the "embed_credentials" is set to false (by default). + assert.Subset(t, out.Dashboard["my_dashboard"], map[string]any{ + "embed_credentials": false, + }) }