From ca7e54462ce3b9a6c26cbd6828086bc40afa7c92 Mon Sep 17 00:00:00 2001 From: Gleb Stepanov Date: Thu, 3 Oct 2024 10:45:39 +0200 Subject: [PATCH] KUBE-576: Add impersonate cluster support (#386) Update terraform provider supporting SA impersonation Add example gke sa impersonation. Co-authored-by: gleb --- castai/provider.go | 1 + castai/resource_gke_cluster.go | 4 - castai/resource_gke_cluster_id.go | 188 +++++++++++++++++++ castai/resource_gke_cluster_id_test.go | 136 ++++++++++++++ castai/sdk/api.gen.go | 24 +++ castai/sdk/client.gen.go | 154 +++++++++++++++ castai/sdk/mock/client.go | 70 +++++++ docs/resources/gke_cluster_id.md | 44 +++++ examples/gke/gke_impersonate/README.MD | 19 ++ examples/gke/gke_impersonate/castai.tf | 144 ++++++++++++++ examples/gke/gke_impersonate/gke.tf | 36 ++++ examples/gke/gke_impersonate/imperson.tf | 89 +++++++++ examples/gke/gke_impersonate/providers.tf | 15 ++ examples/gke/gke_impersonate/tf.vars.example | 6 + examples/gke/gke_impersonate/variables.tf | 50 +++++ examples/gke/gke_impersonate/version.tf | 17 ++ examples/gke/gke_impersonate/vpc.tf | 33 ++++ 17 files changed, 1026 insertions(+), 4 deletions(-) create mode 100644 castai/resource_gke_cluster_id.go create mode 100644 castai/resource_gke_cluster_id_test.go create mode 100644 docs/resources/gke_cluster_id.md create mode 100644 examples/gke/gke_impersonate/README.MD create mode 100644 examples/gke/gke_impersonate/castai.tf create mode 100644 examples/gke/gke_impersonate/gke.tf create mode 100644 examples/gke/gke_impersonate/imperson.tf create mode 100644 examples/gke/gke_impersonate/providers.tf create mode 100644 examples/gke/gke_impersonate/tf.vars.example create mode 100644 examples/gke/gke_impersonate/variables.tf create mode 100644 examples/gke/gke_impersonate/version.tf create mode 100644 examples/gke/gke_impersonate/vpc.tf diff --git a/castai/provider.go b/castai/provider.go index b108c156..8113882b 100644 --- a/castai/provider.go +++ b/castai/provider.go @@ -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(), diff --git a/castai/resource_gke_cluster.go b/castai/resource_gke_cluster.go index d9c4a6c5..980a52cf 100644 --- a/castai/resource_gke_cluster.go +++ b/castai/resource_gke_cluster.go @@ -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) @@ -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) } diff --git a/castai/resource_gke_cluster_id.go b/castai/resource_gke_cluster_id.go new file mode 100644 index 00000000..08174e89 --- /dev/null +++ b/castai/resource_gke_cluster_id.go @@ -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) +} diff --git a/castai/resource_gke_cluster_id_test.go b/castai/resource_gke_cluster_id_test.go new file mode 100644 index 00000000..59bd4034 --- /dev/null +++ b/castai/resource_gke_cluster_id_test.go @@ -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(``, data.State().String()) +} diff --git a/castai/sdk/api.gen.go b/castai/sdk/api.gen.go index 83c26b48..a7d3708b 100644 --- a/castai/sdk/api.gen.go +++ b/castai/sdk/api.gen.go @@ -332,6 +332,12 @@ const ( WorkloadoptimizationV1ResourcePoliciesFunctionQUANTILE WorkloadoptimizationV1ResourcePoliciesFunction = "QUANTILE" ) +// ExternalClusterAPIGKECreateSARequest defines model for ExternalClusterAPI_GKECreateSA_request. +type ExternalClusterAPIGKECreateSARequest struct { + // UpdateGKEClusterParams defines updatable GKE cluster configuration. + Gke *ExternalclusterV1UpdateGKEClusterParams `json:"gke,omitempty"` +} + // UsersAPIUpdateOrganizationUserRequest defines model for UsersAPI_UpdateOrganizationUser_request. type UsersAPIUpdateOrganizationUserRequest struct { Role *string `json:"role,omitempty"` @@ -1589,6 +1595,9 @@ type ExternalclusterV1Cluster struct { // The cluster's ID. Id *string `json:"id,omitempty"` + // Indicates if the cluster is in phase 2. + IsPhase2 *bool `json:"isPhase2,omitempty"` + // KOPSClusterParams defines KOPS-specific arguments. Kops *ExternalclusterV1KOPSClusterParams `json:"kops,omitempty"` KubernetesVersion *string `json:"kubernetesVersion"` @@ -1758,6 +1767,9 @@ type ExternalclusterV1EKSClusterParams_Tags struct { // GKEClusterParams defines GKE-specific arguments. type ExternalclusterV1GKEClusterParams struct { + CastServiceAccount *string `json:"castServiceAccount,omitempty"` + ClientServiceAccount *string `json:"clientServiceAccount,omitempty"` + // Name of the cluster. ClusterName *string `json:"clusterName,omitempty"` @@ -1774,6 +1786,11 @@ type ExternalclusterV1GKEClusterParams struct { Region *string `json:"region,omitempty"` } +// ExternalclusterV1GKECreateSAResponse defines model for externalcluster.v1.GKECreateSAResponse. +type ExternalclusterV1GKECreateSAResponse struct { + ServiceAccount *string `json:"serviceAccount,omitempty"` +} + // GPUConfig describes instance GPU configuration. // // Required while provisioning GCP N1 instance types with GPU. @@ -3644,6 +3661,7 @@ type WorkloadoptimizationV1ResourcePolicies struct { // Min values for the recommendation, applies to every container. For memory - this is in MiB, for CPU - this is in cores. // If not set, the default value will be 10m for CPU and 10MiB for memory. This value will be overridden if configured on workload level. + // Value cannot be lower than 10m for CPU and 10MiB for memory. Min *float64 `json:"min"` // The overhead for the recommendation, the formula is: (1 + overhead) * function(args). @@ -4033,6 +4051,9 @@ type ExternalClusterAPIDisconnectClusterJSONBody = ExternalclusterV1DisconnectCo // ExternalClusterAPIHandleCloudEventJSONBody defines parameters for ExternalClusterAPIHandleCloudEvent. type ExternalClusterAPIHandleCloudEventJSONBody = ExternalclusterV1CloudEvent +// ExternalClusterAPIGKECreateSAJSONBody defines parameters for ExternalClusterAPIGKECreateSA. +type ExternalClusterAPIGKECreateSAJSONBody = ExternalClusterAPIGKECreateSARequest + // ExternalClusterAPIListNodesParams defines parameters for ExternalClusterAPIListNodes. type ExternalClusterAPIListNodesParams struct { PageLimit *string `form:"page.limit,omitempty" json:"page.limit,omitempty"` @@ -4322,6 +4343,9 @@ type ExternalClusterAPIDisconnectClusterJSONRequestBody = ExternalClusterAPIDisc // ExternalClusterAPIHandleCloudEventJSONRequestBody defines body for ExternalClusterAPIHandleCloudEvent for application/json ContentType. type ExternalClusterAPIHandleCloudEventJSONRequestBody = ExternalClusterAPIHandleCloudEventJSONBody +// ExternalClusterAPIGKECreateSAJSONRequestBody defines body for ExternalClusterAPIGKECreateSA for application/json ContentType. +type ExternalClusterAPIGKECreateSAJSONRequestBody = ExternalClusterAPIGKECreateSAJSONBody + // ExternalClusterAPIAddNodeJSONRequestBody defines body for ExternalClusterAPIAddNode for application/json ContentType. type ExternalClusterAPIAddNodeJSONRequestBody = ExternalClusterAPIAddNodeJSONBody diff --git a/castai/sdk/client.gen.go b/castai/sdk/client.gen.go index 10aacb31..6b58b021 100644 --- a/castai/sdk/client.gen.go +++ b/castai/sdk/client.gen.go @@ -270,6 +270,11 @@ type ClientInterface interface { ExternalClusterAPIHandleCloudEvent(ctx context.Context, clusterId string, body ExternalClusterAPIHandleCloudEventJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ExternalClusterAPIGKECreateSA request with any body + ExternalClusterAPIGKECreateSAWithBody(ctx context.Context, clusterId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) + + ExternalClusterAPIGKECreateSA(ctx context.Context, clusterId string, body ExternalClusterAPIGKECreateSAJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) + // ExternalClusterAPIListNodes request ExternalClusterAPIListNodes(ctx context.Context, clusterId string, params *ExternalClusterAPIListNodesParams, reqEditors ...RequestEditorFn) (*http.Response, error) @@ -1328,6 +1333,30 @@ func (c *Client) ExternalClusterAPIHandleCloudEvent(ctx context.Context, cluster return c.Client.Do(req) } +func (c *Client) ExternalClusterAPIGKECreateSAWithBody(ctx context.Context, clusterId string, contentType string, body io.Reader, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExternalClusterAPIGKECreateSARequestWithBody(c.Server, clusterId, contentType, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + +func (c *Client) ExternalClusterAPIGKECreateSA(ctx context.Context, clusterId string, body ExternalClusterAPIGKECreateSAJSONRequestBody, reqEditors ...RequestEditorFn) (*http.Response, error) { + req, err := NewExternalClusterAPIGKECreateSARequest(c.Server, clusterId, body) + if err != nil { + return nil, err + } + req = req.WithContext(ctx) + if err := c.applyEditors(ctx, req, reqEditors); err != nil { + return nil, err + } + return c.Client.Do(req) +} + func (c *Client) ExternalClusterAPIListNodes(ctx context.Context, clusterId string, params *ExternalClusterAPIListNodesParams, reqEditors ...RequestEditorFn) (*http.Response, error) { req, err := NewExternalClusterAPIListNodesRequest(c.Server, clusterId, params) if err != nil { @@ -4556,6 +4585,53 @@ func NewExternalClusterAPIHandleCloudEventRequestWithBody(server string, cluster return req, nil } +// NewExternalClusterAPIGKECreateSARequest calls the generic ExternalClusterAPIGKECreateSA builder with application/json body +func NewExternalClusterAPIGKECreateSARequest(server string, clusterId string, body ExternalClusterAPIGKECreateSAJSONRequestBody) (*http.Request, error) { + var bodyReader io.Reader + buf, err := json.Marshal(body) + if err != nil { + return nil, err + } + bodyReader = bytes.NewReader(buf) + return NewExternalClusterAPIGKECreateSARequestWithBody(server, clusterId, "application/json", bodyReader) +} + +// NewExternalClusterAPIGKECreateSARequestWithBody generates requests for ExternalClusterAPIGKECreateSA with any type of body +func NewExternalClusterAPIGKECreateSARequestWithBody(server string, clusterId string, contentType string, body io.Reader) (*http.Request, error) { + var err error + + var pathParam0 string + + pathParam0, err = runtime.StyleParamWithLocation("simple", false, "clusterId", runtime.ParamLocationPath, clusterId) + if err != nil { + return nil, err + } + + serverURL, err := url.Parse(server) + if err != nil { + return nil, err + } + + operationPath := fmt.Sprintf("/v1/kubernetes/external-clusters/%s/gke-create-sa", pathParam0) + if operationPath[0] == '/' { + operationPath = "." + operationPath + } + + queryURL, err := serverURL.Parse(operationPath) + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", queryURL.String(), body) + if err != nil { + return nil, err + } + + req.Header.Add("Content-Type", contentType) + + return req, nil +} + // NewExternalClusterAPIListNodesRequest generates requests for ExternalClusterAPIListNodes func NewExternalClusterAPIListNodesRequest(server string, clusterId string, params *ExternalClusterAPIListNodesParams) (*http.Request, error) { var err error @@ -8305,6 +8381,11 @@ type ClientWithResponsesInterface interface { ExternalClusterAPIHandleCloudEventWithResponse(ctx context.Context, clusterId string, body ExternalClusterAPIHandleCloudEventJSONRequestBody) (*ExternalClusterAPIHandleCloudEventResponse, error) + // ExternalClusterAPIGKECreateSA request with any body + ExternalClusterAPIGKECreateSAWithBodyWithResponse(ctx context.Context, clusterId string, contentType string, body io.Reader) (*ExternalClusterAPIGKECreateSAResponse, error) + + ExternalClusterAPIGKECreateSAWithResponse(ctx context.Context, clusterId string, body ExternalClusterAPIGKECreateSAJSONRequestBody) (*ExternalClusterAPIGKECreateSAResponse, error) + // ExternalClusterAPIListNodes request ExternalClusterAPIListNodesWithResponse(ctx context.Context, clusterId string, params *ExternalClusterAPIListNodesParams) (*ExternalClusterAPIListNodesResponse, error) @@ -10019,6 +10100,36 @@ func (r ExternalClusterAPIHandleCloudEventResponse) GetBody() []byte { // TODO: to have common interface. https://github.com/deepmap/oapi-codegen/issues/240 +type ExternalClusterAPIGKECreateSAResponse struct { + Body []byte + HTTPResponse *http.Response + JSON200 *ExternalclusterV1GKECreateSAResponse +} + +// Status returns HTTPResponse.Status +func (r ExternalClusterAPIGKECreateSAResponse) Status() string { + if r.HTTPResponse != nil { + return r.HTTPResponse.Status + } + return http.StatusText(0) +} + +// StatusCode returns HTTPResponse.StatusCode +func (r ExternalClusterAPIGKECreateSAResponse) StatusCode() int { + if r.HTTPResponse != nil { + return r.HTTPResponse.StatusCode + } + return 0 +} + +// TODO: to have common interface. https://github.com/deepmap/oapi-codegen/issues/240 +// Body returns body of byte array +func (r ExternalClusterAPIGKECreateSAResponse) GetBody() []byte { + return r.Body +} + +// TODO: to have common interface. https://github.com/deepmap/oapi-codegen/issues/240 + type ExternalClusterAPIListNodesResponse struct { Body []byte HTTPResponse *http.Response @@ -12781,6 +12892,23 @@ func (c *ClientWithResponses) ExternalClusterAPIHandleCloudEventWithResponse(ctx return ParseExternalClusterAPIHandleCloudEventResponse(rsp) } +// ExternalClusterAPIGKECreateSAWithBodyWithResponse request with arbitrary body returning *ExternalClusterAPIGKECreateSAResponse +func (c *ClientWithResponses) ExternalClusterAPIGKECreateSAWithBodyWithResponse(ctx context.Context, clusterId string, contentType string, body io.Reader) (*ExternalClusterAPIGKECreateSAResponse, error) { + rsp, err := c.ExternalClusterAPIGKECreateSAWithBody(ctx, clusterId, contentType, body) + if err != nil { + return nil, err + } + return ParseExternalClusterAPIGKECreateSAResponse(rsp) +} + +func (c *ClientWithResponses) ExternalClusterAPIGKECreateSAWithResponse(ctx context.Context, clusterId string, body ExternalClusterAPIGKECreateSAJSONRequestBody) (*ExternalClusterAPIGKECreateSAResponse, error) { + rsp, err := c.ExternalClusterAPIGKECreateSA(ctx, clusterId, body) + if err != nil { + return nil, err + } + return ParseExternalClusterAPIGKECreateSAResponse(rsp) +} + // ExternalClusterAPIListNodesWithResponse request returning *ExternalClusterAPIListNodesResponse func (c *ClientWithResponses) ExternalClusterAPIListNodesWithResponse(ctx context.Context, clusterId string, params *ExternalClusterAPIListNodesParams) (*ExternalClusterAPIListNodesResponse, error) { rsp, err := c.ExternalClusterAPIListNodes(ctx, clusterId, params) @@ -14860,6 +14988,32 @@ func ParseExternalClusterAPIHandleCloudEventResponse(rsp *http.Response) (*Exter return response, nil } +// ParseExternalClusterAPIGKECreateSAResponse parses an HTTP response from a ExternalClusterAPIGKECreateSAWithResponse call +func ParseExternalClusterAPIGKECreateSAResponse(rsp *http.Response) (*ExternalClusterAPIGKECreateSAResponse, error) { + bodyBytes, err := ioutil.ReadAll(rsp.Body) + defer rsp.Body.Close() + if err != nil { + return nil, err + } + + response := &ExternalClusterAPIGKECreateSAResponse{ + Body: bodyBytes, + HTTPResponse: rsp, + } + + switch { + case strings.Contains(rsp.Header.Get("Content-Type"), "json") && rsp.StatusCode == 200: + var dest ExternalclusterV1GKECreateSAResponse + if err := json.Unmarshal(bodyBytes, &dest); err != nil { + return nil, err + } + response.JSON200 = &dest + + } + + return response, nil +} + // ParseExternalClusterAPIListNodesResponse parses an HTTP response from a ExternalClusterAPIListNodesWithResponse call func ParseExternalClusterAPIListNodesResponse(rsp *http.Response) (*ExternalClusterAPIListNodesResponse, error) { bodyBytes, err := ioutil.ReadAll(rsp.Body) diff --git a/castai/sdk/mock/client.go b/castai/sdk/mock/client.go index b185f7f9..7ad6367a 100644 --- a/castai/sdk/mock/client.go +++ b/castai/sdk/mock/client.go @@ -835,6 +835,46 @@ func (mr *MockClientInterfaceMockRecorder) ExternalClusterAPIDrainNodeWithBody(c return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIDrainNodeWithBody", reflect.TypeOf((*MockClientInterface)(nil).ExternalClusterAPIDrainNodeWithBody), varargs...) } +// ExternalClusterAPIGKECreateSA mocks base method. +func (m *MockClientInterface) ExternalClusterAPIGKECreateSA(ctx context.Context, clusterId string, body sdk.ExternalClusterAPIGKECreateSAJSONRequestBody, reqEditors ...sdk.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, clusterId, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExternalClusterAPIGKECreateSA", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExternalClusterAPIGKECreateSA indicates an expected call of ExternalClusterAPIGKECreateSA. +func (mr *MockClientInterfaceMockRecorder) ExternalClusterAPIGKECreateSA(ctx, clusterId, body interface{}, reqEditors ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, clusterId, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIGKECreateSA", reflect.TypeOf((*MockClientInterface)(nil).ExternalClusterAPIGKECreateSA), varargs...) +} + +// ExternalClusterAPIGKECreateSAWithBody mocks base method. +func (m *MockClientInterface) ExternalClusterAPIGKECreateSAWithBody(ctx context.Context, clusterId, contentType string, body io.Reader, reqEditors ...sdk.RequestEditorFn) (*http.Response, error) { + m.ctrl.T.Helper() + varargs := []interface{}{ctx, clusterId, contentType, body} + for _, a := range reqEditors { + varargs = append(varargs, a) + } + ret := m.ctrl.Call(m, "ExternalClusterAPIGKECreateSAWithBody", varargs...) + ret0, _ := ret[0].(*http.Response) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExternalClusterAPIGKECreateSAWithBody indicates an expected call of ExternalClusterAPIGKECreateSAWithBody. +func (mr *MockClientInterfaceMockRecorder) ExternalClusterAPIGKECreateSAWithBody(ctx, clusterId, contentType, body interface{}, reqEditors ...interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + varargs := append([]interface{}{ctx, clusterId, contentType, body}, reqEditors...) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIGKECreateSAWithBody", reflect.TypeOf((*MockClientInterface)(nil).ExternalClusterAPIGKECreateSAWithBody), varargs...) +} + // ExternalClusterAPIGetAssumeRolePrincipal mocks base method. func (m *MockClientInterface) ExternalClusterAPIGetAssumeRolePrincipal(ctx context.Context, clusterId string, reqEditors ...sdk.RequestEditorFn) (*http.Response, error) { m.ctrl.T.Helper() @@ -3908,6 +3948,36 @@ func (mr *MockClientWithResponsesInterfaceMockRecorder) ExternalClusterAPIDrainN return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIDrainNodeWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).ExternalClusterAPIDrainNodeWithResponse), ctx, clusterId, nodeId, body) } +// ExternalClusterAPIGKECreateSAWithBodyWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) ExternalClusterAPIGKECreateSAWithBodyWithResponse(ctx context.Context, clusterId, contentType string, body io.Reader) (*sdk.ExternalClusterAPIGKECreateSAResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExternalClusterAPIGKECreateSAWithBodyWithResponse", ctx, clusterId, contentType, body) + ret0, _ := ret[0].(*sdk.ExternalClusterAPIGKECreateSAResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExternalClusterAPIGKECreateSAWithBodyWithResponse indicates an expected call of ExternalClusterAPIGKECreateSAWithBodyWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) ExternalClusterAPIGKECreateSAWithBodyWithResponse(ctx, clusterId, contentType, body interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIGKECreateSAWithBodyWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).ExternalClusterAPIGKECreateSAWithBodyWithResponse), ctx, clusterId, contentType, body) +} + +// ExternalClusterAPIGKECreateSAWithResponse mocks base method. +func (m *MockClientWithResponsesInterface) ExternalClusterAPIGKECreateSAWithResponse(ctx context.Context, clusterId string, body sdk.ExternalClusterAPIGKECreateSAJSONRequestBody) (*sdk.ExternalClusterAPIGKECreateSAResponse, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "ExternalClusterAPIGKECreateSAWithResponse", ctx, clusterId, body) + ret0, _ := ret[0].(*sdk.ExternalClusterAPIGKECreateSAResponse) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ExternalClusterAPIGKECreateSAWithResponse indicates an expected call of ExternalClusterAPIGKECreateSAWithResponse. +func (mr *MockClientWithResponsesInterfaceMockRecorder) ExternalClusterAPIGKECreateSAWithResponse(ctx, clusterId, body interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ExternalClusterAPIGKECreateSAWithResponse", reflect.TypeOf((*MockClientWithResponsesInterface)(nil).ExternalClusterAPIGKECreateSAWithResponse), ctx, clusterId, body) +} + // ExternalClusterAPIGetAssumeRolePrincipalWithResponse mocks base method. func (m *MockClientWithResponsesInterface) ExternalClusterAPIGetAssumeRolePrincipalWithResponse(ctx context.Context, clusterId string) (*sdk.ExternalClusterAPIGetAssumeRolePrincipalResponse, error) { m.ctrl.T.Helper() diff --git a/docs/resources/gke_cluster_id.md b/docs/resources/gke_cluster_id.md new file mode 100644 index 00000000..8e1bee62 --- /dev/null +++ b/docs/resources/gke_cluster_id.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "castai_gke_cluster_id Resource - terraform-provider-castai" +subcategory: "" +description: |- + GKE cluster resource allows connecting an existing GKE cluster to CAST AI. +--- + +# castai_gke_cluster_id (Resource) + +GKE cluster resource allows connecting an existing GKE cluster to CAST AI. + + + + +## Schema + +### Required + +- `location` (String) GCP cluster zone in case of zonal or region in case of regional cluster +- `name` (String) GKE cluster name +- `project_id` (String) GCP project id + +### Optional + +- `cast_service_account` (String) Service account email in cast project +- `client_service_account` (String) Service account email in client project +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `cluster_token` (String, Sensitive) CAST.AI agent cluster token +- `id` (String) The ID of this resource. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `delete` (String) +- `update` (String) + + diff --git a/examples/gke/gke_impersonate/README.MD b/examples/gke/gke_impersonate/README.MD new file mode 100644 index 00000000..f3cdc6d7 --- /dev/null +++ b/examples/gke/gke_impersonate/README.MD @@ -0,0 +1,19 @@ +## GKE and CAST AI example with CAST AI service account impersonation + +# Usage +1. Rename `tf.vars.example` to `tf.vars` +2. Update `tf.vars` file with your project name, cluster name, cluster region and CAST AI API token. +3. Initialize Terraform. Under example root folder run: +``` +terraform init +``` +4. Run Terraform apply: +``` +terraform apply -var-file=tf.vars +``` +5. To destroy resources created by this example: +``` +terraform destroy -var-file=tf.vars +``` + +Please refer to this guide if you run into any issues https://docs.cast.ai/docs/terraform-troubleshooting diff --git a/examples/gke/gke_impersonate/castai.tf b/examples/gke/gke_impersonate/castai.tf new file mode 100644 index 00000000..a989b745 --- /dev/null +++ b/examples/gke/gke_impersonate/castai.tf @@ -0,0 +1,144 @@ +module "castai-gke-cluster" { + source = "castai/gke-cluster/castai" + + api_url = var.castai_api_url + castai_api_token = var.castai_api_token + grpc_url = var.castai_grpc_url + wait_for_cluster_ready = true + + project_id = var.project_id + gke_cluster_name = var.cluster_name + gke_cluster_location = module.gke.location + + gke_credentials = "{}" + delete_nodes_on_disconnect = var.delete_nodes_on_disconnect + + default_node_configuration_name = "default" + + node_configurations = { + default = { + disk_cpu_ratio = 25 + subnets = [module.vpc.subnets_ids[0]] + tags = var.tags + } + + test_node_config = { + disk_cpu_ratio = 10 + subnets = [module.vpc.subnets_ids[0]] + tags = var.tags + max_pods_per_node = 40 + disk_type = "pd-ssd", + network_tags = ["dev"] + } + + } + + node_templates = { + default_by_castai = { + name = "default-by-castai" + configuration_name = "default" + is_default = true + is_enabled = true + should_taint = false + + constraints = { + on_demand = true + spot = true + use_spot_fallbacks = true + + enable_spot_diversity = false + spot_diversity_price_increase_limit_percent = 20 + } + } + + spot_tmpl = { + configuration_id = module.castai-gke-cluster.castai_node_configurations["default"] + is_enabled = true + should_taint = true + + custom_labels = { + custom-label-key-1 = "custom-label-value-1" + custom-label-key-2 = "custom-label-value-2" + } + + custom_taints = [ + { + key = "custom-taint-key-1" + value = "custom-taint-value-1" + effect = "NoSchedule" + }, + { + key = "custom-taint-key-2" + value = "custom-taint-value-2" + effect = "NoSchedule" + } + ] + + constraints = { + fallback_restore_rate_seconds = 1800 + spot = true + use_spot_fallbacks = true + min_cpu = 4 + max_cpu = 100 + instance_families = { + exclude = ["e2"] + } + compute_optimized_state = "disabled" + storage_optimized_state = "disabled" + # Optional: define custom priority for instances selection. + # + # 1. Prioritize C2D and C2 spot instances above all else, regardless of price. + # 2. If C2D and C2 is not available, try C3D family. + custom_priority = [ + { + instance_families = ["c2d", "c2"] + spot = true + }, + { + instance_families = ["c3d"] + spot = true + } + # 3. instances not matching any of custom priority groups will be tried after + # nothing matches from priority groups. + ] + } + custom_instances_enabled = true + } + } + + autoscaler_settings = { + enabled = true + node_templates_partial_matching_enabled = false + + unschedulable_pods = { + enabled = true + } + + node_downscaler = { + enabled = true + + empty_nodes = { + enabled = true + } + + evictor = { + aggressive_mode = false + cycle_interval = "5m10s" + dry_run = false + enabled = true + node_grace_period_minutes = 10 + scoped_mode = false + } + } + + cluster_limits = { + enabled = true + + cpu = { + max_cores = 20 + min_cores = 1 + } + } + } + depends_on = [module.gke] +} diff --git a/examples/gke/gke_impersonate/gke.tf b/examples/gke/gke_impersonate/gke.tf new file mode 100644 index 00000000..3d5b56ac --- /dev/null +++ b/examples/gke/gke_impersonate/gke.tf @@ -0,0 +1,36 @@ +module "gke" { + source = "terraform-google-modules/kubernetes-engine/google" + version = "24.1.0" + project_id = var.project_id + name = var.cluster_name + region = var.cluster_region + zones = var.cluster_zones + network = module.vpc.network_name + subnetwork = module.vpc.subnets_names[0] + ip_range_pods = local.ip_range_pods + ip_range_services = local.ip_range_services + http_load_balancing = false + network_policy = false + horizontal_pod_autoscaling = true + filestore_csi_driver = false + create_service_account = false + service_account = google_service_account.client_service_account.email + + node_pools = [ + { + name = "default-node-pool" + machine_type = "e2-standard-2" + min_count = 0 + max_count = 10 + local_ssd_count = 0 + disk_size_gb = 100 + disk_type = "pd-standard" + image_type = "COS_CONTAINERD" + auto_repair = true + auto_upgrade = true + preemptible = false + initial_node_count = 2 # has to be >=2 to successfully deploy CAST AI controller + }, + ] + depends_on = [time_sleep.wait_3_minutes] +} diff --git a/examples/gke/gke_impersonate/imperson.tf b/examples/gke/gke_impersonate/imperson.tf new file mode 100644 index 00000000..7ba04b31 --- /dev/null +++ b/examples/gke/gke_impersonate/imperson.tf @@ -0,0 +1,89 @@ +locals { + service_account_id = "castai-gke-tf-${substr(sha1(var.cluster_name), 0, 8)}" +} + +data "castai_gke_user_policies" "gke" {} + +data "google_project" "project" { + project_id = var.project_id +} + +resource "google_service_account" "client_service_account" { + account_id = local.service_account_id + display_name = "Service account to manage ${var.cluster_name} cluster via CAST" + project = var.project_id +} + +resource "google_project_iam_custom_role" "castai_role" { + role_id = "castai.gkeAccess.${substr(sha1(var.cluster_name), 0, 8)}.tf" + title = "Role to manage GKE cluster via CAST AI" + description = "Role to manage GKE cluster via CAST AI" + permissions = toset(data.castai_gke_user_policies.gke.policy) + project = var.project_id + stage = "GA" +} + +resource "google_project_iam_binding" "compute_manager_binding" { + project = var.project_id + role = "projects/${var.project_id}/roles/castai.gkeAccess.${substr(sha1(var.cluster_name), 0, 8)}.tf" + members = ["serviceAccount:${google_service_account.client_service_account.email}"] +} + +# Configure GKE cluster and obtain the castai service account. +resource "castai_gke_cluster_id" "cluster_id" { + name = var.cluster_name + location = var.cluster_region + project_id = var.project_id + client_service_account = google_service_account.client_service_account.email + # DO NOT UNCOMMENT: cast service account will be computed and filled after apply. + # cast_service_account = "to-be-computed" +} + +# Grant the roles/iam.serviceAccountTokenCreator role to the CASTAI_SERVICE_ACCOUNT +resource "google_service_account_iam_member" "token_creator_binding" { + service_account_id = google_service_account.client_service_account.name + role = "roles/iam.serviceAccountTokenCreator" + member = "serviceAccount:${castai_gke_cluster_id.cluster_id.cast_service_account}" + + condition { + title = "AlwaysTrueCondition" + description = "This condition is always true" + expression = "true" + } + + depends_on = [castai_gke_cluster_id.cluster_id] +} + +# Grant the roles/iam.serviceAccountUser role to the CASTAI_SERVICE_ACCOUNT with a specific condition +resource "google_service_account_iam_member" "impersonation_user_binding" { + service_account_id = google_service_account.client_service_account.name + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${castai_gke_cluster_id.cluster_id.cast_service_account}" + + condition { + title = "SpecificServiceAccountCondition" + description = "Allow impersonation only for CASTAI_SERVICE_ACCOUNT" + expression = "request.auth.claims.email == \"${castai_gke_cluster_id.cluster_id.cast_service_account}\"" + } + + depends_on = [castai_gke_cluster_id.cluster_id] +} + +# Grant the roles/iam.serviceAccountUser role to the CLIENT_SERVICE_ACCOUNT without. +resource "google_project_iam_member" "service_account_user" { + project = var.project_id + role = "roles/iam.serviceAccountUser" + member = "serviceAccount:${google_service_account.client_service_account.email}" +} + +// service_account +resource "time_sleep" "wait_3_minutes" { + depends_on = [ + google_service_account.client_service_account, + google_service_account_iam_member.token_creator_binding, + google_service_account_iam_member.impersonation_user_binding, + google_project_iam_member.service_account_user, + ] + + create_duration = "180s" +} \ No newline at end of file diff --git a/examples/gke/gke_impersonate/providers.tf b/examples/gke/gke_impersonate/providers.tf new file mode 100644 index 00000000..77d9d6b1 --- /dev/null +++ b/examples/gke/gke_impersonate/providers.tf @@ -0,0 +1,15 @@ +# Configure Data sources and providers required for CAST AI connection. +data "google_client_config" "default" {} + +provider "castai" { + api_url = var.castai_api_url + api_token = var.castai_api_token +} + +provider "helm" { + kubernetes { + host = "https://${module.gke.endpoint}" + token = data.google_client_config.default.access_token + cluster_ca_certificate = base64decode(module.gke.ca_certificate) + } +} \ No newline at end of file diff --git a/examples/gke/gke_impersonate/tf.vars.example b/examples/gke/gke_impersonate/tf.vars.example new file mode 100644 index 00000000..35e5b7a4 --- /dev/null +++ b/examples/gke/gke_impersonate/tf.vars.example @@ -0,0 +1,6 @@ +castai_api_token = "" +project_id = "" +cluster_region = "" +cluster_name = "" +cluster_zones = [""] +castai_api_url = "" \ No newline at end of file diff --git a/examples/gke/gke_impersonate/variables.tf b/examples/gke/gke_impersonate/variables.tf new file mode 100644 index 00000000..1819c1e4 --- /dev/null +++ b/examples/gke/gke_impersonate/variables.tf @@ -0,0 +1,50 @@ +# GKE module variables. +variable "cluster_name" { + type = string + description = "GKE cluster name in GCP project." +} + +variable "cluster_region" { + type = string + description = "The region to create the cluster." +} + +variable "cluster_zones" { + type = list(string) + description = "The zones to create the cluster." +} + +variable "project_id" { + type = string + description = "GCP project ID in which GKE cluster would be created." +} + +variable "castai_api_url" { + type = string + description = "URL of alternative CAST AI API to be used during development or testing" + default = "https://api.cast.ai" +} + +# Variables required for connecting GKE cluster to CAST AI +variable "castai_api_token" { + type = string + description = "CAST AI API token created in console.cast.ai API Access keys section." +} + +variable "castai_grpc_url" { + type = string + description = "CAST AI gRPC URL" + default = "grpc.cast.ai:443" +} + +variable "delete_nodes_on_disconnect" { + type = bool + description = "Optional parameter, if set to true - CAST AI provisioned nodes will be deleted from cloud on cluster disconnection. For production use it is recommended to set it to false." + default = true +} + +variable "tags" { + type = map(any) + description = "Optional tags for new cluster nodes. This parameter applies only to new nodes - tags for old nodes are not reconciled." + default = {} +} diff --git a/examples/gke/gke_impersonate/version.tf b/examples/gke/gke_impersonate/version.tf new file mode 100644 index 00000000..502f0c51 --- /dev/null +++ b/examples/gke/gke_impersonate/version.tf @@ -0,0 +1,17 @@ +terraform { + required_providers { + castai = { + source = "castai/castai" + } + kubernetes = { + source = "hashicorp/kubernetes" + } + google = { + source = "hashicorp/google" + } + google-beta = { + source = "hashicorp/google-beta" + } + } + required_version = ">= 0.13" +} diff --git a/examples/gke/gke_impersonate/vpc.tf b/examples/gke/gke_impersonate/vpc.tf new file mode 100644 index 00000000..5fa6a7fe --- /dev/null +++ b/examples/gke/gke_impersonate/vpc.tf @@ -0,0 +1,33 @@ +locals { + ip_range_pods = "${var.cluster_name}-ip-range-pods" + ip_range_services = "${var.cluster_name}-ip-range-services" + ip_range_nodes = "${var.cluster_name}-ip-range-nodes" +} + +module "vpc" { + source = "terraform-google-modules/network/google" + version = "6.0.0" + project_id = var.project_id + network_name = var.cluster_name + subnets = [ + { + subnet_name = local.ip_range_nodes + subnet_ip = "10.0.0.0/16" + subnet_region = var.cluster_region + subnet_private_access = "true" + }, + ] + + secondary_ranges = { + (local.ip_range_nodes) = [ + { + range_name = local.ip_range_pods + ip_cidr_range = "10.20.0.0/16" + }, + { + range_name = local.ip_range_services + ip_cidr_range = "10.30.0.0/24" + } + ] + } +}