Skip to content

Commit

Permalink
add sso_connection resource
Browse files Browse the repository at this point in the history
  • Loading branch information
davex98 committed Nov 7, 2023
1 parent cd2621d commit 6555c26
Show file tree
Hide file tree
Showing 17 changed files with 2,067 additions and 13 deletions.
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ init-examples:

generate-sdk:
@echo "==> Generating castai sdk client"
@API_TAGS=ExternalClusterAPI,PoliciesAPI,NodeConfigurationAPI,NodeTemplatesAPI,AuthTokenAPI,ScheduledRebalancingAPI,InventoryAPI,UsersAPI,OperationsAPI,EvictorAPI go generate castai/sdk/generate.go
@API_TAGS=ExternalClusterAPI,PoliciesAPI,NodeConfigurationAPI,NodeTemplatesAPI,AuthTokenAPI,ScheduledRebalancingAPI,InventoryAPI,UsersAPI,OperationsAPI,EvictorAPI,SSOAPI go generate castai/sdk/generate.go

# The following command also rewrites existing documentation
generate-docs:
Expand Down Expand Up @@ -48,4 +48,4 @@ validate-terraform-examples:
terraform validate; \
cd -; \
done \
done
done
1 change: 1 addition & 0 deletions castai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ func Provider(version string) *schema.Provider {
"castai_eks_user_arn": resourceEKSClusterUserARN(),
"castai_reservations": resourceReservations(),
"castai_organization_members": resourceOrganizationMembers(),
"castai_sso_connection": resourceSSOConnection(),
},

DataSourcesMap: map[string]*schema.Resource{
Expand Down
309 changes: 309 additions & 0 deletions castai/resource_sso_connection.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,309 @@
package castai

import (
"context"
"encoding/base64"
"errors"
"fmt"
"time"

"github.com/castai/terraform-provider-castai/castai/sdk"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation"
"golang.org/x/crypto/bcrypt"
)

const (
FieldSSOConnectionName = "name"
FieldSSOConnectionEmailDomain = "email_domain"

FieldSSOConnectionAAD = "aad"
FieldSSOConnectionADDomain = "ad_domain"
FieldSSOConnectionADClientID = "client_id"
FieldSSOConnectionADClientSecret = "client_secret"

FieldSSOConnectionOkta = "okta"
FieldSSOConnectionOktaDomain = "okta_domain"
FieldSSOConnectionOktaClientID = "client_id"
FieldSSOConnectionOktaClientSecret = "client_secret"
)

func resourceSSOConnection() *schema.Resource {
return &schema.Resource{
CreateContext: resourceCastaiSSOConnectionCreate,
ReadContext: resourceCastaiSSOConnectionRead,
UpdateContext: resourceCastaiSSOConnectionUpdate,
DeleteContext: resourceCastaiSSOConnectionDelete,
CustomizeDiff: resourceCastaiSSOConnectionDiff,
Description: "SSO Connection resource allows creating SSO trust relationship with CAST AI.",
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{
FieldSSOConnectionName: {
Type: schema.TypeString,
Required: true,
Description: "Connection name",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionEmailDomain: {
Type: schema.TypeString,
Required: true,
Description: "Email domain of the connection",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionAAD: {
Type: schema.TypeList,
MaxItems: 1,
MinItems: 1,
Optional: true,
Description: "Azure AD connector",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
FieldSSOConnectionADDomain: {
Type: schema.TypeString,
Required: true,
Description: "Azure AD domain",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionADClientID: {
Type: schema.TypeString,
Required: true,
Description: "Azure AD client ID",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionADClientSecret: {
Type: schema.TypeString,
Sensitive: true,
Required: true,
Description: "Azure AD client secret",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
DiffSuppressFunc: func(_, oldValue, newValue string, _ *schema.ResourceData) bool {
decodedSecret, err := base64.StdEncoding.DecodeString(oldValue)
if err != nil {
return false
}
return bcrypt.CompareHashAndPassword(decodedSecret, []byte(newValue)) == nil
},
},
},
},
},
FieldSSOConnectionOkta: {
Type: schema.TypeList,
MaxItems: 1,
MinItems: 1,
Optional: true,
Description: "Okta connector",
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
FieldSSOConnectionOktaDomain: {
Type: schema.TypeString,
Required: true,
Description: "Okta domain",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionOktaClientID: {
Type: schema.TypeString,
Required: true,
Description: "Okta client ID",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
},
FieldSSOConnectionOktaClientSecret: {
Type: schema.TypeString,
Required: true,
Sensitive: true,
Description: "Okta client secret",
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
DiffSuppressFunc: func(_, oldValue, newValue string, _ *schema.ResourceData) bool {
decodedSecret, err := base64.StdEncoding.DecodeString(oldValue)
if err != nil {
return false
}
return bcrypt.CompareHashAndPassword(decodedSecret, []byte(newValue)) == nil
},
},
},
},
},
},
}
}

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

req := sdk.CastaiSsoV1beta1CreateSSOConnection{
Name: data.Get(FieldSSOConnectionName).(string),
EmailDomain: data.Get(FieldSSOConnectionEmailDomain).(string),
}

if v, ok := data.Get(FieldSSOConnectionAAD).([]any); ok && len(v) > 0 {
req.Aad = toADConnector(v[0].(map[string]any))
}

if v, ok := data.Get(FieldSSOConnectionOkta).([]any); ok && len(v) > 0 {
req.Okta = toOktaConnector(v[0].(map[string]any))
}

resp, err := client.SSOAPICreateSSOConnectionWithResponse(ctx, req)
if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("creating sso connection: %v", err)
}

if err := checkSSOStatus(resp.JSON200); err != nil {
return diag.FromErr(err)
}

data.SetId(*resp.JSON200.Id)

return resourceCastaiSSOConnectionRead(ctx, data, meta)
}

func resourceCastaiSSOConnectionRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
if data.Id() == "" {
return nil
}

client := meta.(*ProviderConfig).api
resp, err := client.SSOAPIGetSSOConnectionWithResponse(ctx, data.Id())
if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("retrieving sso connection: %v", err)
}

connection := resp.JSON200

if err := data.Set(FieldSSOConnectionName, connection.Name); err != nil {
return diag.Errorf("setting connection name: %v", err)
}
if err := data.Set(FieldSSOConnectionEmailDomain, connection.EmailDomain); err != nil {
return diag.Errorf("setting email domain: %v", err)
}

return nil
}

func resourceCastaiSSOConnectionUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
if !data.HasChanges(
FieldSSOConnectionName,
FieldSSOConnectionEmailDomain,
FieldSSOConnectionAAD,
FieldSSOConnectionOkta,
) {
return nil
}

client := meta.(*ProviderConfig).api
req := sdk.CastaiSsoV1beta1UpdateSSOConnection{}

if v, ok := data.GetOk(FieldSSOConnectionName); ok {
req.Name = toPtr(v.(string))
}
if v, ok := data.GetOk(FieldSSOConnectionEmailDomain); ok {
req.EmailDomain = toPtr(v.(string))
}

if v, ok := data.Get(FieldSSOConnectionAAD).([]any); ok && len(v) > 0 {
req.Aad = toADConnector(v[0].(map[string]any))
}

if v, ok := data.Get(FieldSSOConnectionOkta).([]any); ok && len(v) > 0 {
req.Okta = toOktaConnector(v[0].(map[string]any))
}

resp, err := client.SSOAPIUpdateSSOConnectionWithResponse(ctx, data.Id(), req)
if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("updating sso connection: %v", err)
}

if err := checkSSOStatus(resp.JSON200); err != nil {
return diag.FromErr(err)
}

return resourceCastaiSSOConnectionRead(ctx, data, meta)
}

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

resp, err := client.SSOAPIDeleteSSOConnectionWithResponse(ctx, data.Id())
if err := sdk.CheckOKResponse(resp, err); err != nil {
return diag.Errorf("deleting sso connection: %v", err)
}

return nil
}

func checkSSOStatus(input *sdk.CastaiSsoV1beta1SSOConnection) error {
if input == nil && input.Status == nil {
return nil
}

if *input.Status == sdk.STATUSACTIVE {
return nil
}

if input.Error == nil {
return fmt.Errorf("invalid SSO connection status: %s", *input.Status)
}

return fmt.Errorf("SSO connection status: %s failed with error: %s", *input.Status, *input.Error)
}

func resourceCastaiSSOConnectionDiff(_ context.Context, rd *schema.ResourceDiff, _ interface{}) error {
connectors := 0
if v, ok := rd.Get(FieldSSOConnectionAAD).([]any); ok && len(v) > 0 {
connectors++
}

if v, ok := rd.Get(FieldSSOConnectionOkta).([]any); ok && len(v) > 0 {
connectors++
}

if connectors != 1 {
return errors.New("only 1 connector can be configured")
}

return nil
}

func toADConnector(obj map[string]any) *sdk.CastaiSsoV1beta1AzureAAD {
if obj == nil {
return nil
}

out := &sdk.CastaiSsoV1beta1AzureAAD{}
if v, ok := obj[FieldSSOConnectionADDomain].(string); ok {
out.AdDomain = v
}
if v, ok := obj[FieldSSOConnectionADClientID].(string); ok {
out.ClientId = v
}
if v, ok := obj[FieldSSOConnectionADClientSecret].(string); ok {
out.ClientSecret = toPtr(v)
}

return out
}

func toOktaConnector(obj map[string]any) *sdk.CastaiSsoV1beta1Okta {
if obj == nil {
return nil
}

out := &sdk.CastaiSsoV1beta1Okta{}
if v, ok := obj[FieldSSOConnectionOktaDomain].(string); ok {
out.OktaDomain = v
}
if v, ok := obj[FieldSSOConnectionOktaClientID].(string); ok {
out.ClientId = v
}
if v, ok := obj[FieldSSOConnectionOktaClientSecret].(string); ok {
out.ClientSecret = toPtr(v)
}

return out
}
Loading

0 comments on commit 6555c26

Please sign in to comment.