-
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
- Loading branch information
gleb
committed
Oct 1, 2024
1 parent
65f04d9
commit 4666931
Showing
19 changed files
with
1,234 additions
and
6 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{ | ||
Check failure on line 125 in castai/resource_gke_cluster_id.go GitHub Actions / Build
|
||
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.