Skip to content

Commit a3cfdee

Browse files
feat: support iam related arguments for ServiceAccountBinding (#121)
1 parent 7f6aa98 commit a3cfdee

File tree

6 files changed

+231
-3
lines changed

6 files changed

+231
-3
lines changed

cloud/data_source_service_account_binding.go

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,11 @@ package cloud
1717
import (
1818
"context"
1919
"fmt"
20-
apierrors "k8s.io/apimachinery/pkg/api/errors"
2120
"strings"
2221

2322
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
2423
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2525
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2626
)
2727

@@ -69,6 +69,19 @@ func dataSourceServiceAccountBinding() *schema.Resource {
6969
Description: descriptions["pool_member_namespace"],
7070
Computed: true,
7171
},
72+
"enable_iam_account_creation": {
73+
Type: schema.TypeBool,
74+
Description: descriptions["enable_iam_account_creation"],
75+
Computed: true,
76+
},
77+
"aws_assume_role_arns": {
78+
Type: schema.TypeList,
79+
Description: descriptions["aws_assume_role_arns"],
80+
Elem: &schema.Schema{
81+
Type: schema.TypeString,
82+
},
83+
Computed: true,
84+
},
7285
},
7386
}
7487
}
@@ -93,6 +106,8 @@ func DataSourceServiceAccountBindingRead(ctx context.Context, d *schema.Resource
93106
_ = d.Set("service_account_name", serviceAccountBinding.Spec.ServiceAccountName)
94107
_ = d.Set("pool_member_name", serviceAccountBinding.Spec.PoolMemberRef.Name)
95108
_ = d.Set("pool_member_namespace", serviceAccountBinding.Spec.PoolMemberRef.Namespace)
109+
_ = d.Set("enable_iam_account_creation", serviceAccountBinding.Spec.EnableIAMAccountCreation)
110+
_ = d.Set("aws_assume_role_arns", flattenStringSlice(serviceAccountBinding.Spec.AWSAssumeRoleARNs))
96111
d.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))
97112

98113
return nil

cloud/provider.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ func init() {
216216
"iam_policy": "IAM policy JSON for S3Table catalog access. This policy should be applied to your AWS IAM role to allow access to S3Table resources.",
217217
"principal_name": "The principal name of apikey, it is the principal name of the service account that the apikey is associated with, it is used to grant permission on pulsar side",
218218
"customized_metadata": "The custom metadata in the api key token",
219+
"enable_iam_account_creation": "Whether to create an IAM account for the service account binding",
220+
"aws_assume_role_arns": "A list of AWS IAM role ARNs which can be assumed by the AWS IAM role created for the service account binding",
219221
}
220222
}
221223

cloud/resource_service_account_binding.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,11 @@ import (
2020
"strings"
2121
"time"
2222

23-
apierrors "k8s.io/apimachinery/pkg/api/errors"
24-
2523
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
2624
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
2725
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
2826
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
27+
apierrors "k8s.io/apimachinery/pkg/api/errors"
2928
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3029
)
3130

@@ -99,6 +98,19 @@ func resourceServiceAccountBinding() *schema.Resource {
9998
Computed: true,
10099
Optional: true,
101100
},
101+
"enable_iam_account_creation": {
102+
Type: schema.TypeBool,
103+
Description: descriptions["enable_iam_account_creation"],
104+
Optional: true,
105+
},
106+
"aws_assume_role_arns": {
107+
Type: schema.TypeList,
108+
Description: descriptions["aws_assume_role_arns"],
109+
Optional: true,
110+
Elem: &schema.Schema{
111+
Type: schema.TypeString,
112+
},
113+
},
102114
},
103115
}
104116
}
@@ -113,6 +125,12 @@ func resourceServiceAccountBindingCreate(ctx context.Context, d *schema.Resource
113125
return diag.FromErr(fmt.Errorf("ERROR_CREATE_SERVICE_ACCOUNT_BINDING: " +
114126
"either (pool_member_name & pool_member_namespace) or cluster_name must be provided"))
115127
}
128+
enableIAMAccountCreation := d.Get("enable_iam_account_creation").(bool)
129+
awsAssumeRoleARNRawList := d.Get("aws_assume_role_arns").([]interface{})
130+
awsAssumeRoleARNs := make([]string, len(awsAssumeRoleARNRawList))
131+
for i, v := range awsAssumeRoleARNRawList {
132+
awsAssumeRoleARNs[i] = v.(string)
133+
}
116134

117135
clientSet, err := getClientSet(getFactoryFromMeta(meta))
118136
if err != nil {
@@ -145,6 +163,8 @@ func resourceServiceAccountBindingCreate(ctx context.Context, d *schema.Resource
145163
Name: poolMemberName,
146164
Namespace: poolMemberNamespace,
147165
},
166+
EnableIAMAccountCreation: enableIAMAccountCreation,
167+
AWSAssumeRoleARNs: awsAssumeRoleARNs,
148168
},
149169
}
150170
serviceAccountBinding, err := clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Create(ctx, sab, metav1.CreateOptions{
@@ -188,6 +208,8 @@ func resourceServiceAccountBindingRead(ctx context.Context, d *schema.ResourceDa
188208
_ = d.Set("service_account_name", serviceAccountBinding.Spec.ServiceAccountName)
189209
_ = d.Set("pool_member_name", serviceAccountBinding.Spec.PoolMemberRef.Name)
190210
_ = d.Set("pool_member_namespace", serviceAccountBinding.Spec.PoolMemberRef.Namespace)
211+
_ = d.Set("enable_iam_account_creation", serviceAccountBinding.Spec.EnableIAMAccountCreation)
212+
_ = d.Set("aws_assume_role_arns", flattenStringSlice(serviceAccountBinding.Spec.AWSAssumeRoleARNs))
191213
d.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))
192214

193215
return nil
Lines changed: 185 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,185 @@
1+
// Copyright 2024 StreamNative, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package cloud
16+
17+
import (
18+
"context"
19+
"fmt"
20+
"math/rand"
21+
"strings"
22+
"testing"
23+
"time"
24+
25+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
26+
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
27+
"k8s.io/apimachinery/pkg/api/errors"
28+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
29+
)
30+
31+
var saGeneratedName = fmt.Sprintf("t-%d-%d", rand.Intn(1000), rand.Intn(100))
32+
33+
func TestServiceAccountBinding(t *testing.T) {
34+
resource.Test(t, resource.TestCase{
35+
PreCheck: func() {
36+
testAccPreCheck(t)
37+
},
38+
ProviderFactories: testAccProviderFactories,
39+
CheckDestroy: testCheckServiceAccountBindingDestroy,
40+
Steps: []resource.TestStep{
41+
{
42+
Config: testResourceDataSourceServiceAccountBinding(
43+
"sndev",
44+
saGeneratedName,
45+
"gcp-shared-usc1-test",
46+
"streamnative"),
47+
Check: resource.ComposeTestCheckFunc(
48+
testCheckServiceAccountBindingExists("streamnative_service_account_binding.test-service-account-binding"),
49+
),
50+
},
51+
},
52+
})
53+
}
54+
55+
func TestServiceAccountBindingRemovedExternally(t *testing.T) {
56+
// This test case is to simulate the situation that the service account binding is removed externally
57+
// and the terraform state still has the resource
58+
resource.Test(t, resource.TestCase{
59+
PreCheck: func() {
60+
testAccPreCheck(t)
61+
},
62+
ProviderFactories: testAccProviderFactories,
63+
CheckDestroy: testCheckServiceAccountBindingDestroy,
64+
Steps: []resource.TestStep{
65+
{
66+
Config: testResourceDataSourceServiceAccountBinding(
67+
"sndev",
68+
saGeneratedName,
69+
"gcp-shared-usc1-test",
70+
"streamnative"),
71+
Check: resource.ComposeTestCheckFunc(
72+
testCheckServiceAccountBindingExists("streamnative_service_account_binding.test-service-account-binding"),
73+
),
74+
},
75+
{
76+
PreConfig: func() {
77+
meta := testAccProvider.Meta()
78+
clientSet, err := getClientSet(getFactoryFromMeta(meta))
79+
if err != nil {
80+
t.Fatal(err)
81+
}
82+
err = clientSet.CloudV1alpha1().
83+
ServiceAccountBindings("sndev").
84+
Delete(context.Background(), saGeneratedName+".streamnative.gcp-shared-usc1-test", metav1.DeleteOptions{})
85+
if err != nil {
86+
t.Fatal(err)
87+
}
88+
},
89+
Config: testResourceDataSourceServiceAccountBinding(
90+
"sndev",
91+
saGeneratedName,
92+
"gcp-shared-usc1-test",
93+
"streamnative"),
94+
PlanOnly: true,
95+
ExpectNonEmptyPlan: true,
96+
},
97+
},
98+
})
99+
}
100+
101+
func testCheckServiceAccountBindingDestroy(s *terraform.State) error {
102+
// Add a sleep for wait the service account binding to be deleted
103+
time.Sleep(5 * time.Second)
104+
for _, rs := range s.RootModule().Resources {
105+
if rs.Type != "streamnative_service_account_binding" {
106+
continue
107+
}
108+
meta := testAccProvider.Meta()
109+
clientSet, err := getClientSet(getFactoryFromMeta(meta))
110+
if err != nil {
111+
return err
112+
}
113+
organizationServiceAccountBinding := strings.Split(rs.Primary.ID, "/")
114+
_, err = clientSet.CloudV1alpha1().
115+
ServiceAccountBindings(organizationServiceAccountBinding[0]).
116+
Get(context.Background(), organizationServiceAccountBinding[1], metav1.GetOptions{})
117+
if err != nil {
118+
if errors.IsNotFound(err) {
119+
return nil
120+
}
121+
return err
122+
}
123+
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_STILL_EXISTS: "%s"`, rs.Primary.ID)
124+
}
125+
return nil
126+
}
127+
128+
func testCheckServiceAccountBindingExists(name string) resource.TestCheckFunc {
129+
return func(s *terraform.State) error {
130+
rs, ok := s.RootModule().Resources[name]
131+
if !ok {
132+
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_NOT_FOUND: "%s"`, name)
133+
}
134+
if rs.Primary.ID == "" {
135+
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_ID_NOT_SET`)
136+
}
137+
meta := testAccProvider.Meta()
138+
clientSet, err := getClientSet(getFactoryFromMeta(meta))
139+
if err != nil {
140+
return err
141+
}
142+
organizationCluster := strings.Split(rs.Primary.ID, "/")
143+
serviceAccountBinding, err := clientSet.CloudV1alpha1().
144+
ServiceAccountBindings(organizationCluster[0]).
145+
Get(context.Background(), organizationCluster[1], metav1.GetOptions{})
146+
if err != nil {
147+
return err
148+
}
149+
length := len(serviceAccountBinding.Status.Conditions)
150+
// the IAM
151+
if serviceAccountBinding.Status.Conditions[0].Type != "IAMAccountReady" || serviceAccountBinding.Status.Conditions[0].Status != "True" ||
152+
serviceAccountBinding.Status.Conditions[length-1].Type != "Ready" || serviceAccountBinding.Status.Conditions[length-1].Status != "True" {
153+
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_NOT_READY: "%s"`, rs.Primary.ID)
154+
}
155+
return nil
156+
}
157+
}
158+
159+
func testResourceDataSourceServiceAccountBinding(organization, name, poolMemberName, poolMemberNamespace string) string {
160+
return fmt.Sprintf(`
161+
provider "streamnative" {
162+
}
163+
164+
resource "streamnative_service_account" "test-service-account" {
165+
organization = "%s"
166+
name = "%s"
167+
admin = %t
168+
}
169+
170+
resource "streamnative_service_account_binding" "test-service-account-binding" {
171+
organization = "%s"
172+
service_account_name = streamnative_service_account.test-service-account.name
173+
pool_member_name = "%s"
174+
pool_member_namespace = "%s"
175+
enable_iam_account_creation = true
176+
}
177+
178+
data "streamnative_service_account_binding" "test-service-account-binding" {
179+
depends_on = [streamnative_service_account_binding.test-service-account-binding]
180+
organization = streamnative_service_account_binding.test-service-account-binding.organization
181+
name = streamnative_service_account_binding.test-service-account-binding.name
182+
}
183+
184+
`, organization, name, true, organization, poolMemberName, poolMemberNamespace)
185+
}

docs/data-sources/service_account_binding.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ description: |-
2222

2323
### Read-Only
2424

25+
- `aws_assume_role_arns` (List of String) A list of AWS IAM role ARNs which can be assumed by the AWS IAM role created for the service account binding
26+
- `enable_iam_account_creation` (Boolean) Whether to create an IAM account for the service account binding
2527
- `id` (String) The ID of this resource.
2628
- `pool_member_name` (String) The infrastructure pool member name
2729
- `pool_member_namespace` (String) The infrastructure pool member namespace

docs/resources/service_account_binding.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,9 @@ description: |-
2222

2323
### Optional
2424

25+
- `aws_assume_role_arns` (List of String) A list of AWS IAM role ARNs which can be assumed by the AWS IAM role created for the service account binding
2526
- `cluster_name` (String) The pulsar cluster name
27+
- `enable_iam_account_creation` (Boolean) Whether to create an IAM account for the service account binding
2628
- `pool_member_name` (String) The infrastructure pool member name
2729
- `pool_member_namespace` (String) The infrastructure pool member namespace
2830

0 commit comments

Comments
 (0)