Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIRE-839 add service account resource #430

Merged
merged 16 commits into from
Dec 12, 2024
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
SHELL := /bin/bash

export API_TAGS ?= ExternalClusterAPI,PoliciesAPI,NodeConfigurationAPI,NodeTemplatesAPI,AuthTokenAPI,ScheduledRebalancingAPI,InventoryAPI,UsersAPI,OperationsAPI,EvictorAPI,SSOAPI,CommitmentsAPI,WorkloadOptimizationAPI,RbacServiceAPI
export API_TAGS ?= ExternalClusterAPI,PoliciesAPI,NodeConfigurationAPI,NodeTemplatesAPI,AuthTokenAPI,ScheduledRebalancingAPI,InventoryAPI,UsersAPI,OperationsAPI,EvictorAPI,SSOAPI,CommitmentsAPI,WorkloadOptimizationAPI,ServiceAccountsAPI,RbacServiceAPI
export SWAGGER_LOCATION ?= https://api.cast.ai/v1/spec/openapi.json

default: build
Expand Down
1 change: 1 addition & 0 deletions castai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ func Provider(version string) *schema.Provider {
"castai_commitments": resourceCommitments(),
"castai_organization_members": resourceOrganizationMembers(),
"castai_sso_connection": resourceSSOConnection(),
"castai_service_account": resourceServiceAccount(),
"castai_workload_scaling_policy": resourceWorkloadScalingPolicy(),
"castai_organization_group": resourceOrganizationGroup(),
"castai_role_bindings": resourceRoleBindings(),
Expand Down
280 changes: 280 additions & 0 deletions castai/resource_service_account.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
package castai

import (
"context"
"fmt"
"net/http"
"time"

"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"

"github.com/castai/terraform-provider-castai/castai/sdk"
)

const (
FieldServiceAccountOrganizationID = "organization_id"
FieldServiceAccountName = "name"
FieldServiceAccountID = "service_account_id"
FieldServiceAccountDescription = "description"
FieldServiceAccountEmail = "email"

FieldServiceAccountAuthor = "author"
FieldServiceAccountAuthorID = "id"
FieldServiceAccountAuthorEmail = "email"
FieldServiceAccountAuthorKind = "kind"
)

func resourceServiceAccount() *schema.Resource {
return &schema.Resource{
CreateContext: resourceServiceAccountCreate,
ReadContext: resourceServiceAccountRead,
UpdateContext: resourceServiceAccountUpdate,
DeleteContext: resourceServiceAccountDelete,

Description: "Service Account resource allows managing CAST AI service accounts.",
Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(3 * time.Minute),
Update: schema.DefaultTimeout(3 * time.Minute),
Delete: schema.DefaultTimeout(3 * time.Minute),
},

Schema: map[string]*schema.Schema{
FieldServiceAccountOrganizationID: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
Description: "ID of the organization.",
},
FieldServiceAccountName: {
Type: schema.TypeString,
Required: true,
Description: "Name of the service account.",
},
FieldServiceAccountDescription: {
Type: schema.TypeString,
Optional: true,
Description: "Description of the service account.",
},
FieldServiceAccountEmail: {
Type: schema.TypeString,
Computed: true,
Description: "Email of the service account.",
},
FieldServiceAccountAuthor: {
Type: schema.TypeList,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
FieldServiceAccountAuthorID: {Type: schema.TypeString, Computed: true},
FieldServiceAccountAuthorEmail: {Type: schema.TypeString, Computed: true},
FieldServiceAccountAuthorKind: {Type: schema.TypeString, Computed: true},
},
},
Computed: true,
Description: "Author of the service account.",
},
},
}
}

func resourceServiceAccountRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*ProviderConfig).api

if data.Id() == "" {
return diag.Errorf("service account ID is not set")
}

organizationID, err := getOrganizationID(ctx, data, meta)
if err != nil {
return diag.FromErr(err)
}

tflog.Info(ctx, "reading service account", map[string]interface{}{
"resource_id": data.Id(),
"organization_id": organizationID,
})

resp, err := client.ServiceAccountsAPIGetServiceAccountWithResponse(ctx, organizationID, data.Id())
if resp.StatusCode() == http.StatusNotFound {
tflog.Warn(ctx, "resource is not found, removing from state", map[string]interface{}{
"resource_id": data.Id(),
"organization_id": organizationID,
})
data.SetId("") // Mark resource as deleted
return nil
}
if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("getting service account: %v", err)
}

tflog.Info(ctx, "found service account", map[string]interface{}{
"resource_id": data.Id(),
"organization_id": organizationID,
})
serviceAccount := resp.JSON200

if err := data.Set(FieldServiceAccountName, serviceAccount.ServiceAccount.Name); err != nil {
return diag.Errorf("setting service account name: %v", err)
}

if err := data.Set(FieldServiceAccountEmail, serviceAccount.ServiceAccount.Email); err != nil {
return diag.Errorf("setting service account email: %v", err)
}

if err := data.Set(FieldServiceAccountDescription, serviceAccount.ServiceAccount.Description); err != nil {
return diag.Errorf("setting service account description: %v", err)
}

authorData := flattenServiceAccountAuthor(serviceAccount.ServiceAccount.Author)
if err := data.Set(FieldServiceAccountAuthor, authorData); err != nil {
return diag.Errorf("setting service account author: %v", err)
}
return nil
}

func resourceServiceAccountCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*ProviderConfig).api

organizationID, err := getOrganizationID(ctx, data, meta)
if err != nil {
return diag.FromErr(err)
}

name := data.Get(FieldServiceAccountName).(string)
description := data.Get(FieldServiceAccountDescription).(string)

tflog.Info(ctx, "creating service account", map[string]interface{}{
"name": name,
"description": description,
"organization_id": organizationID,
})

resp, err := client.ServiceAccountsAPICreateServiceAccountWithResponse(ctx, organizationID, sdk.CastaiServiceaccountsV1beta1CreateServiceAccountRequestServiceAccount{
Name: name,
Description: &description,
},
)

if err := sdk.CheckResponseCreated(resp, err); err != nil {
return diag.Errorf("creating service account: %v", err)
}

tflog.Info(ctx, "created service account", map[string]interface{}{
"resource_id": *resp.JSON201.Id,
"organization_id": organizationID,
})
data.SetId(*resp.JSON201.Id)

return resourceServiceAccountRead(ctx, data, meta)
}

func resourceServiceAccountUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*ProviderConfig).api

organizationID, err := getOrganizationID(ctx, data, meta)
if err != nil {
return diag.FromErr(err)
}

serviceAccountID := data.Id()
name := data.Get(FieldServiceAccountName).(string)
description := data.Get(FieldServiceAccountDescription).(string)

tflog.Info(ctx, "updating service account", map[string]interface{}{
"resource_id": serviceAccountID,
"name": name,
"description": description,
"organization_id": organizationID,
})

resp, err := client.ServiceAccountsAPIUpdateServiceAccountWithResponse(
ctx,
organizationID,
serviceAccountID,
sdk.ServiceAccountsAPIUpdateServiceAccountRequest{
ServiceAccount: sdk.CastaiServiceaccountsV1beta1UpdateServiceAccountRequestServiceAccount{
Name: name,
Description: &description,
},
},
)

if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("updating service account: %v", err)
}

tflog.Info(ctx, "created service account", map[string]interface{}{
"resource_id": serviceAccountID,
"organization_id": organizationID,
"name": name,
"description": description,
})

return resourceServiceAccountRead(ctx, data, meta)
}

func resourceServiceAccountDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*ProviderConfig).api
organizationID, err := getOrganizationID(ctx, data, meta)
if err != nil {
return diag.FromErr(err)
}
serviceAccountID := data.Id()

tflog.Info(ctx, "deleting service account", map[string]interface{}{
"resource_id": serviceAccountID,
"organization_id": organizationID,
})

resp, err := client.ServiceAccountsAPIDeleteServiceAccount(ctx, organizationID, serviceAccountID)
if err != nil {
return diag.Errorf("deleting service account: %v", err)
}
if resp.StatusCode != http.StatusNoContent {
return diag.Errorf("deleteting service account: expected status: [204], received status: [%d]", resp.StatusCode)
}

tflog.Info(ctx, "deleted service account", map[string]interface{}{
"resource_id": serviceAccountID,
"organization_id": organizationID,
})

return nil
}

func flattenServiceAccountAuthor(author *sdk.CastaiServiceaccountsV1beta1ServiceAccountAuthor) []map[string]interface{} {
if author == nil {
return []map[string]interface{}{}
}

return []map[string]interface{}{
{
FieldServiceAccountAuthorID: stringValue(author.Id),
FieldServiceAccountAuthorEmail: stringValue(author.Email),
FieldServiceAccountAuthorKind: stringValue(author.Kind),
},
}
}

func stringValue(value *string) string {
if value == nil {
return ""
}
return *value
}

func getOrganizationID(ctx context.Context, data *schema.ResourceData, meta interface{}) (string, error) {
var organizationID string
var err error

organizationID = data.Get(FieldServiceAccountOrganizationID).(string)
if organizationID == "" {
organizationID, err = getDefaultOrganizationId(ctx, meta)
if err != nil {
return "", fmt.Errorf("getting organization ID: %w", err)
}
}

return organizationID, nil
}
Loading
Loading