Skip to content

Commit

Permalink
Add ServiceAccountBinding resource (#39)
Browse files Browse the repository at this point in the history
Co-authored-by: Guangning E <[email protected]>
  • Loading branch information
jiangpengcheng and tuteng authored May 14, 2024
1 parent 3d3cf80 commit 3d5b9fa
Show file tree
Hide file tree
Showing 6 changed files with 408 additions and 22 deletions.
94 changes: 94 additions & 0 deletions cloud/data_source_service_account_binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// 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"
"strings"

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

func dataSourceServiceAccountBinding() *schema.Resource {
return &schema.Resource{
ReadContext: DataSourceServiceAccountBindingRead,
Importer: &schema.ResourceImporter{
StateContext: func(
ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
organizationServiceAccount := strings.Split(d.Id(), "/")
_ = d.Set("organization", organizationServiceAccount[0])
_ = d.Set("name", organizationServiceAccount[1])
err := resourceServiceAccountBindingRead(ctx, d, meta)
if err.HasError() {
return nil, fmt.Errorf("import %q: %s", d.Id(), err[0].Summary)
}
return []*schema.ResourceData{d}, nil
},
},
Schema: map[string]*schema.Schema{
"organization": {
Type: schema.TypeString,
Required: true,
Description: descriptions["organization"],
ValidateFunc: validateNotBlank,
},
"name": {
Type: schema.TypeString,
Required: true,
Description: descriptions["service_account_binding_name"],
ValidateFunc: validateNotBlank,
},
"service_account_name": {
Type: schema.TypeString,
Computed: true,
Description: descriptions["service_account_name"],
},
"pool_member_name": {
Type: schema.TypeString,
Description: descriptions["pool_member_name"],
Computed: true,
},
"pool_member_namespace": {
Type: schema.TypeString,
Description: descriptions["pool_member_namespace"],
Computed: true,
},
},
}
}

func DataSourceServiceAccountBindingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
namespace := d.Get("organization").(string)
name := d.Get("name").(string)
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_INIT_CLIENT_ON_READ_SERVICE_ACCOUNT_BINDING: %w", err))
}
serviceAccountBinding, err := clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_READ_SERVICE_ACCOUNT_BINDING: %w", err))
}
_ = d.Set("name", serviceAccountBinding.Name)
_ = d.Set("organization", serviceAccountBinding.Namespace)
_ = 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.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))

return nil
}
49 changes: 27 additions & 22 deletions cloud/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,16 +50,19 @@ var descriptions map[string]string

func init() {
descriptions = map[string]string{
"key_file_path": "The path of the private key file",
"organization": "The organization name",
"service_account_name": "The service account name",
"cluster_name": "The pulsar cluster name",
"admin": "Whether the service account is admin",
"private_key_data": "The private key data",
"availability-mode": "The availability mode, supporting 'zonal' and 'regional'",
"pool_name": "The infrastructure pool name to use, supported pool 'shared-aws', 'shared-gcp'",
"pool_namespace": "The infrastructure pool namespace to use, supported 'streamnative'",
"instance_name": "The pulsar instance name",
"key_file_path": "The path of the private key file",
"organization": "The organization name",
"service_account_name": "The service account name",
"service_account_binding_name": "The service account binding name",
"cluster_name": "The pulsar cluster name",
"admin": "Whether the service account is admin",
"private_key_data": "The private key data",
"availability-mode": "The availability mode, supporting 'zonal' and 'regional'",
"pool_name": "The infrastructure pool name to use, supported pool 'shared-aws', 'shared-gcp'",
"pool_namespace": "The infrastructure pool namespace to use, supported 'streamnative'",
"pool_member_name": "The infrastructure pool member name to use, can be got from the PulsarCluster resource",
"pool_member_namespace": "The infrastructure pool member namespace to use, supported 'streamnative'",
"instance_name": "The pulsar instance name",
"location": "The location of the pulsar cluster, " +
"supported location https://docs.streamnative.io/docs/cluster#cluster-location",
"release_channel": "The release channel of the pulsar cluster subscribe to, it must to be lts or rapid, default rapid",
Expand Down Expand Up @@ -127,20 +130,22 @@ func Provider() *schema.Provider {
},
},
ResourcesMap: map[string]*schema.Resource{
"streamnative_service_account": resourceServiceAccount(),
"streamnative_pulsar_instance": resourcePulsarInstance(),
"streamnative_pulsar_cluster": resourcePulsarCluster(),
"streamnative_cloud_connection": resourceCloudConnection(),
"streamnative_cloud_environment": resourceCloudEnvironment(),
"streamnative_apikey": resourceApiKey(),
"streamnative_service_account": resourceServiceAccount(),
"streamnative_service_account_binding": resourceServiceAccountBinding(),
"streamnative_pulsar_instance": resourcePulsarInstance(),
"streamnative_pulsar_cluster": resourcePulsarCluster(),
"streamnative_cloud_connection": resourceCloudConnection(),
"streamnative_cloud_environment": resourceCloudEnvironment(),
"streamnative_apikey": resourceApiKey(),
},
DataSourcesMap: map[string]*schema.Resource{
"streamnative_service_account": dataSourceServiceAccount(),
"streamnative_pulsar_instance": dataSourcePulsarInstance(),
"streamnative_pulsar_cluster": dataSourcePulsarCluster(),
"streamnative_cloud_connection": dataSourceCloudConnection(),
"streamnative_cloud_environment": dataSourceCloudEnvironment(),
"streamnative_apikey": dataSourceApiKey(),
"streamnative_service_account": dataSourceServiceAccount(),
"streamnative_service_account_binding": dataSourceServiceAccountBinding(),
"streamnative_pulsar_instance": dataSourcePulsarInstance(),
"streamnative_pulsar_cluster": dataSourcePulsarCluster(),
"streamnative_cloud_connection": dataSourceCloudConnection(),
"streamnative_cloud_environment": dataSourceCloudEnvironment(),
"streamnative_apikey": dataSourceApiKey(),
},
}
provider.ConfigureContextFunc = func(_ context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
Expand Down
184 changes: 184 additions & 0 deletions cloud/resource_service_account_binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
// 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"
"strings"
"time"

"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"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)

func resourceServiceAccountBinding() *schema.Resource {
return &schema.Resource{
CreateContext: resourceServiceAccountBindingCreate,
ReadContext: resourceServiceAccountBindingRead,
UpdateContext: resourceServiceAccountBindingUpdate,
DeleteContext: resourceServiceAccountBindingDelete,
CustomizeDiff: func(ctx context.Context, diff *schema.ResourceDiff, i interface{}) error {
oldOrg, _ := diff.GetChange("organization")
oldName, _ := diff.GetChange("name")
if oldOrg.(string) == "" && oldName.(string) == "" {
// This is create event, so we don't need to check the diff.
return nil
}
if diff.HasChange("name") ||
diff.HasChanges("organization") ||
diff.HasChanges("pool_member_name") ||
diff.HasChanges("pool_member_namespace") ||
diff.HasChanges("service_account_name") {
return fmt.Errorf("ERROR_UPDATE_SERVICE_ACCOUNT_BINDING: " +
"The service account binding does not support updates, please recreate it")
}
return nil
},
Importer: &schema.ResourceImporter{
StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
organizationServiceAccount := strings.Split(d.Id(), "/")
_ = d.Set("organization", organizationServiceAccount[0])
_ = d.Set("name", organizationServiceAccount[1])
err := resourceServiceAccountBindingRead(ctx, d, meta)
if err.HasError() {
return nil, fmt.Errorf("import %q: %s", d.Id(), err[0].Summary)
}
return []*schema.ResourceData{d}, nil
},
},
Schema: map[string]*schema.Schema{
"organization": {
Type: schema.TypeString,
Required: true,
Description: descriptions["organization"],
ValidateFunc: validateNotBlank,
},
"name": {
Type: schema.TypeString,
Computed: true,
Description: descriptions["service_account_binding_name"],
},
"service_account_name": {
Type: schema.TypeString,
Required: true,
Description: descriptions["service_account_name"],
ValidateFunc: validateNotBlank,
},
"pool_member_name": {
Type: schema.TypeString,
Description: descriptions["pool_member_name"],
Required: true,
},
"pool_member_namespace": {
Type: schema.TypeString,
Description: descriptions["pool_member_namespace"],
Required: true,
},
},
}
}

func resourceServiceAccountBindingCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
namespace := d.Get("organization").(string)
serviceAccountName := d.Get("service_account_name").(string)
poolMemberName := d.Get("pool_member_name").(string)
poolMemberNamespace := d.Get("pool_member_namespace").(string)
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_INIT_CLIENT_ON_CREATE_SERVICE_ACCOUNT_BINDING: %w", err))
}
name := fmt.Sprintf("%s.%s.%s", serviceAccountName, poolMemberNamespace, poolMemberName)
sab := &v1alpha1.ServiceAccountBinding{
TypeMeta: metav1.TypeMeta{
Kind: "ServiceAccount",
APIVersion: v1alpha1.SchemeGroupVersion.String(),
},
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Spec: v1alpha1.ServiceAccountBindingSpec{
ServiceAccountName: serviceAccountName,
PoolMemberRef: v1alpha1.PoolMemberReference{
Name: poolMemberName,
Namespace: poolMemberNamespace,
},
},
}
serviceAccountBinding, err := clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Create(ctx, sab, metav1.CreateOptions{
FieldManager: "terraform-create",
})
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_CREATE_SERVICE_ACCOUNT_BINDING: %w", err))
}
_ = d.Set("name", serviceAccountBinding.Name)
// Don't retry too frequently to avoid affecting the api-server.
err = retry.RetryContext(ctx, 5*time.Second, func() *retry.RetryError {
dia := resourceServiceAccountBindingRead(ctx, d, meta)
if dia.HasError() {
return retry.NonRetryableError(fmt.Errorf("ERROR_RETRY_CREATE_SERVICE_ACCOUNT_BINDING: %s", dia[0].Summary))
}
return nil
})
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_RETRY_CREATE_SERVICE_ACCOUNT_BINDING: %w", err))
}
return nil
}

func resourceServiceAccountBindingRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
namespace := d.Get("organization").(string)
name := d.Get("name").(string)
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_INIT_CLIENT_ON_READ_SERVICE_ACCOUNT_BINDING: %w", err))
}
serviceAccountBinding, err := clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Get(ctx, name, metav1.GetOptions{})
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_READ_SERVICE_ACCOUNT_BINDING: %w", err))
}
_ = d.Set("name", serviceAccountBinding.Name)
_ = d.Set("organization", serviceAccountBinding.Namespace)
_ = 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.SetId(fmt.Sprintf("%s/%s", serviceAccountBinding.Namespace, serviceAccountBinding.Name))

return nil
}

func resourceServiceAccountBindingDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
clientSet, err := getClientSet(getFactoryFromMeta(meta))
if err != nil {
return diag.FromErr(fmt.Errorf("ERROR_INIT_CLIENT_ON_DELETE_SERVICE_ACCOUNT_BINDING: %w", err))
}
namespace := d.Get("organization").(string)
name := d.Get("name").(string)
err = clientSet.CloudV1alpha1().ServiceAccountBindings(namespace).Delete(ctx, name, metav1.DeleteOptions{})
if err != nil {
return diag.FromErr(fmt.Errorf("DELETE_SERVICE_ACCOUNT_BINDING: %w", err))
}
_ = d.Set("name", "")
return nil
}

func resourceServiceAccountBindingUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
return diag.FromErr(fmt.Errorf("ERROR_UPDATE_SERVICE_ACCOUNT_BINDING: " +
"The service account binding does not support updates, please recreate it"))
}
28 changes: 28 additions & 0 deletions docs/data-sources/service_account_binding.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "streamnative_service_account_binding Data Source - terraform-provider-streamnative"
subcategory: ""
description: |-
---

# streamnative_service_account_binding (Data Source)





<!-- schema generated by tfplugindocs -->
## Schema

### Required

- `name` (String) The service account binding name
- `organization` (String) The organization name

### Read-Only

- `id` (String) The ID of this resource.
- `pool_member_name` (String) The infrastructure pool member name to use, can be got from the PulsarCluster resource
- `pool_member_namespace` (String) The infrastructure pool member namespace to use, supported 'streamnative'
- `service_account_name` (String) The service account name
Loading

0 comments on commit 3d5b9fa

Please sign in to comment.