Skip to content

Commit

Permalink
KUBE-576: Add impersonate cluster support (#386)
Browse files Browse the repository at this point in the history
Update terraform provider supporting SA impersonation
Add example gke sa impersonation.

Co-authored-by: gleb <gleb@cast.ai>
  • Loading branch information
2 people authored and sarvesh-cast committed Oct 15, 2024
1 parent c4de9a5 commit ca7e544
Show file tree
Hide file tree
Showing 17 changed files with 1,026 additions and 4 deletions.
1 change: 1 addition & 0 deletions castai/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ func Provider(version string) *schema.Provider {
"castai_eks_cluster": resourceEKSCluster(),
"castai_eks_clusterid": resourceEKSClusterID(),
"castai_gke_cluster": resourceGKECluster(),
"castai_gke_cluster_id": resourceGKEClusterId(),
"castai_aks_cluster": resourceAKSCluster(),
"castai_autoscaler": resourceAutoscaler(),
"castai_evictor_advanced_config": resourceEvictionConfig(),
Expand Down
4 changes: 0 additions & 4 deletions castai/resource_gke_cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -111,9 +111,6 @@ func resourceCastaiGKEClusterCreate(ctx context.Context, data *schema.ResourceDa
Location: &location,
ClusterName: toPtr(data.Get(FieldGKEClusterName).(string)),
}

log.Printf("[INFO] Registering new external cluster: %#v", req)

resp, err := client.ExternalClusterAPIRegisterClusterWithResponse(ctx, req)
if checkErr := sdk.CheckOKResponse(resp, err); checkErr != nil {
return diag.FromErr(checkErr)
Expand All @@ -128,7 +125,6 @@ func resourceCastaiGKEClusterCreate(ctx context.Context, data *schema.ResourceDa
return diag.FromErr(fmt.Errorf("setting cluster token: %w", err))
}
data.SetId(clusterID)

if err := updateGKEClusterSettings(ctx, data, client); err != nil {
return diag.FromErr(err)
}
Expand Down
188 changes: 188 additions & 0 deletions castai/resource_gke_cluster_id.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package castai

import (
"context"
"fmt"
"log"
"strings"
"time"

"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"

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

const (
FieldGKEClusterIdName = "name"
FieldGKEClusterIdProjectId = "project_id"
FieldGKEClusterIdLocation = "location"
FieldGKEClientSA = "client_service_account"
FieldGKECastSA = "cast_service_account"
)

func resourceGKEClusterId() *schema.Resource {
return &schema.Resource{
CreateContext: resourceCastaiGKEClusterIdCreate,
ReadContext: resourceCastaiGKEClusterIdRead,
UpdateContext: resourceCastaiGKEClusterIdUpdate,
DeleteContext: resourceCastaiGKEClusterIdDelete,
CustomizeDiff: clusterTokenDiff,
Description: "GKE cluster resource allows connecting an existing GKE cluster to CAST AI.",

Timeouts: &schema.ResourceTimeout{
Create: schema.DefaultTimeout(5 * time.Minute),
Update: schema.DefaultTimeout(1 * time.Minute),
Delete: schema.DefaultTimeout(6 * time.Minute), // Cluster action timeout is 5 minutes.
},

Schema: map[string]*schema.Schema{
FieldGKEClusterIdName: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
Description: "GKE cluster name",
},
FieldGKEClusterIdProjectId: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
Description: "GCP project id",
},
FieldGKEClusterIdLocation: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
ValidateDiagFunc: validation.ToDiagFunc(validation.StringIsNotWhiteSpace),
Description: "GCP cluster zone in case of zonal or region in case of regional cluster",
},
FieldClusterToken: {
Type: schema.TypeString,
Computed: true,
Sensitive: true,
Description: "CAST.AI agent cluster token",
},
FieldGKEClientSA: {
Type: schema.TypeString,
Optional: true,
Description: "Service account email in client project",
},
FieldGKECastSA: {
Type: schema.TypeString,
Optional: true,
Computed: true,
Description: "Service account email in cast project",
},
},
}
}

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

req := sdk.ExternalClusterAPIRegisterClusterJSONRequestBody{
Name: data.Get(FieldGKEClusterName).(string),
}

location := data.Get(FieldGKEClusterLocation).(string)
region := location
// Check if location is zone or location.
if strings.Count(location, "-") > 1 {
// region "europe-central2"
// zone "europe-central2-a"
regionParts := strings.Split(location, "-")
regionParts = regionParts[:2]
region = strings.Join(regionParts, "-")
}

req.Gke = &sdk.ExternalclusterV1GKEClusterParams{
ProjectId: toPtr(data.Get(FieldGKEClusterProjectId).(string)),
Region: &region,
Location: &location,
ClusterName: toPtr(data.Get(FieldGKEClusterName).(string)),
}

log.Printf("[INFO] Registering new external cluster: %#v", req)
resp, err := client.ExternalClusterAPIRegisterClusterWithResponse(ctx, req)
if checkErr := sdk.CheckOKResponse(resp, err); checkErr != nil {
return diag.FromErr(checkErr)
}

clusterID := *resp.JSON200.Id
tkn, err := createClusterToken(ctx, client, clusterID)
if err != nil {
return diag.FromErr(err)
}
if err := data.Set(FieldClusterToken, tkn); err != nil {
return diag.FromErr(fmt.Errorf("setting cluster token: %w", err))
}
data.SetId(clusterID)
// If client service account is set, create service account on cast side.
if len(data.Get(FieldGKEClientSA).(string)) > 0 {
resp, err := client.ExternalClusterAPIGKECreateSAWithResponse(ctx, data.Id(), sdk.ExternalClusterAPIGKECreateSARequest{
Gke: &sdk.ExternalclusterV1UpdateGKEClusterParams{
GkeSaImpersonate: toPtr(data.Get(FieldGKEClientSA).(string)),
ProjectId: toPtr(data.Get(FieldGKEClusterProjectId).(string)),
},
})
if err != nil {
return diag.FromErr(err)
}
if resp.JSON200 == nil || resp.JSON200.ServiceAccount == nil {
return diag.FromErr(fmt.Errorf("service account not returned"))
}
if err := data.Set(FieldGKECastSA, toString(resp.JSON200.ServiceAccount)); err != nil {
return diag.FromErr(fmt.Errorf("service account id: %w", err))
}
}
return nil
}

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

if data.Id() == "" {
log.Printf("[INFO] id is null not fetching anything.")
return nil
}

log.Printf("[INFO] Getting cluster information.")
resp, err := fetchClusterData(ctx, client, data.Id())
if err != nil {
return diag.FromErr(err)
}

if resp == nil {
data.SetId("")
return nil
}
if GKE := resp.JSON200.Gke; GKE != nil {
if err := data.Set(FieldGKEClusterProjectId, toString(GKE.ProjectId)); err != nil {
return diag.FromErr(fmt.Errorf("setting project id: %w", err))
}
if err := data.Set(FieldGKEClusterLocation, toString(GKE.Location)); err != nil {
return diag.FromErr(fmt.Errorf("setting location: %w", err))
}
if err := data.Set(FieldGKEClusterName, toString(GKE.ClusterName)); err != nil {
return diag.FromErr(fmt.Errorf("setting cluster name: %w", err))
}
if err := data.Set(FieldGKEClientSA, toString(GKE.ClientServiceAccount)); err != nil {
return diag.FromErr(fmt.Errorf("setting cluster client sa email: %w", err))
}
if err := data.Set(FieldGKECastSA, toString(GKE.CastServiceAccount)); err != nil {
return diag.FromErr(fmt.Errorf("setting cluster cast sa email: %w", err))
}
}
return nil
}

func resourceCastaiGKEClusterIdUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceCastaiGKEClusterIdRead(ctx, data, meta)
}

func resourceCastaiGKEClusterIdDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics {
return resourceCastaiClusterDelete(ctx, data, meta)
}
136 changes: 136 additions & 0 deletions castai/resource_gke_cluster_id_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
package castai

import (
"bytes"
"context"
"io"
"net/http"
"testing"

"github.com/golang/mock/gomock"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/require"

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

func TestGKEClusterIdResourceReadContext(t *testing.T) {
r := require.New(t)
mockctrl := gomock.NewController(t)
mockClient := mock_sdk.NewMockClientInterface(mockctrl)

ctx := context.Background()
provider := &ProviderConfig{
api: &sdk.ClientWithResponses{
ClientInterface: mockClient,
},
}

clusterId := "b6bfc074-a267-400f-b8f1-db0850c36gke"

body := io.NopCloser(bytes.NewReader([]byte(`{
"id": "b6bfc074-a267-400f-b8f1-db0850c36gk3",
"name": "gke-cluster",
"organizationId": "2836f775-aaaa-eeee-bbbb-3d3c29512GKE",
"credentialsId": "9b8d0456-177b-4a3d-b162-e68030d65GKE",
"createdAt": "2022-04-27T19:03:31.570829Z",
"region": {
"name": "eu-central-1",
"displayName": "EU (Frankfurt)"
},
"status": "ready",
"agentSnapshotReceivedAt": "2022-05-21T10:33:56.192020Z",
"agentStatus": "online",
"providerType": "gke",
"gke": {
"clusterName": "gke-cluster",
"region": "eu-central-1",
"location": "eu-central-1",
"projectId": "project-id",
"clientServiceAccount": "client-service-account",
"castServiceAccount": "cast-service-account"
},
"clusterNameId": "gke-cluster-b6bfc074"
}`)))
mockClient.EXPECT().
ExternalClusterAPIGetCluster(gomock.Any(), clusterId).
Return(&http.Response{StatusCode: 200, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)

resource := resourceGKEClusterId()

val := cty.ObjectVal(map[string]cty.Value{})
state := terraform.NewInstanceStateShimmedFromValue(val, 0)
state.ID = clusterId

data := resource.Data(state)
result := resource.ReadContext(ctx, data, provider)
r.Nil(result)
r.False(result.HasError())
r.Equal(`ID = b6bfc074-a267-400f-b8f1-db0850c36gke
cast_service_account = cast-service-account
client_service_account = client-service-account
location = eu-central-1
name = gke-cluster
project_id = project-id
Tainted = false
`, data.State().String())
}

func TestGKEClusterIdResourceReadContextArchived(t *testing.T) {
r := require.New(t)
mockctrl := gomock.NewController(t)
mockClient := mock_sdk.NewMockClientInterface(mockctrl)

ctx := context.Background()
provider := &ProviderConfig{
api: &sdk.ClientWithResponses{
ClientInterface: mockClient,
},
}

clusterId := "b6bfc074-a267-400f-b8f1-db0850c36gke"

body := io.NopCloser(bytes.NewReader([]byte(`{
"id": "b6bfc074-a267-400f-b8f1-db0850c36gk3",
"name": "gke-cluster",
"organizationId": "2836f775-aaaa-eeee-bbbb-3d3c29512GKE",
"credentialsId": "9b8d0456-177b-4a3d-b162-e68030d65GKE",
"createdAt": "2022-04-27T19:03:31.570829Z",
"region": {
"name": "eu-central-1",
"displayName": "EU (Frankfurt)"
},
"status": "archived",
"agentSnapshotReceivedAt": "2022-05-21T10:33:56.192020Z",
"agentStatus": "online",
"providerType": "gke",
"gke": {
"clusterName": "gke-cluster",
"region": "eu-central-1",
"location": "eu-central-1",
"projectId": "project-id",
"clientServiceAccount": "client-service-account",
"castServiceAccount": "cast-service-account"
},
"sshPublicKey": "key-123",
"clusterNameId": "gke-cluster-b6bfc074",
"private": true
}`)))
mockClient.EXPECT().
ExternalClusterAPIGetCluster(gomock.Any(), clusterId).
Return(&http.Response{StatusCode: 200, Body: body, Header: map[string][]string{"Content-Type": {"json"}}}, nil)

resource := resourceGKEClusterId()

val := cty.ObjectVal(map[string]cty.Value{})
state := terraform.NewInstanceStateShimmedFromValue(val, 0)
state.ID = clusterId

data := resource.Data(state)
result := resource.ReadContext(ctx, data, provider)
r.Nil(result)
r.False(result.HasError())
r.Equal(`<not created>`, data.State().String())
}
Loading

0 comments on commit ca7e544

Please sign in to comment.