Skip to content

Commit

Permalink
[Feature] Added resource databricks_custom_app_integration (#4124)
Browse files Browse the repository at this point in the history
## Changes
Add resource `databricks_custom_app_integration` for OAuth custom app
integration

## 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
- [x] covered with integration tests in `internal/acceptance`
- [x] relevant acceptance tests are passing
- [x] using Go SDK
  • Loading branch information
nkvuong authored Oct 20, 2024
1 parent 4bebb0d commit 6088aa8
Show file tree
Hide file tree
Showing 5 changed files with 349 additions and 0 deletions.
80 changes: 80 additions & 0 deletions apps/resource_custom_app_integration.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
package apps

import (
"context"

"github.com/databricks/databricks-sdk-go/service/oauth2"
"github.com/databricks/terraform-provider-databricks/common"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
)

type CustomAppIntegration struct {
oauth2.GetCustomAppIntegrationOutput
// OAuth client-secret generated by the Databricks. If this is a
// confidential OAuth app client-secret will be generated.
ClientSecret string `json:"client_secret,omitempty"`
}

func ResourceCustomAppIntegration() common.Resource {
s := common.StructToSchema(CustomAppIntegration{}, func(m map[string]*schema.Schema) map[string]*schema.Schema {
for _, p := range []string{"client_id", "create_time", "created_by", "creator_username", "integration_id"} {
common.CustomizeSchemaPath(m, p).SetComputed()
}
for _, p := range []string{"confidential", "name", "scopes"} {
common.CustomizeSchemaPath(m, p).SetForceNew()
}
common.CustomizeSchemaPath(m, "client_secret").SetSensitive().SetComputed()
common.CustomizeSchemaPath(m, "token_access_policy", "access_token_ttl_in_minutes").SetValidateFunc(validation.IntBetween(5, 1440))
common.CustomizeSchemaPath(m, "token_access_policy", "refresh_token_ttl_in_minutes").SetValidateFunc(validation.IntBetween(5, 129600))
return m
})
return common.Resource{
Create: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
var create oauth2.CreateCustomAppIntegration
common.DataToStructPointer(d, s, &create)
acc, err := c.AccountClient()
if err != nil {
return err
}
integration, err := acc.CustomAppIntegration.Create(ctx, create)
if err != nil {
return err
}
d.Set("integration_id", integration.IntegrationId)
d.Set("client_id", integration.ClientId)
d.Set("client_secret", integration.ClientSecret)
d.SetId(integration.IntegrationId)
return nil
},
Read: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
acc, err := c.AccountClient()
if err != nil {
return err
}
integration, err := acc.CustomAppIntegration.GetByIntegrationId(ctx, d.Id())
if err != nil {
return err
}
return common.StructToData(integration, s, d)
},
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
var update oauth2.UpdateCustomAppIntegration
update.IntegrationId = d.Id()
common.DataToStructPointer(d, s, &update)
acc, err := c.AccountClient()
if err != nil {
return err
}
return acc.CustomAppIntegration.Update(ctx, update)
},
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
acc, err := c.AccountClient()
if err != nil {
return err
}
return acc.CustomAppIntegration.DeleteByIntegrationId(ctx, d.Id())
},
Schema: s,
}
}
175 changes: 175 additions & 0 deletions apps/resource_custom_app_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package apps

import (
"testing"

"github.com/databricks/databricks-sdk-go/experimental/mocks"
"github.com/databricks/databricks-sdk-go/service/oauth2"
"github.com/stretchr/testify/mock"

"github.com/databricks/terraform-provider-databricks/qa"
)

func TestResourceCustomAppIntegrationCreate(t *testing.T) {
qa.ResourceFixture{
MockAccountClientFunc: func(a *mocks.MockAccountClient) {
api := a.GetMockCustomAppIntegrationAPI().EXPECT()
api.Create(mock.Anything, oauth2.CreateCustomAppIntegration{
Name: "custom_integration_name",
RedirectUrls: []string{
"https://example.com",
},
Scopes: []string{
"all",
},
TokenAccessPolicy: &oauth2.TokenAccessPolicy{
AccessTokenTtlInMinutes: 60,
RefreshTokenTtlInMinutes: 30,
},
}).Return(&oauth2.CreateCustomAppIntegrationOutput{
ClientId: "client_id",
ClientSecret: "client_secret",
IntegrationId: "integration_id",
}, nil)
api.GetByIntegrationId(mock.Anything, "integration_id").Return(
&oauth2.GetCustomAppIntegrationOutput{
Name: "custom_integration_name",
RedirectUrls: []string{
"https://example.com",
},
Scopes: []string{
"all",
},
TokenAccessPolicy: &oauth2.TokenAccessPolicy{
AccessTokenTtlInMinutes: 60,
RefreshTokenTtlInMinutes: 30,
},
ClientId: "client_id",
IntegrationId: "integration_id",
}, nil,
)
},
Create: true,
AccountID: "account_id",
HCL: `
name = "custom_integration_name"
redirect_urls = ["https://example.com"]
scopes = ["all"]
token_access_policy {
access_token_ttl_in_minutes = 60
refresh_token_ttl_in_minutes = 30
}`,
Resource: ResourceCustomAppIntegration(),
}.ApplyAndExpectData(t, map[string]any{
"name": "custom_integration_name",
"integration_id": "integration_id",
"client_id": "client_id",
"client_secret": "client_secret",
})
}

func TestResourceCustomAppIntegrationRead(t *testing.T) {
qa.ResourceFixture{
MockAccountClientFunc: func(a *mocks.MockAccountClient) {
a.GetMockCustomAppIntegrationAPI().EXPECT().GetByIntegrationId(mock.Anything, "integration_id").Return(
&oauth2.GetCustomAppIntegrationOutput{
Name: "custom_integration_name",
RedirectUrls: []string{
"https://example.com",
},
Scopes: []string{
"all",
},
TokenAccessPolicy: &oauth2.TokenAccessPolicy{
AccessTokenTtlInMinutes: 60,
RefreshTokenTtlInMinutes: 30,
},
ClientId: "client_id",
IntegrationId: "integration_id",
}, nil,
)
},
Resource: ResourceCustomAppIntegration(),
Read: true,
New: true,
AccountID: "account_id",
ID: "integration_id",
}.ApplyAndExpectData(t, map[string]any{
"name": "custom_integration_name",
"integration_id": "integration_id",
"client_id": "client_id",
})
}

func TestResourceCustomAppIntegrationUpdate(t *testing.T) {
qa.ResourceFixture{
MockAccountClientFunc: func(a *mocks.MockAccountClient) {
api := a.GetMockCustomAppIntegrationAPI().EXPECT()
api.Update(mock.Anything, oauth2.UpdateCustomAppIntegration{
IntegrationId: "integration_id",
RedirectUrls: []string{
"https://example.com",
},
TokenAccessPolicy: &oauth2.TokenAccessPolicy{
AccessTokenTtlInMinutes: 30,
RefreshTokenTtlInMinutes: 30,
},
}).Return(nil)
api.GetByIntegrationId(mock.Anything, "integration_id").Return(
&oauth2.GetCustomAppIntegrationOutput{
Name: "custom_integration_name",
RedirectUrls: []string{
"https://example.com",
},
Scopes: []string{
"all",
},
TokenAccessPolicy: &oauth2.TokenAccessPolicy{
AccessTokenTtlInMinutes: 30,
RefreshTokenTtlInMinutes: 30,
},
ClientId: "client_id",
IntegrationId: "integration_id",
}, nil,
)
},
Resource: ResourceCustomAppIntegration(),
Update: true,
HCL: `
name = "custom_integration_name"
redirect_urls = ["https://example.com"]
scopes = ["all"]
token_access_policy {
access_token_ttl_in_minutes = 30
refresh_token_ttl_in_minutes = 30
}`,
InstanceState: map[string]string{
"name": "custom_integration_name",
"integration_id": "integration_id",
"client_id": "client_id",
"scopes.#": "1",
"scopes.0": "all",
"redirect_urls.#": "1",
"redirect_urls.0": "https://example.com",
"token_access_policy.access_token_ttl_in_minutes": "30",
"token_access_policy.refresh_token_ttl_in_minutes": "30",
},
AccountID: "account_id",
ID: "integration_id",
}.ApplyAndExpectData(t, map[string]any{
"name": "custom_integration_name",
"token_access_policy.0.access_token_ttl_in_minutes": 30,
})
}

func TestResourceCustomAppIntegrationDelete(t *testing.T) {
qa.ResourceFixture{
MockAccountClientFunc: func(a *mocks.MockAccountClient) {
a.GetMockCustomAppIntegrationAPI().EXPECT().DeleteByIntegrationId(mock.Anything, "integration_id").Return(nil)
},
Resource: ResourceCustomAppIntegration(),
AccountID: "account_id",
Delete: true,
ID: "integration_id",
}.ApplyAndExpectData(t, nil)
}
58 changes: 58 additions & 0 deletions docs/resources/custom_app_integration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
---
subcategory: "Apps"
---
# databricks_custom_app_integration Resource

-> Initialize provider with `alias = "account"`, and `host` pointing to the account URL, like, `host = "https://accounts.cloud.databricks.com"`. Use `provider = databricks.account` for all account-level resources.

This resource allows you to enable [custom OAuth applications](https://docs.databricks.com/en/integrations/enable-disable-oauth.html#enable-custom-oauth-applications-using-the-databricks-ui).

## Example Usage

```hcl
resource "databricks_custom_app_integration" "this" {
name = "custom_integration_name"
redirect_urls = ["https://example.com"]
scopes = ["all-apis"]
token_access_policy {
access_token_ttl_in_minutes = %s
refresh_token_ttl_in_minutes = 30
}
}
```

## Argument Reference

The following arguments are available:

* `name` - (Required) Name of the custom OAuth app. Change requires a new resource.
* `confidential` - Indicates whether an OAuth client secret is required to authenticate this client. Default to `false`. Change requires a new resource.
* `redirect_urls` - List of OAuth redirect urls.
* `scopes` - OAuth scopes granted to the application. Supported scopes: `all-apis`, `sql`, `offline_access`, `openid`, `profile`, `email`.

### token_access_policy Configuration Block (Optional)

* `access_token_ttl_in_minutes` - access token time to live (TTL) in minutes.
* `refresh_token_ttl_in_minutes` - refresh token TTL in minutes. The TTL of refresh token cannot be lower than TTL of access token.

## Attribute Reference

In addition to all arguments above, the following attributes are exported:

* `integration_id` - Unique integration id for the custom OAuth app.
* `client_id` - OAuth client-id generated by Databricks
* `client_secret` - OAuth client-secret generated by the Databricks if this is a confidential OAuth app.

## Import

This resource can be imported by its integration ID.

```sh
terraform import databricks_custom_app_integration.this '<integration_id>'
```

## Related Resources

The following resources are used in the context:

* [databricks_mws_workspaces](mws_workspaces.md) to set up Databricks workspaces.
34 changes: 34 additions & 0 deletions internal/acceptance/custom_app_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package acceptance

import (
"fmt"
"testing"
)

var (
customAppIntegrationTemplate = `resource "databricks_custom_app_integration" "this" {
name = "custom_integration_name"
redirect_urls = ["https://example.com"]
scopes = ["all-apis"]
token_access_policy {
access_token_ttl_in_minutes = %s
refresh_token_ttl_in_minutes = 30
}
}`
)

func TestMwsAccCustomAppIntegrationCreate(t *testing.T) {
loadAccountEnv(t)
AccountLevel(t, Step{
Template: fmt.Sprintf(customAppIntegrationTemplate, "30"),
})
}

func TestMwsAccCustomAppIntegrationUpdate(t *testing.T) {
loadAccountEnv(t)
AccountLevel(t, Step{
Template: fmt.Sprintf(customAppIntegrationTemplate, "30"),
}, Step{
Template: fmt.Sprintf(customAppIntegrationTemplate, "15"),
})
}
2 changes: 2 additions & 0 deletions internal/providers/sdkv2/sdkv2.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"github.com/databricks/databricks-sdk-go/useragent"

"github.com/databricks/terraform-provider-databricks/access"
"github.com/databricks/terraform-provider-databricks/apps"
"github.com/databricks/terraform-provider-databricks/aws"
"github.com/databricks/terraform-provider-databricks/catalog"
"github.com/databricks/terraform-provider-databricks/clusters"
Expand Down Expand Up @@ -137,6 +138,7 @@ func DatabricksProvider() *schema.Provider {
"databricks_budget": finops.ResourceBudget().ToResource(),
"databricks_catalog": catalog.ResourceCatalog().ToResource(),
"databricks_catalog_workspace_binding": catalog.ResourceCatalogWorkspaceBinding().ToResource(),
"databricks_custom_app_integration": apps.ResourceCustomAppIntegration().ToResource(),
"databricks_connection": catalog.ResourceConnection().ToResource(),
"databricks_cluster": clusters.ResourceCluster().ToResource(),
"databricks_cluster_policy": policies.ResourceClusterPolicy().ToResource(),
Expand Down

0 comments on commit 6088aa8

Please sign in to comment.