Skip to content

Commit c27ccd6

Browse files
Added support for iam_role_arn in databricks_instance_profile (#1943)
* Add IamRoleArn to instance_profile resource * Add tests * Add handling for empty string role ARN * Add documentation * Modifications according to PR comments * Fix typo * Unify ARN validation functions * Rename validation tests * Do not allow empty instance profile ARNs * Remove `omitempty` from required field * Add default clause
1 parent 8470f9a commit c27ccd6

File tree

5 files changed

+280
-9
lines changed

5 files changed

+280
-9
lines changed

aws/resource_group_instance_profile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func ResourceGroupInstanceProfile() *schema.Resource {
1515
r := common.NewPairID("group_id", "instance_profile_id").Schema(func(
1616
m map[string]*schema.Schema) map[string]*schema.Schema {
17-
m["instance_profile_id"].ValidateDiagFunc = ValidInstanceProfile
17+
m["instance_profile_id"].ValidateDiagFunc = ValidArn
1818
return m
1919
}).BindResource(common.BindResource{
2020
ReadContext: func(ctx context.Context, groupID, roleARN string, c *common.DatabricksClient) error {

aws/resource_instance_profile.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ import (
1818

1919
// InstanceProfileInfo contains the ARN for aws instance profiles
2020
type InstanceProfileInfo struct {
21-
InstanceProfileArn string `json:"instance_profile_arn,omitempty"`
21+
InstanceProfileArn string `json:"instance_profile_arn"`
22+
IamRoleArn string `json:"iam_role_arn,omitempty"`
2223
IsMetaInstanceProfile bool `json:"is_meta_instance_profile,omitempty"`
2324
SkipValidation bool `json:"skip_validation,omitempty" tf:"computed"`
2425
}
@@ -83,6 +84,18 @@ func (a InstanceProfilesAPI) Delete(instanceProfileARN string) error {
8384
}, nil)
8485
}
8586

87+
// Update updates the IAM role ARN of an existing instance profile
88+
func (a InstanceProfilesAPI) Update(ipi InstanceProfileInfo) error {
89+
data := map[string]any{
90+
"instance_profile_arn": ipi.InstanceProfileArn,
91+
"iam_role_arn": ipi.InstanceProfileArn,
92+
}
93+
if ipi.IamRoleArn != "" {
94+
data["iam_role_arn"] = ipi.IamRoleArn
95+
}
96+
return a.client.Post(a.context, "/instance-profiles/edit", data, nil)
97+
}
98+
8699
// IsRegistered checks if instance profile exists
87100
func (a InstanceProfilesAPI) IsRegistered(arn string) bool {
88101
if _, err := a.Read(arn); err == nil {
@@ -137,7 +150,8 @@ func (a InstanceProfilesAPI) Synchronized(arn string, testCallback func() bool)
137150
func ResourceInstanceProfile() *schema.Resource {
138151
instanceProfileSchema := common.StructToSchema(InstanceProfileInfo{},
139152
func(m map[string]*schema.Schema) map[string]*schema.Schema {
140-
m["instance_profile_arn"].ValidateDiagFunc = ValidInstanceProfile
153+
m["instance_profile_arn"].ValidateDiagFunc = ValidArn
154+
m["iam_role_arn"].ValidateDiagFunc = ValidArn
141155
m["skip_validation"].DiffSuppressFunc = func(k, old, new string, d *schema.ResourceData) bool {
142156
if old == "false" && new == "true" {
143157
return true
@@ -167,11 +181,16 @@ func ResourceInstanceProfile() *schema.Resource {
167181
Delete: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
168182
return NewInstanceProfilesAPI(ctx, c).Delete(d.Id())
169183
},
184+
Update: func(ctx context.Context, d *schema.ResourceData, c *common.DatabricksClient) error {
185+
var profile InstanceProfileInfo
186+
common.DataToStructPointer(d, instanceProfileSchema, &profile)
187+
return NewInstanceProfilesAPI(ctx, c).Update(profile)
188+
},
170189
}.ToResource()
171190
}
172191

173-
// ValidInstanceProfile validate if it's valid instance profile ARN
174-
func ValidInstanceProfile(v any, c cty.Path) diag.Diagnostics {
192+
// ValidArn validate if it's valid instance profile or role ARN
193+
func ValidArn(v any, c cty.Path) diag.Diagnostics {
175194
s, ok := v.(string)
176195
if !ok {
177196
return diag.Diagnostics{
@@ -182,6 +201,24 @@ func ValidInstanceProfile(v any, c cty.Path) diag.Diagnostics {
182201
},
183202
}
184203
}
204+
var arnType string
205+
switch c[0].(cty.GetAttrStep).Name {
206+
case "instance_profile_arn", "instance_profile_id":
207+
arnType = "instance-profile"
208+
case "iam_role_arn":
209+
arnType = "role"
210+
default:
211+
return diag.Diagnostics{
212+
diag.Diagnostic{
213+
AttributePath: c,
214+
Summary: "Unknown attribute",
215+
Detail: "ARN type associated with attribute is not known",
216+
},
217+
}
218+
}
219+
if s == "" && arnType == "role" {
220+
return nil
221+
}
185222
if !strings.HasPrefix(s, "arn:") {
186223
return diag.Diagnostics{
187224
diag.Diagnostic{
@@ -201,12 +238,12 @@ func ValidInstanceProfile(v any, c cty.Path) diag.Diagnostics {
201238
},
202239
}
203240
}
204-
if !strings.HasPrefix(arnSections[5], "instance-profile") {
241+
if !strings.HasPrefix(arnSections[5], arnType) {
205242
return diag.Diagnostics{
206243
diag.Diagnostic{
207244
AttributePath: c,
208245
Summary: "Invalid ARN",
209-
Detail: fmt.Sprintf("Not an instance profile ARN: %s", v),
246+
Detail: fmt.Sprintf("Not a %s ARN: %s", arnType, v),
210247
},
211248
}
212249
}

aws/resource_instance_profile_test.go

Lines changed: 197 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,73 @@ func TestResourceInstanceProfileCreate(t *testing.T) {
4242
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
4343
}
4444

45+
func TestResourceInstanceProfileWithRoleCreate(t *testing.T) {
46+
d, err := qa.ResourceFixture{
47+
Fixtures: []qa.HTTPFixture{
48+
{
49+
Method: "POST",
50+
Resource: "/api/2.0/instance-profiles/add",
51+
ExpectedRequest: InstanceProfileInfo{
52+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
53+
IamRoleArn: "arn:aws:iam::999999999999:role/my-fake-instance-profile-role",
54+
},
55+
},
56+
{
57+
Method: "GET",
58+
Resource: "/api/2.0/instance-profiles/list",
59+
Response: InstanceProfileList{
60+
InstanceProfiles: []InstanceProfileInfo{
61+
{
62+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
63+
},
64+
},
65+
},
66+
},
67+
},
68+
Resource: ResourceInstanceProfile(),
69+
State: map[string]any{
70+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
71+
"iam_role_arn": "arn:aws:iam::999999999999:role/my-fake-instance-profile-role",
72+
},
73+
Create: true,
74+
}.Apply(t)
75+
assert.NoError(t, err, err)
76+
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
77+
}
78+
79+
func TestResourceInstanceProfileWithEmptyRoleCreate(t *testing.T) {
80+
d, err := qa.ResourceFixture{
81+
Fixtures: []qa.HTTPFixture{
82+
{
83+
Method: "POST",
84+
Resource: "/api/2.0/instance-profiles/add",
85+
ExpectedRequest: InstanceProfileInfo{
86+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
87+
},
88+
},
89+
{
90+
Method: "GET",
91+
Resource: "/api/2.0/instance-profiles/list",
92+
Response: InstanceProfileList{
93+
InstanceProfiles: []InstanceProfileInfo{
94+
{
95+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
96+
},
97+
},
98+
},
99+
},
100+
},
101+
Resource: ResourceInstanceProfile(),
102+
State: map[string]any{
103+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
104+
"iam_role_arn": "",
105+
},
106+
Create: true,
107+
}.Apply(t)
108+
assert.NoError(t, err, err)
109+
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
110+
}
111+
45112
func TestResourceInstanceProfileCreate_Error(t *testing.T) {
46113
d, err := qa.ResourceFixture{
47114
Fixtures: []qa.HTTPFixture{
@@ -65,7 +132,7 @@ func TestResourceInstanceProfileCreate_Error(t *testing.T) {
65132
assert.Equal(t, "", d.Id(), "Id should be empty for error creates")
66133
}
67134

68-
func TestResourceInstanceProfileCreate_Error_InvalidARN(t *testing.T) {
135+
func TestResourceInstanceProfileValidate_Error_InvalidInstanceProfileARN(t *testing.T) {
69136
_, err := qa.ResourceFixture{
70137
Resource: ResourceInstanceProfile(),
71138
State: map[string]any{
@@ -76,6 +143,64 @@ func TestResourceInstanceProfileCreate_Error_InvalidARN(t *testing.T) {
76143
assert.EqualError(t, err, "invalid config supplied. [instance_profile_arn] Invalid ARN")
77144
}
78145

146+
func TestResourceInstanceProfileValidate_Error_InvalidRoleARN(t *testing.T) {
147+
_, err := qa.ResourceFixture{
148+
Resource: ResourceInstanceProfile(),
149+
State: map[string]any{
150+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
151+
"iam_role_arn": "abc",
152+
},
153+
Create: true,
154+
}.Apply(t)
155+
assert.EqualError(t, err, "invalid config supplied. [iam_role_arn] Invalid ARN")
156+
}
157+
158+
func TestResourceInstanceProfileValidate_Error_MalformedARN(t *testing.T) {
159+
_, err := qa.ResourceFixture{
160+
Resource: ResourceInstanceProfile(),
161+
State: map[string]any{
162+
"instance_profile_arn": "arn:aws:iam::instance-profile/my-fake-instance-profile",
163+
},
164+
Create: true,
165+
}.Apply(t)
166+
assert.EqualError(t, err, "invalid config supplied. [instance_profile_arn] Invalid ARN")
167+
}
168+
169+
func TestResourceInstanceProfileValidate_Error_WrongTypeProfileARN(t *testing.T) {
170+
_, err := qa.ResourceFixture{
171+
Resource: ResourceInstanceProfile(),
172+
State: map[string]any{
173+
"instance_profile_arn": "arn:aws:iam::999999999999:failure/my-fake-instance-profile",
174+
},
175+
Create: true,
176+
}.Apply(t)
177+
assert.EqualError(t, err, "invalid config supplied. [instance_profile_arn] Invalid ARN")
178+
}
179+
180+
func TestResourceInstanceProfileValidate_Error_WrongTypeRoleARN(t *testing.T) {
181+
_, err := qa.ResourceFixture{
182+
Resource: ResourceInstanceProfile(),
183+
State: map[string]any{
184+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
185+
"iam_role_arn": "arn:aws:iam::999999999999:failure/my-fake-instance-profile-role",
186+
},
187+
Create: true,
188+
}.Apply(t)
189+
assert.EqualError(t, err, "invalid config supplied. [iam_role_arn] Invalid ARN")
190+
}
191+
192+
func TestResourceInstanceProfileValidate_Error_EmptyInstanceProfileARN(t *testing.T) {
193+
_, err := qa.ResourceFixture{
194+
Resource: ResourceInstanceProfile(),
195+
State: map[string]any{
196+
"instance_profile_arn": "",
197+
"iam_role_arn": "arn:aws:iam::999999999999:role/my-fake-instance-profile-role",
198+
},
199+
Create: true,
200+
}.Apply(t)
201+
assert.EqualError(t, err, "invalid config supplied. [instance_profile_arn] Invalid ARN")
202+
}
203+
79204
func TestResourceInstanceProfileRead(t *testing.T) {
80205
d, err := qa.ResourceFixture{
81206
Fixtures: []qa.HTTPFixture{
@@ -180,6 +305,77 @@ func TestResourceInstanceProfileDelete_Error(t *testing.T) {
180305
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
181306
}
182307

308+
func TestResourceInstanceProfileUpdate(t *testing.T) {
309+
d, err := qa.ResourceFixture{
310+
Fixtures: []qa.HTTPFixture{
311+
{
312+
Method: "GET",
313+
Resource: "/api/2.0/instance-profiles/list",
314+
Response: InstanceProfileList{
315+
InstanceProfiles: []InstanceProfileInfo{
316+
{
317+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
318+
},
319+
},
320+
},
321+
},
322+
{
323+
Method: "POST",
324+
Resource: "/api/2.0/instance-profiles/edit",
325+
ExpectedRequest: InstanceProfileInfo{
326+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
327+
IamRoleArn: "arn:aws:iam::999999999999:role/my-fake-instance-profile-role",
328+
},
329+
},
330+
},
331+
Resource: ResourceInstanceProfile(),
332+
State: map[string]any{
333+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
334+
"iam_role_arn": "arn:aws:iam::999999999999:role/my-fake-instance-profile-role",
335+
},
336+
Update: true,
337+
ID: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
338+
}.Apply(t)
339+
assert.NoError(t, err, err)
340+
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
341+
}
342+
343+
func TestResourceInstanceProfileUpdate_Error(t *testing.T) {
344+
d, err := qa.ResourceFixture{
345+
Fixtures: []qa.HTTPFixture{
346+
{
347+
Method: "GET",
348+
Resource: "/api/2.0/instance-profiles/list",
349+
Response: InstanceProfileList{
350+
InstanceProfiles: []InstanceProfileInfo{
351+
{
352+
InstanceProfileArn: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
353+
},
354+
},
355+
},
356+
},
357+
{
358+
Method: "POST",
359+
Resource: "/api/2.0/instance-profiles/edit",
360+
Response: common.APIErrorBody{
361+
ErrorCode: "INVALID_REQUEST",
362+
Message: "Internal error happened",
363+
},
364+
Status: 400,
365+
},
366+
},
367+
Resource: ResourceInstanceProfile(),
368+
State: map[string]any{
369+
"instance_profile_arn": "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
370+
"iam_role_arn": "",
371+
},
372+
Update: true,
373+
ID: "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile",
374+
}.Apply(t)
375+
qa.AssertErrorStartsWith(t, err, "Internal error happened")
376+
assert.Equal(t, "arn:aws:iam::999999999999:instance-profile/my-fake-instance-profile", d.Id())
377+
}
378+
183379
func TestAccAwsInstanceProfiles(t *testing.T) {
184380
arn := qa.GetEnvOrSkipTest(t, "TEST_EC2_INSTANCE_PROFILE")
185381
client := common.NewClientFromEnvironment()

aws/resource_user_instance_profile.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import (
1414
func ResourceUserInstanceProfile() *schema.Resource {
1515
r := common.NewPairID("user_id", "instance_profile_id").Schema(func(
1616
m map[string]*schema.Schema) map[string]*schema.Schema {
17-
m["instance_profile_id"].ValidateDiagFunc = ValidInstanceProfile
17+
m["instance_profile_id"].ValidateDiagFunc = ValidArn
1818
return m
1919
}).BindResource(common.BindResource{
2020
CreateContext: func(ctx context.Context, userID, roleARN string, c *common.DatabricksClient) error {

docs/resources/instance_profile.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,12 +108,50 @@ resource "databricks_group_instance_profile" "all" {
108108
instance_profile_id = databricks_instance_profile.this.id
109109
}
110110
```
111+
## Usage with Databricks SQL serverless
112+
When the instance profile ARN and its associated IAM role ARN don't match and the instance profile is intended for use with Databricks SQL serverless, the `iam_role_arn` parameter can be specified
113+
114+
```hcl
115+
data "aws_iam_policy_document" "sql_serverless_assume_role" {
116+
statement {
117+
actions = ["sts:AssumeRole"]
118+
principals {
119+
type = "AWS"
120+
identifiers = ["arn:aws:iam::790110701330:role/serverless-customer-resource-role"]
121+
}
122+
condition {
123+
test = "StringEquals"
124+
variable = "sts:ExternalID"
125+
values = [
126+
"databricks-serverless-<YOUR_WORKSPACE_ID1>",
127+
"databricks-serverless-<YOUR_WORKSPACE_ID2>"
128+
]
129+
}
130+
}
131+
}
132+
133+
resource "aws_iam_role" "this" {
134+
name = "my-databricks-sql-serverless-role"
135+
assume_role_policy = data.aws_iam_policy_document.sql_serverless_assume_role.json
136+
}
137+
138+
resource "aws_iam_instance_profile" "this" {
139+
name = "my-databricks-sql-serverless-instance-profile"
140+
role = aws_iam_role.this.name
141+
}
142+
143+
resource "databricks_instance_profile" "this" {
144+
instance_profile_arn = aws_iam_instance_profile.this.arn
145+
iam_role_arn = aws_iam_role.this.arn
146+
}
147+
```
111148

112149
## Argument Reference
113150

114151
The following arguments are supported:
115152

116153
* `instance_profile_arn` - (Required) `ARN` attribute of `aws_iam_instance_profile` output, the EC2 instance profile association to AWS IAM role. This ARN would be validated upon resource creation.
154+
* `iam_role_arn` - (Optional) The AWS IAM role ARN of the role associated with the instance profile. It must have the form `arn:aws:iam::<account-id>:role/<name>`. This field is required if your role name and instance profile name do not match and you want to use the instance profile with Databricks SQL Serverless.
117155
* `is_meta_instance_profile` - (Optional) Whether the instance profile is a meta instance profile. Used only in [IAM credential passthrough](https://docs.databricks.com/security/credential-passthrough/iam-passthrough.html).
118156
* `skip_validation` - (Optional) **For advanced usage only.** If validation fails with an error message that does not indicate an IAM related permission issue, (e.g. “Your requested instance type is not supported in your requested availability zone”), you can pass this flag to skip the validation and forcibly add the instance profile.
119157

0 commit comments

Comments
 (0)