-
Notifications
You must be signed in to change notification settings - Fork 35
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
KUBE-576: Add impersonate cluster support
Update terraform provider supporting SA impersonation Add example gke sa impersonation.
- Loading branch information
gleb
committed
Oct 2, 2024
1 parent
c88a94e
commit 72d7aa6
Showing
17 changed files
with
1,026 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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: ®ion, | ||
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) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
Oops, something went wrong.