Skip to content

Commit 8b63826

Browse files
mgyuchttanmay-db
andauthored
[Feature] Add no_compute attribute to databricks_app (#4364)
## Changes Support `no_compute` flag for Apps. The flag is only used on create to determine whether compute should be provisioned as part of the Create App RPC. Changes to this flag for existing resources are no-ops and simply propagated from plan to state. ## Tests <!-- How is this tested? Please see the checklist below and also describe any other relevant tests --> - [ ] `make test` run locally - [ ] relevant change in `docs/` folder - [ ] covered with integration tests in `internal/acceptance` - [ ] using Go SDK - [ ] using TF Plugin Framework --------- Co-authored-by: Tanmay Rustagi <[email protected]> Co-authored-by: Tanmay Rustagi <[email protected]>
1 parent d497347 commit 8b63826

File tree

2 files changed

+129
-15
lines changed

2 files changed

+129
-15
lines changed

internal/providers/pluginfw/products/app/resource_app.go

Lines changed: 88 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package app
22

33
import (
44
"context"
5+
"fmt"
56

7+
"github.com/databricks/databricks-sdk-go"
68
"github.com/databricks/databricks-sdk-go/apierr"
9+
"github.com/databricks/databricks-sdk-go/retries"
710
"github.com/databricks/databricks-sdk-go/service/apps"
811
"github.com/databricks/terraform-provider-databricks/common"
912
pluginfwcommon "github.com/databricks/terraform-provider-databricks/internal/providers/pluginfw/common"
@@ -14,14 +17,27 @@ import (
1417
"github.com/hashicorp/terraform-plugin-framework-validators/objectvalidator"
1518
"github.com/hashicorp/terraform-plugin-framework/path"
1619
"github.com/hashicorp/terraform-plugin-framework/resource"
20+
"github.com/hashicorp/terraform-plugin-framework/resource/schema/int64planmodifier"
1721
"github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier"
22+
"github.com/hashicorp/terraform-plugin-framework/types"
1823
)
1924

2025
const (
2126
resourceName = "app"
2227
resourceNamePlural = "apps"
2328
)
2429

30+
type appResource struct {
31+
apps_tf.App
32+
NoCompute types.Bool `tfsdk:"no_compute"`
33+
}
34+
35+
func (a appResource) ApplySchemaCustomizations(s map[string]tfschema.AttributeBuilder) map[string]tfschema.AttributeBuilder {
36+
s["no_compute"] = s["no_compute"].SetOptional()
37+
s = apps_tf.App{}.ApplySchemaCustomizations(s)
38+
return s
39+
}
40+
2541
func ResourceApp() resource.Resource {
2642
return &resourceApp{}
2743
}
@@ -35,14 +51,24 @@ func (a resourceApp) Metadata(ctx context.Context, req resource.MetadataRequest,
3551
}
3652

3753
func (a resourceApp) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) {
38-
resp.Schema = tfschema.ResourceStructToSchema(ctx, apps_tf.App{}, func(cs tfschema.CustomizableSchema) tfschema.CustomizableSchema {
54+
resp.Schema = tfschema.ResourceStructToSchema(ctx, appResource{}, func(cs tfschema.CustomizableSchema) tfschema.CustomizableSchema {
3955
cs.AddPlanModifier(stringplanmodifier.RequiresReplace(), "name")
4056
exclusiveFields := []string{"job", "secret", "serving_endpoint", "sql_warehouse"}
4157
paths := path.Expressions{}
4258
for _, field := range exclusiveFields[1:] {
4359
paths = append(paths, path.MatchRelative().AtParent().AtName(field))
4460
}
4561
cs.AddValidator(objectvalidator.ExactlyOneOf(paths...), "resources", exclusiveFields[0])
62+
for _, field := range []string{
63+
"create_time",
64+
"creator",
65+
"service_principal_client_id",
66+
"service_principal_name",
67+
"url",
68+
} {
69+
cs.AddPlanModifier(stringplanmodifier.UseStateForUnknown(), field)
70+
}
71+
cs.AddPlanModifier(int64planmodifier.UseStateForUnknown(), "service_principal_id")
4672
return cs
4773
})
4874
}
@@ -61,7 +87,7 @@ func (a *resourceApp) Create(ctx context.Context, req resource.CreateRequest, re
6187
return
6288
}
6389

64-
var app apps_tf.App
90+
var app appResource
6591
resp.Diagnostics.Append(req.Plan.Get(ctx, &app)...)
6692
if resp.Diagnostics.HasError() {
6793
return
@@ -73,30 +99,39 @@ func (a *resourceApp) Create(ctx context.Context, req resource.CreateRequest, re
7399
}
74100

75101
// Create the app
76-
waiter, err := w.Apps.Create(ctx, apps.CreateAppRequest{App: &appGoSdk})
102+
var forceSendFields []string
103+
if !app.NoCompute.IsNull() {
104+
forceSendFields = append(forceSendFields, "NoCompute")
105+
}
106+
waiter, err := w.Apps.Create(ctx, apps.CreateAppRequest{
107+
App: &appGoSdk,
108+
NoCompute: app.NoCompute.ValueBool(),
109+
ForceSendFields: forceSendFields,
110+
})
77111
if err != nil {
78112
resp.Diagnostics.AddError("failed to create app", err.Error())
79113
return
80114
}
81115

82116
// Store the initial version of the app in state
83-
var newApp apps_tf.App
117+
var newApp appResource
84118
resp.Diagnostics.Append(converters.GoSdkToTfSdkStruct(ctx, waiter.Response, &newApp)...)
85119
if resp.Diagnostics.HasError() {
86120
return
87121
}
122+
newApp.NoCompute = app.NoCompute
88123
resp.Diagnostics.Append(resp.State.Set(ctx, newApp)...)
89124
if resp.Diagnostics.HasError() {
90125
return
91126
}
92127

93-
// Wait for the app to be created
94-
finalApp, err := waiter.Get()
128+
// Wait for the app to be created. If no_compute is specified, the terminal state is
129+
// STOPPED, otherwise it is ACTIVE.
130+
finalApp, err := a.waitForApp(ctx, w, appGoSdk.Name)
95131
if err != nil {
96-
resp.Diagnostics.AddError("error waiting for app to be ready", err.Error())
132+
resp.Diagnostics.AddError("error waiting for app to be active or stopped", err.Error())
97133
return
98134
}
99-
100135
// Store the final version of the app in state
101136
resp.Diagnostics.Append(converters.GoSdkToTfSdkStruct(ctx, finalApp, &newApp)...)
102137
if resp.Diagnostics.HasError() {
@@ -108,6 +143,43 @@ func (a *resourceApp) Create(ctx context.Context, req resource.CreateRequest, re
108143
}
109144
}
110145

146+
// This is copied from the retries package of the databricks-sdk-go. It should be made public,
147+
// but for now, I'm copying it here.
148+
func shouldRetry(err error) bool {
149+
if err == nil {
150+
return false
151+
}
152+
e := err.(*retries.Err)
153+
if e == nil {
154+
return false
155+
}
156+
return !e.Halt
157+
}
158+
159+
// waitForApp waits for the app to reach the target state. The target state is either ACTIVE or STOPPED.
160+
// Apps with no_compute set to true will reach the STOPPED state, otherwise they will reach the ACTIVE state.
161+
func (a *resourceApp) waitForApp(ctx context.Context, w *databricks.WorkspaceClient, name string) (*apps.App, error) {
162+
retrier := retries.New[apps.App](retries.WithTimeout(-1), retries.WithRetryFunc(shouldRetry))
163+
return retrier.Run(ctx, func(ctx context.Context) (*apps.App, error) {
164+
app, err := w.Apps.GetByName(ctx, name)
165+
if err != nil {
166+
return nil, retries.Halt(err)
167+
}
168+
status := app.ComputeStatus.State
169+
statusMessage := app.ComputeStatus.Message
170+
switch status {
171+
case apps.ComputeStateActive, apps.ComputeStateStopped:
172+
return app, nil
173+
case apps.ComputeStateError:
174+
err := fmt.Errorf("failed to reach %s or %s, got %s: %s",
175+
apps.ComputeStateActive, apps.ComputeStateStopped, status, statusMessage)
176+
return nil, retries.Halt(err)
177+
default:
178+
return nil, retries.Continues(statusMessage)
179+
}
180+
})
181+
}
182+
111183
func (a *resourceApp) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) {
112184
ctx = pluginfwcontext.SetUserAgentInResourceContext(ctx, resourceName)
113185
w, diags := a.client.GetWorkspaceClient()
@@ -116,7 +188,7 @@ func (a *resourceApp) Read(ctx context.Context, req resource.ReadRequest, resp *
116188
return
117189
}
118190

119-
var app apps_tf.App
191+
var app appResource
120192
resp.Diagnostics.Append(req.State.Get(ctx, &app)...)
121193
if resp.Diagnostics.HasError() {
122194
return
@@ -128,11 +200,12 @@ func (a *resourceApp) Read(ctx context.Context, req resource.ReadRequest, resp *
128200
return
129201
}
130202

131-
var newApp apps_tf.App
203+
var newApp appResource
132204
resp.Diagnostics.Append(converters.GoSdkToTfSdkStruct(ctx, appGoSdk, &newApp)...)
133205
if resp.Diagnostics.HasError() {
134206
return
135207
}
208+
newApp.NoCompute = app.NoCompute
136209
resp.Diagnostics.Append(resp.State.Set(ctx, newApp)...)
137210
if resp.Diagnostics.HasError() {
138211
return
@@ -147,7 +220,7 @@ func (a *resourceApp) Update(ctx context.Context, req resource.UpdateRequest, re
147220
return
148221
}
149222

150-
var app apps_tf.App
223+
var app appResource
151224
resp.Diagnostics.Append(req.Plan.Get(ctx, &app)...)
152225
if resp.Diagnostics.HasError() {
153226
return
@@ -166,11 +239,13 @@ func (a *resourceApp) Update(ctx context.Context, req resource.UpdateRequest, re
166239
}
167240

168241
// Store the updated version of the app in state
169-
var newApp apps_tf.App
242+
var newApp appResource
170243
resp.Diagnostics.Append(converters.GoSdkToTfSdkStruct(ctx, response, &newApp)...)
171244
if resp.Diagnostics.HasError() {
172245
return
173246
}
247+
// Modifying no_compute after creation has no effect.
248+
newApp.NoCompute = app.NoCompute
174249
resp.Diagnostics.Append(resp.State.Set(ctx, newApp)...)
175250
if resp.Diagnostics.HasError() {
176251
return
@@ -185,7 +260,7 @@ func (a *resourceApp) Delete(ctx context.Context, req resource.DeleteRequest, re
185260
return
186261
}
187262

188-
var app apps_tf.App
263+
var app appResource
189264
resp.Diagnostics.Append(req.State.Get(ctx, &app)...)
190265
if resp.Diagnostics.HasError() {
191266
return

internal/providers/pluginfw/products/app/resource_app_acc_test.go

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ const baseResources = `
5555
func makeTemplate(description string) string {
5656
appTemplate := baseResources + `
5757
resource "databricks_app" "this" {
58-
name = "{var.STICKY_RANDOM}"
58+
name = "tf-{var.STICKY_RANDOM}"
5959
description = "%s"
6060
resources = [{
6161
name = "secret"
@@ -93,7 +93,7 @@ func makeTemplate(description string) string {
9393

9494
var templateWithInvalidResource = `
9595
resource "databricks_app" "this" {
96-
name = "{var.STICKY_RANDOM}"
96+
name = "tf-{var.STICKY_RANDOM}"
9797
description = "My app"
9898
resources = [{
9999
name = "invalid resource"
@@ -147,3 +147,42 @@ func TestAccAppResource(t *testing.T) {
147147
ImportStateVerifyIdentifierAttribute: "name",
148148
})
149149
}
150+
151+
func TestAccAppResource_NoCompute(t *testing.T) {
152+
acceptance.LoadWorkspaceEnv(t)
153+
if acceptance.IsGcp(t) {
154+
acceptance.Skipf(t)("not available on GCP")
155+
}
156+
acceptance.WorkspaceLevel(t, acceptance.Step{
157+
Template: `
158+
resource "databricks_secret_scope" "this" {
159+
name = "tf-{var.STICKY_RANDOM}"
160+
}
161+
162+
resource "databricks_secret" "this" {
163+
scope = databricks_secret_scope.this.name
164+
key = "tf-{var.STICKY_RANDOM}"
165+
string_value = "secret"
166+
}
167+
resource "databricks_app" "this" {
168+
no_compute = true
169+
name = "tf-{var.STICKY_RANDOM}"
170+
description = "no_compute app"
171+
resources = [{
172+
name = "secret"
173+
description = "secret for app"
174+
secret = {
175+
scope = databricks_secret_scope.this.name
176+
key = databricks_secret.this.key
177+
permission = "MANAGE"
178+
}
179+
}]
180+
}
181+
`,
182+
Check: func(s *terraform.State) error {
183+
computeStatus := s.RootModule().Resources["databricks_app.this"].Primary.Attributes["compute_status.state"]
184+
assert.Equal(t, "STOPPED", computeStatus)
185+
return nil
186+
},
187+
})
188+
}

0 commit comments

Comments
 (0)