Skip to content

Commit 1075e39

Browse files
authored
[Exporter] Added support for Databricks Apps resources (#5208)
## Changes <!-- Summary of your changes that are easy to understand --> This change adds exporter support for `databricks_app` and `databricks_apps_settings_custom_template` resources. Changes: * Add exporter support for `databricks_app` resource` * Add exporter support for `databricks_apps_settings_custom_template` * Fix Plugin Framework list processing in `codegen.go`: - Include numeric indices in nested list paths (e.g., `resources.0.secret.key`) for consistent reference resolution across SDKv2 and Plugin Framework resources - Ensures `IsValidApproximation` functions receive correct paths with indices * Refactor and improvements: - Export AppResource type in resource_app.go for exporter use - Do group caching only if `groups` service is enabled ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [x] `make test` run locally - [x] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] using Go SDK - [ ] using TF Plugin Framework - [x] has entry in `NEXT_CHANGELOG.md` file
1 parent 8d2aaec commit 1075e39

File tree

11 files changed

+459
-13
lines changed

11 files changed

+459
-13
lines changed

NEXT_CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
* Added support for `databricks_data_quality_monitor` resource ([#5193](https://github.com/databricks/terraform-provider-databricks/pull/5193)).
2121
* Fix typo in the name of environment variable ([#5158](https://github.com/databricks/terraform-provider-databricks/pull/5158)).
2222
* Export permission assignments on workspace level ([#5169](https://github.com/databricks/terraform-provider-databricks/pull/5169)).
23+
* Added support for Databricks Apps resources ([#5208](https://github.com/databricks/terraform-provider-databricks/pull/5208)).
2324

2425
### Internal Changes

docs/guides/experimental-exporter.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ Services could be specified in combination with predefined aliases (`all` - for
174174

175175
* `access` - **listing** [databricks_permissions](../resources/permissions.md), [databricks_instance_profile](../resources/instance_profile.md), [databricks_ip_access_list](../resources/ip_access_list.md), and [databricks_access_control_rule_set](../resources/access_control_rule_set.md). *Please note that for `databricks_permissions` we list only `authorization = "tokens"`, the permissions for other objects (notebooks, ...) will be emitted when corresponding objects are processed!*
176176
* `alerts` - **listing** [databricks_alert](../resources/alert.md) and [databricks_alert_v2](../resources/alert_v2.md).
177+
* `apps` - **listing** [databricks_app](../resources/app.md) and [databricks_apps_settings_custom_template](../resources/apps_settings_custom_template.md).
177178
* `billing` - **listing** [databricks_budget](../resources/budget.md).
178179
* `compute` - **listing** [databricks_cluster](../resources/cluster.md).
179180
* `dashboards` - **listing** [databricks_dashboard](../resources/dashboard.md).
@@ -241,6 +242,8 @@ Exporter aims to generate HCL code for most of the resources within the Databric
241242
| [databricks_access_control_rule_set](../resources/access_control_rule_set.md) | Yes | No | No | Yes |
242243
| [databricks_alert](../resources/alert.md) | Yes | Yes | Yes | No |
243244
| [databricks_alert_v2](../resources/alert_v2.md) | Yes | Yes | Yes | No |
245+
| [databricks_app](../resources/app.md) | Yes | No | Yes | No |
246+
| [databricks_apps_settings_custom_template](../resources/apps_settings_custom_template.md) | Yes | No | Yes | No |
244247
| [databricks_artifact_allowlist](../resources/artifact_allowlist.md) | Yes | No | Yes | No |
245248
| [databricks_budget](../resources/budget.md) | Yes | Yes | No | Yes |
246249
| [databricks_catalog](../resources/catalog.md) | Yes | Yes | Yes | No |

exporter/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ When adding a new resource to Terraform Exporter we need to perform next steps:
3333
1. Define a new `importable` instance in the `importables.go`.
3434
2. Specify if it's account-level or workspace-level resource, or both.
3535
3. Specify a service to which resource belongs to. Either use one of the existing, if it fits, or define a new one (ask user for confirmation).
36-
4. Implement the `List` function that will be discover and emit instances of the specific resource.
36+
4. Implement the `List` function that will be discover and emit instances of the specific resource. When implementing it, prefer to use `List` method of Go SDK instead of `ListAll`.
3737
5. (Optional) Implement the `Name` function that will extract TF resource name from an instance of a specific resource.
3838
6. (Recommended) Implement the `Import` function that is responsible for emitting of dependencies for this resource - permissions/grants, etc.
3939
7. (Optional) Implement the `ShouldOmitField` if some fields should be conditionally omitted.

exporter/codegen.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -762,7 +762,9 @@ func (ic *importContext) pluginFrameworkFieldToHcl(imp importable, path []string
762762
}
763763

764764
if nestedData, ok := item.(map[string]interface{}); ok {
765-
objTokens := ic.pluginFrameworkNestedObjectToTokens(imp, append(path, fieldName), nestedSchema, nestedData, res)
765+
// Include the index in the path for proper reference resolution
766+
nestedPath := append(path, fieldName, strconv.Itoa(i))
767+
objTokens := ic.pluginFrameworkNestedObjectToTokens(imp, nestedPath, nestedSchema, nestedData, res)
766768
listTokens = append(listTokens, objTokens...)
767769
}
768770
}

exporter/exporter_test.go

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import (
1313
"time"
1414

1515
"github.com/databricks/databricks-sdk-go/apierr"
16+
"github.com/databricks/databricks-sdk-go/service/apps"
1617
sdk_uc "github.com/databricks/databricks-sdk-go/service/catalog"
1718
sdk_compute "github.com/databricks/databricks-sdk-go/service/compute"
1819
sdk_dashboards "github.com/databricks/databricks-sdk-go/service/dashboards"
@@ -33,6 +34,7 @@ import (
3334
"github.com/databricks/terraform-provider-databricks/commands"
3435
"github.com/databricks/terraform-provider-databricks/common"
3536
"github.com/databricks/terraform-provider-databricks/internal/service/workspace_tf"
37+
"github.com/databricks/terraform-provider-databricks/permissions/entity"
3638
"github.com/databricks/terraform-provider-databricks/qa"
3739
"github.com/databricks/terraform-provider-databricks/repos"
3840
"github.com/databricks/terraform-provider-databricks/scim"
@@ -345,6 +347,24 @@ var emptyGitCredentials = qa.HTTPFixture{
345347
},
346348
}
347349

350+
var emptyAppsSettingsCustomTemplates = qa.HTTPFixture{
351+
Method: "GET",
352+
Resource: "/api/2.0/apps-settings/templates?",
353+
Response: apps.ListCustomTemplatesResponse{
354+
Templates: []apps.CustomTemplate{},
355+
},
356+
ReuseRequest: true,
357+
}
358+
359+
var emptyApps = qa.HTTPFixture{
360+
Method: "GET",
361+
Resource: "/api/2.0/apps?",
362+
Response: apps.ListAppsResponse{
363+
Apps: []apps.App{},
364+
},
365+
ReuseRequest: true,
366+
}
367+
348368
var emptyModelServing = qa.HTTPFixture{
349369
Method: "GET",
350370
Resource: "/api/2.0/serving-endpoints",
@@ -527,6 +547,8 @@ func TestImportingUsersGroupsSecretScopes(t *testing.T) {
527547
[]qa.HTTPFixture{
528548
emptyDestinationNotficationsList,
529549
noCurrentMetastoreAttached,
550+
emptyApps,
551+
emptyAppsSettingsCustomTemplates,
530552
emptyLakeviewList,
531553
emptyMetastoreList,
532554
meAdminFixture,
@@ -798,6 +820,8 @@ func TestImportingNoResourcesError(t *testing.T) {
798820
Groups: []scim.ComplexValue{},
799821
},
800822
},
823+
emptyApps,
824+
emptyAppsSettingsCustomTemplates,
801825
emptyDataQualityMonitors,
802826
emptyUsersList,
803827
emptySpnsList,
@@ -3386,3 +3410,216 @@ func TestDataQualityMonitorsExport(t *testing.T) {
33863410
}`))
33873411
})
33883412
}
3413+
3414+
func TestAppExport(t *testing.T) {
3415+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
3416+
meAdminFixture,
3417+
noCurrentMetastoreAttached,
3418+
emptyAppsSettingsCustomTemplates,
3419+
{
3420+
Method: "GET",
3421+
Resource: "/api/2.0/apps?",
3422+
Response: apps.ListAppsResponse{
3423+
Apps: []apps.App{
3424+
{
3425+
Name: "test-app",
3426+
Description: "Test app",
3427+
Resources: []apps.AppResource{
3428+
{
3429+
Name: "sql-warehouse",
3430+
SqlWarehouse: &apps.AppResourceSqlWarehouse{
3431+
Id: "warehouse-123",
3432+
Permission: "CAN_MANAGE",
3433+
},
3434+
},
3435+
{
3436+
Name: "serving-endpoint",
3437+
ServingEndpoint: &apps.AppResourceServingEndpoint{
3438+
Name: "endpoint-abc",
3439+
Permission: "CAN_QUERY",
3440+
},
3441+
},
3442+
{
3443+
Name: "job",
3444+
Job: &apps.AppResourceJob{
3445+
Id: "job-456",
3446+
Permission: "CAN_VIEW",
3447+
},
3448+
},
3449+
{
3450+
Name: "secret",
3451+
Secret: &apps.AppResourceSecret{
3452+
Scope: "my-scope",
3453+
Key: "my-key",
3454+
Permission: "READ",
3455+
},
3456+
},
3457+
{
3458+
Name: "uc-volume",
3459+
UcSecurable: &apps.AppResourceUcSecurable{
3460+
SecurableType: "VOLUME",
3461+
SecurableFullName: "catalog.schema.my_volume",
3462+
Permission: "READ_VOLUME",
3463+
},
3464+
},
3465+
},
3466+
BudgetPolicyId: "budget-789",
3467+
},
3468+
},
3469+
},
3470+
},
3471+
{
3472+
Method: "GET",
3473+
Resource: "/api/2.0/apps/test-app?",
3474+
Response: apps.App{
3475+
Name: "test-app",
3476+
Description: "Test app",
3477+
Resources: []apps.AppResource{
3478+
{
3479+
Name: "sql-warehouse",
3480+
SqlWarehouse: &apps.AppResourceSqlWarehouse{
3481+
Id: "warehouse-123",
3482+
Permission: "CAN_MANAGE",
3483+
},
3484+
},
3485+
{
3486+
Name: "serving-endpoint",
3487+
ServingEndpoint: &apps.AppResourceServingEndpoint{
3488+
Name: "endpoint-abc",
3489+
Permission: "CAN_QUERY",
3490+
},
3491+
},
3492+
{
3493+
Name: "job",
3494+
Job: &apps.AppResourceJob{
3495+
Id: "job-456",
3496+
Permission: "CAN_VIEW",
3497+
},
3498+
},
3499+
{
3500+
Name: "secret",
3501+
Secret: &apps.AppResourceSecret{
3502+
Scope: "my-scope",
3503+
Key: "my-key",
3504+
Permission: "READ",
3505+
},
3506+
},
3507+
{
3508+
Name: "uc-volume",
3509+
UcSecurable: &apps.AppResourceUcSecurable{
3510+
SecurableType: "VOLUME",
3511+
SecurableFullName: "catalog.schema.my_volume",
3512+
Permission: "READ_VOLUME",
3513+
},
3514+
},
3515+
},
3516+
BudgetPolicyId: "budget-789",
3517+
},
3518+
},
3519+
{
3520+
Method: "GET",
3521+
Resource: "/api/2.0/permissions/apps/test-app",
3522+
Response: entity.PermissionsEntity{
3523+
ObjectType: "apps",
3524+
AccessControlList: []iam.AccessControlRequest{},
3525+
},
3526+
},
3527+
}, func(ctx context.Context, client *common.DatabricksClient) {
3528+
tmpDir := fmt.Sprintf("/tmp/tf-%s", qa.RandomName())
3529+
defer os.RemoveAll(tmpDir)
3530+
3531+
ic := newImportContext(client)
3532+
ic.enableServices("apps")
3533+
ic.enableListing("apps")
3534+
ic.Directory = tmpDir
3535+
ic.noFormat = true
3536+
3537+
err := ic.Run()
3538+
assert.NoError(t, err)
3539+
3540+
// Verify that the app and its dependencies were generated in the Terraform code
3541+
content, err := os.ReadFile(tmpDir + "/apps.tf")
3542+
assert.NoError(t, err)
3543+
contentStr := normalizeWhitespace(string(content))
3544+
3545+
// Check that the app resource is generated
3546+
assert.Contains(t, contentStr, `resource "databricks_app" "test_app"`)
3547+
assert.Contains(t, contentStr, `name = "test-app"`)
3548+
assert.Contains(t, contentStr, `description = "Test app"`)
3549+
})
3550+
}
3551+
3552+
func TestAppsSettingsCustomTemplateExport(t *testing.T) {
3553+
qa.HTTPFixturesApply(t, []qa.HTTPFixture{
3554+
meAdminFixture,
3555+
noCurrentMetastoreAttached,
3556+
{
3557+
Method: "GET",
3558+
Resource: "/api/2.0/apps?",
3559+
Response: apps.ListAppsResponse{
3560+
Apps: []apps.App{},
3561+
},
3562+
},
3563+
{
3564+
Method: "GET",
3565+
Resource: "/api/2.0/apps-settings/templates?",
3566+
Response: apps.ListCustomTemplatesResponse{
3567+
Templates: []apps.CustomTemplate{
3568+
{
3569+
Name: "my-custom-template",
3570+
Description: "Test template",
3571+
GitRepo: "https://github.com/example/repo.git",
3572+
GitProvider: "github",
3573+
Path: "templates/app",
3574+
Creator: "[email protected]",
3575+
},
3576+
},
3577+
},
3578+
},
3579+
{
3580+
Method: "GET",
3581+
Resource: "/api/2.0/apps-settings/templates/my-custom-template?",
3582+
Response: apps.CustomTemplate{
3583+
Name: "my-custom-template",
3584+
Description: "Test template",
3585+
GitRepo: "https://github.com/example/repo.git",
3586+
GitProvider: "github",
3587+
Path: "templates/app",
3588+
Creator: "[email protected]",
3589+
},
3590+
},
3591+
{
3592+
Method: "GET",
3593+
Resource: "/api/2.0/permissions/apps/templates/my-custom-template",
3594+
Response: entity.PermissionsEntity{
3595+
ObjectType: "apps/templates",
3596+
AccessControlList: []iam.AccessControlRequest{},
3597+
},
3598+
},
3599+
}, func(ctx context.Context, client *common.DatabricksClient) {
3600+
tmpDir := fmt.Sprintf("/tmp/tf-%s", qa.RandomName())
3601+
defer os.RemoveAll(tmpDir)
3602+
3603+
ic := newImportContext(client)
3604+
ic.enableServices("apps")
3605+
ic.enableListing("apps")
3606+
ic.Directory = tmpDir
3607+
ic.noFormat = true
3608+
3609+
err := ic.Run()
3610+
assert.NoError(t, err)
3611+
3612+
// Verify that the custom template was generated in the Terraform code
3613+
content, err := os.ReadFile(tmpDir + "/apps.tf")
3614+
assert.NoError(t, err)
3615+
contentStr := normalizeWhitespace(string(content))
3616+
3617+
// Check that the custom template resource is generated
3618+
assert.Contains(t, contentStr, `resource "databricks_apps_settings_custom_template" "my_custom_template"`)
3619+
assert.Contains(t, contentStr, `name = "my-custom-template"`)
3620+
assert.Contains(t, contentStr, `description = "Test template"`)
3621+
assert.Contains(t, contentStr, `git_repo = "https://github.com/example/repo.git"`)
3622+
assert.Contains(t, contentStr, `git_provider = "github"`)
3623+
assert.Contains(t, contentStr, `path = "templates/app"`)
3624+
})
3625+
}

0 commit comments

Comments
 (0)