Skip to content

Commit 424c609

Browse files
Allow rotating token block in databricks_mws_workspaces resource by only changing comment field (#2114)
Tested manually for the following cases Without this PR the provider recreates the entire workspace on a token update With changes in this PR only the token is refreshed When both token and storage_configuration_id are changed then the entire workspace is recreated Additional unit tests also added that allow checks that patch workspace calls are not made when only token is changed Also added an integration test to check tokens are successfully updated
1 parent aff8649 commit 424c609

File tree

4 files changed

+282
-4
lines changed

4 files changed

+282
-4
lines changed

internal/acceptance/init_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -272,6 +272,24 @@ func resourceCheck(name string,
272272
}
273273
}
274274

275+
// resourceCheckWithState calls back a function with client and resource instance state
276+
func resourceCheckWithState(name string,
277+
cb func(ctx context.Context, client *common.DatabricksClient, state *terraform.InstanceState) error) resource.TestCheckFunc {
278+
return func(s *terraform.State) error {
279+
rs, ok := s.RootModule().Resources[name]
280+
if !ok {
281+
return fmt.Errorf("not found: %s", name)
282+
}
283+
client, err := client.New(&config.Config{})
284+
if err != nil {
285+
panic(err)
286+
}
287+
return cb(context.Background(), &common.DatabricksClient{
288+
DatabricksClient: client,
289+
}, rs.Primary)
290+
}
291+
}
292+
275293
const fullCharset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
276294
const hexCharset = "0123456789abcdef"
277295

internal/acceptance/mws_workspaces_test.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package acceptance
22

33
import (
4+
"context"
45
"testing"
6+
7+
"github.com/databricks/terraform-provider-databricks/common"
8+
"github.com/databricks/terraform-provider-databricks/tokens"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
10+
"github.com/stretchr/testify/assert"
511
)
612

713
func TestMwsAccWorkspaces(t *testing.T) {
@@ -54,6 +60,129 @@ func TestMwsAccWorkspaces(t *testing.T) {
5460
})
5561
}
5662

63+
func TestMwsAccWorkspacesTokenUpdate(t *testing.T) {
64+
accountLevel(t, step{
65+
Template: `
66+
resource "databricks_mws_credentials" "this" {
67+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
68+
credentials_name = "credentials-ws-{var.RANDOM}"
69+
role_arn = "{env.TEST_CROSSACCOUNT_ARN}"
70+
}
71+
resource "databricks_mws_customer_managed_keys" "this" {
72+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
73+
aws_key_info {
74+
key_arn = "{env.TEST_MANAGED_KMS_KEY_ARN}"
75+
key_alias = "{env.TEST_MANAGED_KMS_KEY_ALIAS}"
76+
}
77+
use_cases = ["MANAGED_SERVICES"]
78+
}
79+
resource "databricks_mws_storage_configurations" "this" {
80+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
81+
storage_configuration_name = "storage-ws-{var.RANDOM}"
82+
bucket_name = "{env.TEST_ROOT_BUCKET}"
83+
}
84+
resource "databricks_mws_workspaces" "this" {
85+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
86+
workspace_name = "terra-{var.RANDOM}"
87+
aws_region = "{env.AWS_REGION}"
88+
89+
credentials_id = databricks_mws_credentials.this.credentials_id
90+
storage_configuration_id = databricks_mws_storage_configurations.this.storage_configuration_id
91+
managed_services_customer_managed_key_id = databricks_mws_customer_managed_keys.this.customer_managed_key_id
92+
93+
token {
94+
comment = "test foo"
95+
}
96+
}`,
97+
Check: resourceCheckWithState("databricks_mws_workspaces.this",
98+
func(ctx context.Context, client *common.DatabricksClient, state *terraform.InstanceState) error {
99+
workspaceUrl, ok := state.Attributes["workspace_url"]
100+
assert.True(t, ok, "workspace_url is absent from databricks_mws_workspaces instance state")
101+
102+
workspaceClient, err := client.ClientForHost(ctx, workspaceUrl)
103+
assert.NoError(t, err)
104+
105+
tokensAPI := tokens.NewTokensAPI(ctx, workspaceClient)
106+
tokens, err := tokensAPI.List()
107+
assert.NoError(t, err)
108+
109+
foundFoo := false
110+
foundBar := false
111+
for _, token := range tokens {
112+
if token.Comment == "test foo" {
113+
foundFoo = true
114+
}
115+
if token.Comment == "test bar" {
116+
foundBar = true
117+
}
118+
}
119+
assert.True(t, foundFoo)
120+
assert.False(t, foundBar)
121+
return nil
122+
}),
123+
},
124+
step{
125+
Template: `
126+
resource "databricks_mws_credentials" "this" {
127+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
128+
credentials_name = "credentials-ws-{var.RANDOM}"
129+
role_arn = "{env.TEST_CROSSACCOUNT_ARN}"
130+
}
131+
resource "databricks_mws_customer_managed_keys" "this" {
132+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
133+
aws_key_info {
134+
key_arn = "{env.TEST_MANAGED_KMS_KEY_ARN}"
135+
key_alias = "{env.TEST_MANAGED_KMS_KEY_ALIAS}"
136+
}
137+
use_cases = ["MANAGED_SERVICES"]
138+
}
139+
resource "databricks_mws_storage_configurations" "this" {
140+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
141+
storage_configuration_name = "storage-ws-{var.RANDOM}"
142+
bucket_name = "{env.TEST_ROOT_BUCKET}"
143+
}
144+
resource "databricks_mws_workspaces" "this" {
145+
account_id = "{env.DATABRICKS_ACCOUNT_ID}"
146+
workspace_name = "terra-{var.RANDOM}"
147+
aws_region = "{env.AWS_REGION}"
148+
149+
credentials_id = databricks_mws_credentials.this.credentials_id
150+
storage_configuration_id = databricks_mws_storage_configurations.this.storage_configuration_id
151+
managed_services_customer_managed_key_id = databricks_mws_customer_managed_keys.this.customer_managed_key_id
152+
153+
token {
154+
comment = "test bar"
155+
}
156+
}`,
157+
Check: resourceCheckWithState("databricks_mws_workspaces.this",
158+
func(ctx context.Context, client *common.DatabricksClient, state *terraform.InstanceState) error {
159+
workspaceUrl, ok := state.Attributes["workspace_url"]
160+
assert.True(t, ok, "workspace_url is absent from databricks_mws_workspaces instance state")
161+
162+
workspaceClient, err := client.ClientForHost(ctx, workspaceUrl)
163+
assert.NoError(t, err)
164+
165+
tokensAPI := tokens.NewTokensAPI(ctx, workspaceClient)
166+
tokens, err := tokensAPI.List()
167+
assert.NoError(t, err)
168+
169+
foundFoo := false
170+
foundBar := false
171+
for _, token := range tokens {
172+
if token.Comment == "test foo" {
173+
foundFoo = true
174+
}
175+
if token.Comment == "test bar" {
176+
foundBar = true
177+
}
178+
}
179+
assert.False(t, foundFoo)
180+
assert.True(t, foundBar)
181+
return nil
182+
}),
183+
})
184+
}
185+
57186
func TestMwsAccGcpWorkspaces(t *testing.T) {
58187
accountLevel(t, step{
59188
Template: `

mws/resource_mws_workspaces.go

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -560,9 +560,11 @@ func ResourceMwsWorkspaces() *schema.Resource {
560560
workspace.CustomerManagedKeyID = ""
561561
}
562562
workspacesAPI := NewWorkspacesAPI(ctx, c)
563-
err := workspacesAPI.UpdateRunning(workspace, d.Timeout(schema.TimeoutUpdate))
564-
if err != nil {
565-
return err
563+
if d.HasChangeExcept("token") {
564+
err := workspacesAPI.UpdateRunning(workspace, d.Timeout(schema.TimeoutUpdate))
565+
if err != nil {
566+
return err
567+
}
566568
}
567569
return UpdateTokenIfNeeded(workspacesAPI, workspaceSchema, d)
568570
},

mws/resource_mws_workspaces_test.go

Lines changed: 130 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -990,6 +990,48 @@ func TestWorkspace_WaitForResolve(t *testing.T) {
990990
}
991991

992992
func updateWorkspaceTokenFixture(t *testing.T, fixtures []qa.HTTPFixture, state map[string]string, hcl string) {
993+
accountsAPI := []qa.HTTPFixture{
994+
{
995+
Method: "GET",
996+
ReuseRequest: true,
997+
Resource: "/api/2.0/accounts/c/workspaces/0",
998+
},
999+
}
1000+
tokensAPI := []qa.HTTPFixture{
1001+
{
1002+
Method: "GET",
1003+
Resource: "/api/2.0/token/list",
1004+
Response: `{}`, // we just need a JSON for this
1005+
},
1006+
}
1007+
tokensAPI = append(tokensAPI, fixtures...)
1008+
// outer HTTP server is used for inner request for "just created" workspace
1009+
qa.HTTPFixturesApply(t, tokensAPI, func(ctx context.Context, wsClient *common.DatabricksClient) {
1010+
// a bit hacky, but the whole thing is more readable
1011+
accountsAPI[0].Response = Workspace{
1012+
WorkspaceStatus: "RUNNING",
1013+
WorkspaceURL: wsClient.Config.Host,
1014+
}
1015+
state["workspace_url"] = wsClient.Config.Host
1016+
state["workspace_name"] = "b"
1017+
state["account_id"] = "c"
1018+
state["network_id"] = "d"
1019+
state["is_no_public_ip_enabled"] = "false"
1020+
qa.ResourceFixture{
1021+
Fixtures: accountsAPI,
1022+
Resource: ResourceMwsWorkspaces(),
1023+
InstanceState: state,
1024+
Update: true,
1025+
ID: "a",
1026+
HCL: hcl + `
1027+
workspace_name = "b"
1028+
account_id = "c",
1029+
network_id = "d"`,
1030+
}.Apply(t)
1031+
})
1032+
}
1033+
1034+
func updateWorkspaceTokenFixtureWithPatch(t *testing.T, fixtures []qa.HTTPFixture, state map[string]string, hcl string) {
9931035
accountsAPI := []qa.HTTPFixture{
9941036
{
9951037
Method: "PATCH",
@@ -1019,6 +1061,7 @@ func updateWorkspaceTokenFixture(t *testing.T, fixtures []qa.HTTPFixture, state
10191061
state["workspace_url"] = wsClient.Config.Host
10201062
state["workspace_name"] = "b"
10211063
state["account_id"] = "c"
1064+
state["storage_customer_managed_key_id"] = "1234"
10221065
state["is_no_public_ip_enabled"] = "false"
10231066
qa.ResourceFixture{
10241067
Fixtures: accountsAPI,
@@ -1028,7 +1071,8 @@ func updateWorkspaceTokenFixture(t *testing.T, fixtures []qa.HTTPFixture, state
10281071
ID: "a",
10291072
HCL: hcl + `
10301073
workspace_name = "b"
1031-
account_id = "c"`,
1074+
account_id = "c",
1075+
storage_customer_managed_key_id = "1234"`,
10321076
}.Apply(t)
10331077
})
10341078
}
@@ -1054,6 +1098,53 @@ func TestUpdateWorkspace_AddToken(t *testing.T) {
10541098
}, `token {}`)
10551099
}
10561100

1101+
func TestUpdateWorkspace_AddTokenAndChangeNetworkId(t *testing.T) {
1102+
updateWorkspaceTokenFixtureWithPatch(t, []qa.HTTPFixture{
1103+
{
1104+
Method: "POST",
1105+
Resource: "/api/2.0/token/create",
1106+
ExpectedRequest: Token{
1107+
LifetimeSeconds: 2.592e+06,
1108+
Comment: "Terraform PAT",
1109+
},
1110+
Response: tokens.TokenResponse{
1111+
TokenValue: "sensitive",
1112+
TokenInfo: &tokens.TokenInfo{
1113+
TokenID: "abcdef",
1114+
},
1115+
},
1116+
},
1117+
}, map[string]string{
1118+
"network_id": "alpha",
1119+
// no token in state
1120+
}, `
1121+
network_id = "beta"
1122+
token {}
1123+
`)
1124+
}
1125+
1126+
func TestUpdateWorkspace_DeleteTokenAndChangeNetworkId(t *testing.T) {
1127+
updateWorkspaceTokenFixtureWithPatch(t, []qa.HTTPFixture{
1128+
{
1129+
Method: "POST",
1130+
Resource: "/api/2.0/token/delete",
1131+
ExpectedRequest: map[string]any{
1132+
"token_id": "abcdef",
1133+
},
1134+
},
1135+
}, map[string]string{
1136+
"token.#": "1",
1137+
"token.0.comment": "Terraform PAT",
1138+
"token.0.lifetime_seconds": "2592000",
1139+
"token.0.token_id": "abcdef",
1140+
"token.0.token_value": "sensitive",
1141+
"network_id": "alpha",
1142+
}, `
1143+
network_id = "beta"
1144+
`)
1145+
1146+
}
1147+
10571148
func TestUpdateWorkspace_DeleteToken(t *testing.T) {
10581149
updateWorkspaceTokenFixture(t, []qa.HTTPFixture{
10591150
{
@@ -1072,6 +1163,44 @@ func TestUpdateWorkspace_DeleteToken(t *testing.T) {
10721163
}, ``)
10731164
}
10741165

1166+
func TestUpdateWorkspace_ReplaceTokenAndChangeNetworkId(t *testing.T) {
1167+
updateWorkspaceTokenFixtureWithPatch(t, []qa.HTTPFixture{
1168+
{
1169+
Method: "POST",
1170+
Resource: "/api/2.0/token/delete",
1171+
ExpectedRequest: map[string]any{
1172+
"token_id": "abcdef",
1173+
},
1174+
},
1175+
{
1176+
Method: "POST",
1177+
Resource: "/api/2.0/token/create",
1178+
ExpectedRequest: Token{
1179+
LifetimeSeconds: 2.592e+06,
1180+
Comment: "I am Batman!",
1181+
},
1182+
Response: tokens.TokenResponse{
1183+
TokenValue: "new-value",
1184+
TokenInfo: &tokens.TokenInfo{
1185+
TokenID: "new-id",
1186+
},
1187+
},
1188+
},
1189+
}, map[string]string{
1190+
"token.#": "1",
1191+
"token.0.comment": "Terraform PAT",
1192+
"token.0.lifetime_seconds": "2592000",
1193+
"token.0.token_id": "abcdef",
1194+
"token.0.token_value": "sensitive",
1195+
"network_id": "alpha",
1196+
},
1197+
`
1198+
network_id = "beta"
1199+
token {
1200+
comment = "I am Batman!"
1201+
}`)
1202+
}
1203+
10751204
func TestUpdateWorkspace_ReplaceToken(t *testing.T) {
10761205
updateWorkspaceTokenFixture(t, []qa.HTTPFixture{
10771206
{

0 commit comments

Comments
 (0)