Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion cloud/data_source_service_account_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ package cloud
import (
"context"
"fmt"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"strings"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -69,6 +69,19 @@ func dataSourceServiceAccountBinding() *schema.Resource {
Description: descriptions["pool_member_namespace"],
Computed: true,
},
"enable_iam_account_creation": {
Type: schema.TypeBool,
Description: descriptions["enable_iam_account_creation"],
Computed: true,
},
"aws_assume_role_arns": {
Type: schema.TypeList,
Description: descriptions["aws_assume_role_arns"],
Elem: &schema.Schema{
Type: schema.TypeString,
},
Computed: true,
},
},
}
}
Expand All @@ -93,6 +106,8 @@ func DataSourceServiceAccountBindingRead(ctx context.Context, d *schema.Resource
_ = d.Set("service_account_name", serviceAccountBinding.Spec.ServiceAccountName)
_ = d.Set("pool_member_name", serviceAccountBinding.Spec.PoolMemberRef.Name)
_ = d.Set("pool_member_namespace", serviceAccountBinding.Spec.PoolMemberRef.Namespace)
_ = d.Set("enable_iam_account_creation", serviceAccountBinding.Spec.EnableIAMAccountCreation)
_ = d.Set("aws_assume_role_arns", flattenStringSlice(serviceAccountBinding.Spec.AWSAssumeRoleARNs))
d.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))

return nil
Expand Down
2 changes: 2 additions & 0 deletions cloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ func init() {
"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.",
"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",
"customized_metadata": "The custom metadata in the api key token",
"enable_iam_account_creation": "Whether to create an IAM account for the service account binding",
"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",
}
}

Expand Down
26 changes: 24 additions & 2 deletions cloud/resource_service_account_binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,11 @@ import (
"strings"
"time"

apierrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/streamnative/cloud-api-server/pkg/apis/cloud/v1alpha1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

Expand Down Expand Up @@ -99,6 +98,19 @@ func resourceServiceAccountBinding() *schema.Resource {
Computed: true,
Optional: true,
},
"enable_iam_account_creation": {
Type: schema.TypeBool,
Description: descriptions["enable_iam_account_creation"],
Optional: true,
},
"aws_assume_role_arns": {
Type: schema.TypeList,
Description: descriptions["aws_assume_role_arns"],
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}
Expand All @@ -113,6 +125,12 @@ func resourceServiceAccountBindingCreate(ctx context.Context, d *schema.Resource
return diag.FromErr(fmt.Errorf("ERROR_CREATE_SERVICE_ACCOUNT_BINDING: " +
"either (pool_member_name & pool_member_namespace) or cluster_name must be provided"))
}
enableIAMAccountCreation := d.Get("enable_iam_account_creation").(bool)
awsAssumeRoleARNRawList := d.Get("aws_assume_role_arns").([]interface{})
awsAssumeRoleARNs := make([]string, len(awsAssumeRoleARNRawList))
for i, v := range awsAssumeRoleARNRawList {
awsAssumeRoleARNs[i] = v.(string)
}

clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
Expand Down Expand Up @@ -145,6 +163,8 @@ func resourceServiceAccountBindingCreate(ctx context.Context, d *schema.Resource
Name: poolMemberName,
Namespace: poolMemberNamespace,
},
EnableIAMAccountCreation: enableIAMAccountCreation,
AWSAssumeRoleARNs: awsAssumeRoleARNs,
},
}
serviceAccountBinding, err := clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Create(ctx, sab, metav1.CreateOptions{
Expand Down Expand Up @@ -188,6 +208,8 @@ func resourceServiceAccountBindingRead(ctx context.Context, d *schema.ResourceDa
_ = d.Set("service_account_name", serviceAccountBinding.Spec.ServiceAccountName)
_ = d.Set("pool_member_name", serviceAccountBinding.Spec.PoolMemberRef.Name)
_ = d.Set("pool_member_namespace", serviceAccountBinding.Spec.PoolMemberRef.Namespace)
_ = d.Set("enable_iam_account_creation", serviceAccountBinding.Spec.EnableIAMAccountCreation)
_ = d.Set("aws_assume_role_arns", flattenStringSlice(serviceAccountBinding.Spec.AWSAssumeRoleARNs))
d.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))

return nil
Expand Down
185 changes: 185 additions & 0 deletions cloud/service_account_binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
// Copyright 2024 StreamNative, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package cloud

import (
"context"
"fmt"
"math/rand"
"strings"
"testing"
"time"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

var saGeneratedName = fmt.Sprintf("t-%d-%d", rand.Intn(1000), rand.Intn(100))

func TestServiceAccountBinding(t *testing.T) {
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProviderFactories: testAccProviderFactories,
CheckDestroy: testCheckServiceAccountBindingDestroy,
Steps: []resource.TestStep{
{
Config: testResourceDataSourceServiceAccountBinding(
"sndev",
saGeneratedName,
"gcp-shared-usc1-test",
"streamnative"),
Check: resource.ComposeTestCheckFunc(
testCheckServiceAccountBindingExists("streamnative_service_account_binding.test-service-account-binding"),
),
},
},
})
}

func TestServiceAccountBindingRemovedExternally(t *testing.T) {
// This test case is to simulate the situation that the service account binding is removed externally
// and the terraform state still has the resource
resource.Test(t, resource.TestCase{
PreCheck: func() {
testAccPreCheck(t)
},
ProviderFactories: testAccProviderFactories,
CheckDestroy: testCheckServiceAccountBindingDestroy,
Steps: []resource.TestStep{
{
Config: testResourceDataSourceServiceAccountBinding(
"sndev",
saGeneratedName,
"gcp-shared-usc1-test",
"streamnative"),
Check: resource.ComposeTestCheckFunc(
testCheckServiceAccountBindingExists("streamnative_service_account_binding.test-service-account-binding"),
),
},
{
PreConfig: func() {
meta := testAccProvider.Meta()
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
t.Fatal(err)
}
err = clientSet.CloudV1alpha1().
ServiceAccountBindings("sndev").
Delete(context.Background(), saGeneratedName+".streamnative.gcp-shared-usc1-test", metav1.DeleteOptions{})
if err != nil {
t.Fatal(err)
}
},
Config: testResourceDataSourceServiceAccountBinding(
"sndev",
saGeneratedName,
"gcp-shared-usc1-test",
"streamnative"),
PlanOnly: true,
ExpectNonEmptyPlan: true,
},
},
})
}

func testCheckServiceAccountBindingDestroy(s *terraform.State) error {
// Add a sleep for wait the service account binding to be deleted
time.Sleep(5 * time.Second)
for _, rs := range s.RootModule().Resources {
if rs.Type != "streamnative_service_account_binding" {
continue
}
meta := testAccProvider.Meta()
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return err
}
organizationServiceAccountBinding := strings.Split(rs.Primary.ID, "/")
_, err = clientSet.CloudV1alpha1().
ServiceAccountBindings(organizationServiceAccountBinding[0]).
Get(context.Background(), organizationServiceAccountBinding[1], metav1.GetOptions{})
if err != nil {
if errors.IsNotFound(err) {
return nil
}
return err
}
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_STILL_EXISTS: "%s"`, rs.Primary.ID)
}
return nil
}

func testCheckServiceAccountBindingExists(name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_NOT_FOUND: "%s"`, name)
}
if rs.Primary.ID == "" {
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_ID_NOT_SET`)
}
meta := testAccProvider.Meta()
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return err
}
organizationCluster := strings.Split(rs.Primary.ID, "/")
serviceAccountBinding, err := clientSet.CloudV1alpha1().
ServiceAccountBindings(organizationCluster[0]).
Get(context.Background(), organizationCluster[1], metav1.GetOptions{})
if err != nil {
return err
}
length := len(serviceAccountBinding.Status.Conditions)
// the IAM
if serviceAccountBinding.Status.Conditions[0].Type != "IAMAccountReady" || serviceAccountBinding.Status.Conditions[0].Status != "True" ||
serviceAccountBinding.Status.Conditions[length-1].Type != "Ready" || serviceAccountBinding.Status.Conditions[length-1].Status != "True" {
return fmt.Errorf(`ERROR_RESOURCE_SERVICE_ACCOUNT_BINDING_NOT_READY: "%s"`, rs.Primary.ID)
}
return nil
}
}

func testResourceDataSourceServiceAccountBinding(organization, name, poolMemberName, poolMemberNamespace string) string {
return fmt.Sprintf(`
provider "streamnative" {
}

resource "streamnative_service_account" "test-service-account" {
organization = "%s"
name = "%s"
admin = %t
}

resource "streamnative_service_account_binding" "test-service-account-binding" {
organization = "%s"
service_account_name = streamnative_service_account.test-service-account.name
pool_member_name = "%s"
pool_member_namespace = "%s"
enable_iam_account_creation = true
}

data "streamnative_service_account_binding" "test-service-account-binding" {
depends_on = [streamnative_service_account_binding.test-service-account-binding]
organization = streamnative_service_account_binding.test-service-account-binding.organization
name = streamnative_service_account_binding.test-service-account-binding.name
}

`, organization, name, true, organization, poolMemberName, poolMemberNamespace)
}
2 changes: 2 additions & 0 deletions docs/data-sources/service_account_binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ description: |-

### Read-Only

- `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
- `enable_iam_account_creation` (Boolean) Whether to create an IAM account for the service account binding
- `id` (String) The ID of this resource.
- `pool_member_name` (String) The infrastructure pool member name
- `pool_member_namespace` (String) The infrastructure pool member namespace
Expand Down
2 changes: 2 additions & 0 deletions docs/resources/service_account_binding.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,9 @@ description: |-

### Optional

- `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
- `cluster_name` (String) The pulsar cluster name
- `enable_iam_account_creation` (Boolean) Whether to create an IAM account for the service account binding
- `pool_member_name` (String) The infrastructure pool member name
- `pool_member_namespace` (String) The infrastructure pool member namespace

Expand Down
Loading