diff --git a/client/api_client.go b/client/api_client.go index afd78303..212d8cc0 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -151,6 +151,7 @@ type ApiClientInterface interface { TeamRoleAssignmentDelete(payload *TeamRoleAssignmentDeletePayload) error TeamRoleAssignments(payload *TeamRoleAssignmentListPayload) ([]TeamRoleAssignmentPayload, error) KubernetesCredentialsCreate(payload *KubernetesCredentialsCreatePayload) (*Credentials, error) + KubernetesCredentialsUpdate(id string, payload *KubernetesCredentialsUpdatePayload) (*Credentials, error) } func NewApiClient(client http.HttpClientInterface, defaultOrganizationId string) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 81df5bfa..67e928f3 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -954,6 +954,21 @@ func (mr *MockApiClientInterfaceMockRecorder) KubernetesCredentialsCreate(arg0 a return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubernetesCredentialsCreate", reflect.TypeOf((*MockApiClientInterface)(nil).KubernetesCredentialsCreate), arg0) } +// KubernetesCredentialsUpdate mocks base method. +func (m *MockApiClientInterface) KubernetesCredentialsUpdate(arg0 string, arg1 *KubernetesCredentialsUpdatePayload) (*Credentials, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "KubernetesCredentialsUpdate", arg0, arg1) + ret0, _ := ret[0].(*Credentials) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// KubernetesCredentialsUpdate indicates an expected call of KubernetesCredentialsUpdate. +func (mr *MockApiClientInterfaceMockRecorder) KubernetesCredentialsUpdate(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "KubernetesCredentialsUpdate", reflect.TypeOf((*MockApiClientInterface)(nil).KubernetesCredentialsUpdate), arg0, arg1) +} + // Module mocks base method. func (m *MockApiClientInterface) Module(arg0 string) (*Module, error) { m.ctrl.T.Helper() diff --git a/client/kubernetes_credentials.go b/client/kubernetes_credentials.go index 3df3fe0f..62331b96 100644 --- a/client/kubernetes_credentials.go +++ b/client/kubernetes_credentials.go @@ -15,6 +15,11 @@ type KubernetesCredentialsCreatePayload struct { Value interface{} `json:"value"` } +type KubernetesCredentialsUpdatePayload struct { + Type KubernetesCrednetialsType `json:"type"` + Value interface{} `json:"value"` +} + // K8S_KUBECONFIG_FILE type KubeconfigFileValue struct { KubeConfig string `json:"kubeConfig"` @@ -63,3 +68,12 @@ func (client *ApiClient) KubernetesCredentialsCreate(payload *KubernetesCredenti return &result, nil } + +func (client *ApiClient) KubernetesCredentialsUpdate(id string, payload *KubernetesCredentialsUpdatePayload) (*Credentials, error) { + var result Credentials + if err := client.http.Patch("/credentials/"+id, payload, &result); err != nil { + return nil, err + } + + return &result, nil +} diff --git a/client/kubernetes_credentials_test.go b/client/kubernetes_credentials_test.go index 83c121cb..757fecb3 100644 --- a/client/kubernetes_credentials_test.go +++ b/client/kubernetes_credentials_test.go @@ -10,12 +10,16 @@ import ( var _ = Describe("Kubernetes Credentials", func() { var credentials *Credentials - Describe("KubernetesCredentialsCreate", func() { - value := AzureAksValue{ - ClusterName: "cc11", - ResourceGroup: "rg11", - } + mockCredentials := Credentials{ + Id: "id111", + } + value := AzureAksValue{ + ClusterName: "cc11", + ResourceGroup: "rg11", + } + + Describe("KubernetesCredentialsCreate", func() { createPayload := KubernetesCredentialsCreatePayload{ Name: "n1", Type: "K8S_AZURE_AKS_AUTH", @@ -34,10 +38,6 @@ var _ = Describe("Kubernetes Credentials", func() { Value: createPayload.Value, } - mockCredentials := Credentials{ - Id: "id111", - } - BeforeEach(func() { mockOrganizationIdCall(organizationId) @@ -62,4 +62,29 @@ var _ = Describe("Kubernetes Credentials", func() { Expect(credentials).To(Equal(&mockCredentials)) }) }) + + Describe("KubernetesCredentialsUpdate", func() { + updatePayload := KubernetesCredentialsUpdatePayload{ + Type: "K8S_AZURE_AKS_AUTH", + Value: value, + } + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Patch("/credentials/"+mockCredentials.Id, &updatePayload, gomock.Any()). + Do(func(path string, request interface{}, response *Credentials) { + *response = mockCredentials + }) + + credentials, _ = apiClient.KubernetesCredentialsUpdate(mockCredentials.Id, &updatePayload) + }) + + It("Should send PATCH request with params", func() { + httpCall.Times(1) + }) + + It("Should return key", func() { + Expect(credentials).To(Equal(&mockCredentials)) + }) + }) }) diff --git a/env0/provider.go b/env0/provider.go index 8029acfc..262c91bd 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -149,6 +149,9 @@ func Provider(version string) plugin.ProviderFunc { "env0_project_budget": resourceProjectBudget(), "env0_environment_discovery_configuration": resourceEnvironmentDiscoveryConfiguration(), "env0_kubeconfig_credentials": resourceKubeconfigCredentials(), + "env0_aws_eks_credentials": resourceAwsEksCredentials(), + "env0_azure_aks_credentials": resourceAzureAksCredentials(), + "env0_gcp_gke_credentials": resourceGcpGkeCredentials(), }, } diff --git a/env0/resource_aws_eks_credentials.go b/env0/resource_aws_eks_credentials.go new file mode 100644 index 00000000..7c20f8c4 --- /dev/null +++ b/env0/resource_aws_eks_credentials.go @@ -0,0 +1,83 @@ +package env0 + +import ( + "context" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAwsEksCredentials() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAwsEksCredentialsCreate, + UpdateContext: resourceAwsEksCredentialsUpdate, + ReadContext: resourceCredentialsRead(AWS_EKS_TYPE), + DeleteContext: resourceCredentialsDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceCredentialsImport(AWS_EKS_TYPE)}, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "name for the credentials", + Required: true, + ForceNew: true, + }, + "cluster_name": { + Type: schema.TypeString, + Description: "eks cluster name", + Required: true, + }, + "cluster_region": { + Type: schema.TypeString, + Description: "the AWS region of the eks cluster", + Required: true, + }, + }, + } +} + +func resourceAwsEksCredentialsCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.AwsEksValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsCreatePayload{ + Name: d.Get("name").(string), + Value: value, + Type: client.AwsEksCredentialsType, + } + + credentials, err := apiClient.KubernetesCredentialsCreate(&request) + if err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + d.SetId(credentials.Id) + + return nil +} + +func resourceAwsEksCredentialsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.AwsEksValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsUpdatePayload{ + Value: value, + Type: client.AwsEksCredentialsType, + } + + if _, err := apiClient.KubernetesCredentialsUpdate(d.Id(), &request); err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + return nil +} diff --git a/env0/resource_aws_eks_credentials_test.go b/env0/resource_aws_eks_credentials_test.go new file mode 100644 index 00000000..f7478768 --- /dev/null +++ b/env0/resource_aws_eks_credentials_test.go @@ -0,0 +1,228 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitAwsEksCredentialsResource(t *testing.T) { + resourceType := "env0_aws_eks_credentials" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + accessor := resourceAccessor(resourceType, resourceName) + + credentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster", + "cluster_region": "us-east-2", + } + + updatedCredentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster2", + "cluster_region": "us-east-2", + } + + createPayload := client.KubernetesCredentialsCreatePayload{ + Name: credentialsResource["name"].(string), + Value: client.AwsEksValue{ + ClusterName: credentialsResource["cluster_name"].(string), + ClusterRegion: credentialsResource["cluster_region"].(string), + }, + Type: client.AwsEksCredentialsType, + } + + updatePayload := client.KubernetesCredentialsUpdatePayload{ + Value: client.AwsEksValue{ + ClusterName: updatedCredentialsResource["cluster_name"].(string), + ClusterRegion: updatedCredentialsResource["cluster_region"].(string), + }, + Type: client.AwsEksCredentialsType, + } + + returnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30f", + Name: "test", + OrganizationId: "id", + Type: string(client.AwsEksCredentialsType), + } + + otherTypeReturnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30a", + Name: "test", + OrganizationId: "id", + Type: "AWS_....", + } + + testCaseForCreateAndUpdate := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", credentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", credentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_region", credentialsResource["cluster_region"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, updatedCredentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", updatedCredentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", updatedCredentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_region", updatedCredentialsResource["cluster_region"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + }, + } + + t.Run("create and update", func(t *testing.T) { + runUnitTest(t, testCaseForCreateAndUpdate, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(2).Return(returnValues, nil), + mock.EXPECT().KubernetesCredentialsUpdate(returnValues.Id, &updatePayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("drift", func(t *testing.T) { + stepConfig := resourceConfigCreate(resourceType, resourceName, credentialsResource) + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + }, + { + Config: stepConfig, + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, http.NewMockFailedResponseError(404)), + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "cluster_region"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues, returnValues}, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: returnValues.Id, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "cluster_region"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(3).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: otherTypeReturnValues.Id, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("credentials not found"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(otherTypeReturnValues.Id).Times(1).Return(client.Credentials{}, &client.NotFoundError{}), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("credentials with name %v not found", credentialsResource["name"].(string))), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues}, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_azure_aks_credentials.go b/env0/resource_azure_aks_credentials.go new file mode 100644 index 00000000..4f1ac96c --- /dev/null +++ b/env0/resource_azure_aks_credentials.go @@ -0,0 +1,83 @@ +package env0 + +import ( + "context" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceAzureAksCredentials() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceAzureAksCredentialsCreate, + UpdateContext: resourceAzureAksCredentialsUpdate, + ReadContext: resourceCredentialsRead(AZURE_AKS_TYPE), + DeleteContext: resourceCredentialsDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceCredentialsImport(AZURE_AKS_TYPE)}, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "name for the credentials", + Required: true, + ForceNew: true, + }, + "cluster_name": { + Type: schema.TypeString, + Description: "aks cluster name", + Required: true, + }, + "resource_group": { + Type: schema.TypeString, + Description: "the resource group of the aks", + Required: true, + }, + }, + } +} + +func resourceAzureAksCredentialsCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.AzureAksValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsCreatePayload{ + Name: d.Get("name").(string), + Value: value, + Type: client.AzureAksCredentialsType, + } + + credentials, err := apiClient.KubernetesCredentialsCreate(&request) + if err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + d.SetId(credentials.Id) + + return nil +} + +func resourceAzureAksCredentialsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.AzureAksValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsUpdatePayload{ + Value: value, + Type: client.AzureAksCredentialsType, + } + + if _, err := apiClient.KubernetesCredentialsUpdate(d.Id(), &request); err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + return nil +} diff --git a/env0/resource_azure_aks_credentials_test.go b/env0/resource_azure_aks_credentials_test.go new file mode 100644 index 00000000..2cdc6729 --- /dev/null +++ b/env0/resource_azure_aks_credentials_test.go @@ -0,0 +1,228 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitAzureAksCredentialsResource(t *testing.T) { + resourceType := "env0_azure_aks_credentials" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + accessor := resourceAccessor(resourceType, resourceName) + + credentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster", + "resource_group": "rg1", + } + + updatedCredentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster2", + "resource_group": "rg1", + } + + createPayload := client.KubernetesCredentialsCreatePayload{ + Name: credentialsResource["name"].(string), + Value: client.AzureAksValue{ + ClusterName: credentialsResource["cluster_name"].(string), + ResourceGroup: credentialsResource["resource_group"].(string), + }, + Type: client.AzureAksCredentialsType, + } + + updatePayload := client.KubernetesCredentialsUpdatePayload{ + Value: client.AzureAksValue{ + ClusterName: updatedCredentialsResource["cluster_name"].(string), + ResourceGroup: updatedCredentialsResource["resource_group"].(string), + }, + Type: client.AzureAksCredentialsType, + } + + returnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30f", + Name: "test", + OrganizationId: "id", + Type: string(client.AzureAksCredentialsType), + } + + otherTypeReturnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30a", + Name: "test", + OrganizationId: "id", + Type: "AWS_....", + } + + testCaseForCreateAndUpdate := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", credentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", credentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "resource_group", credentialsResource["resource_group"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, updatedCredentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", updatedCredentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", updatedCredentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "resource_group", updatedCredentialsResource["resource_group"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + }, + } + + t.Run("create and update", func(t *testing.T) { + runUnitTest(t, testCaseForCreateAndUpdate, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(2).Return(returnValues, nil), + mock.EXPECT().KubernetesCredentialsUpdate(returnValues.Id, &updatePayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("drift", func(t *testing.T) { + stepConfig := resourceConfigCreate(resourceType, resourceName, credentialsResource) + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + }, + { + Config: stepConfig, + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, http.NewMockFailedResponseError(404)), + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "resource_group"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues, returnValues}, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: returnValues.Id, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "resource_group"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(3).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: otherTypeReturnValues.Id, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("credentials not found"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(otherTypeReturnValues.Id).Times(1).Return(client.Credentials{}, &client.NotFoundError{}), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("credentials with name %v not found", credentialsResource["name"].(string))), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues}, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_gcp_gke_credentials.go b/env0/resource_gcp_gke_credentials.go new file mode 100644 index 00000000..02c97622 --- /dev/null +++ b/env0/resource_gcp_gke_credentials.go @@ -0,0 +1,83 @@ +package env0 + +import ( + "context" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func resourceGcpGkeCredentials() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceGcpGkeCredentialsCreate, + UpdateContext: resourceGcpGkeCredentialsUpdate, + ReadContext: resourceCredentialsRead(GCP_GKE_TYPE), + DeleteContext: resourceCredentialsDelete, + + Importer: &schema.ResourceImporter{StateContext: resourceCredentialsImport(GCP_GKE_TYPE)}, + + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "name for the credentials", + Required: true, + ForceNew: true, + }, + "cluster_name": { + Type: schema.TypeString, + Description: "gke cluster name", + Required: true, + }, + "compute_region": { + Type: schema.TypeString, + Description: "the GCP gke compute region", + Required: true, + }, + }, + } +} + +func resourceGcpGkeCredentialsCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.GcpGkeValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsCreatePayload{ + Name: d.Get("name").(string), + Value: value, + Type: client.GcpGkeCredentialsType, + } + + credentials, err := apiClient.KubernetesCredentialsCreate(&request) + if err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + d.SetId(credentials.Id) + + return nil +} + +func resourceGcpGkeCredentialsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.GcpGkeValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsUpdatePayload{ + Value: value, + Type: client.GcpGkeCredentialsType, + } + + if _, err := apiClient.KubernetesCredentialsUpdate(d.Id(), &request); err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + return nil +} diff --git a/env0/resource_gcp_gke_credentials_test.go b/env0/resource_gcp_gke_credentials_test.go new file mode 100644 index 00000000..798f1472 --- /dev/null +++ b/env0/resource_gcp_gke_credentials_test.go @@ -0,0 +1,228 @@ +package env0 + +import ( + "fmt" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/env0/terraform-provider-env0/client/http" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "go.uber.org/mock/gomock" +) + +func TestUnitGcpGkeCredentialsResource(t *testing.T) { + resourceType := "env0_gcp_gke_credentials" + resourceName := "test" + resourceNameImport := resourceType + "." + resourceName + accessor := resourceAccessor(resourceType, resourceName) + + credentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster", + "compute_region": "us-west1", + } + + updatedCredentialsResource := map[string]interface{}{ + "name": "test", + "cluster_name": "my-cluster2", + "compute_region": "us-west1", + } + + createPayload := client.KubernetesCredentialsCreatePayload{ + Name: credentialsResource["name"].(string), + Value: client.GcpGkeValue{ + ClusterName: credentialsResource["cluster_name"].(string), + ComputeRegion: credentialsResource["compute_region"].(string), + }, + Type: client.GcpGkeCredentialsType, + } + + updatePayload := client.KubernetesCredentialsUpdatePayload{ + Value: client.GcpGkeValue{ + ClusterName: updatedCredentialsResource["cluster_name"].(string), + ComputeRegion: updatedCredentialsResource["compute_region"].(string), + }, + Type: client.GcpGkeCredentialsType, + } + + returnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30f", + Name: "test", + OrganizationId: "id", + Type: string(client.GcpGkeCredentialsType), + } + + otherTypeReturnValues := client.Credentials{ + Id: "f595c4b6-0a24-4c22-89f7-7030045de30a", + Name: "test", + OrganizationId: "id", + Type: "AWS_....", + } + + testCaseForCreateAndUpdate := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", credentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", credentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "compute_region", credentialsResource["compute_region"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, updatedCredentialsResource), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "name", updatedCredentialsResource["name"].(string)), + resource.TestCheckResourceAttr(accessor, "cluster_name", updatedCredentialsResource["cluster_name"].(string)), + resource.TestCheckResourceAttr(accessor, "compute_region", updatedCredentialsResource["compute_region"].(string)), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), + ), + }, + }, + } + + t.Run("create and update", func(t *testing.T) { + runUnitTest(t, testCaseForCreateAndUpdate, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(2).Return(returnValues, nil), + mock.EXPECT().KubernetesCredentialsUpdate(returnValues.Id, &updatePayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("drift", func(t *testing.T) { + stepConfig := resourceConfigCreate(resourceType, resourceName, credentialsResource) + + createTestCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: stepConfig, + }, + { + Config: stepConfig, + }, + }, + } + + runUnitTest(t, createTestCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, http.NewMockFailedResponseError(404)), + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "compute_region"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues, returnValues}, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: returnValues.Id, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"cluster_name", "compute_region"}, + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(3).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by id not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: otherTypeReturnValues.Id, + ImportStateVerify: true, + ExpectError: regexp.MustCompile("credentials not found"), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentials(otherTypeReturnValues.Id).Times(1).Return(client.Credentials{}, &client.NotFoundError{}), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) + + t.Run("import by name not found", func(t *testing.T) { + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, credentialsResource), + }, + { + ResourceName: resourceNameImport, + ImportState: true, + ImportStateId: credentialsResource["name"].(string), + ImportStateVerify: true, + ExpectError: regexp.MustCompile(fmt.Sprintf("credentials with name %v not found", credentialsResource["name"].(string))), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), + mock.EXPECT().CloudCredentialsList().Times(1).Return([]client.Credentials{otherTypeReturnValues}, nil), + mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), + ) + }) + }) +} diff --git a/env0/resource_kubeconfig_credentials.go b/env0/resource_kubeconfig_credentials.go index aba8c9f2..c768bbc2 100644 --- a/env0/resource_kubeconfig_credentials.go +++ b/env0/resource_kubeconfig_credentials.go @@ -11,6 +11,7 @@ import ( func resourceKubeconfigCredentials() *schema.Resource { return &schema.Resource{ CreateContext: resourceKubeconfigCredentialsCreate, + UpdateContext: resourceKubeconfigCredentialsUpdate, ReadContext: resourceCredentialsRead(KUBECONFIG_TYPE), DeleteContext: resourceCredentialsDelete, @@ -27,7 +28,6 @@ func resourceKubeconfigCredentials() *schema.Resource { Type: schema.TypeString, Description: "A valid kubeconfig file content", Required: true, - ForceNew: true, }, }, } @@ -56,3 +56,23 @@ func resourceKubeconfigCredentialsCreate(ctx context.Context, d *schema.Resource return nil } + +func resourceKubeconfigCredentialsUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + value := client.KubeconfigFileValue{} + if err := readResourceData(&value, d); err != nil { + return diag.Errorf("schema resource data deserialization failed: %v", err) + } + + apiClient := meta.(client.ApiClientInterface) + + request := client.KubernetesCredentialsUpdatePayload{ + Value: value, + Type: client.KubeconfigCredentialsType, + } + + if _, err := apiClient.KubernetesCredentialsUpdate(d.Id(), &request); err != nil { + return diag.Errorf("could not create credentials: %v", err) + } + + return nil +} diff --git a/env0/resource_kubeconfig_credentials_test.go b/env0/resource_kubeconfig_credentials_test.go index 0009c350..31752135 100644 --- a/env0/resource_kubeconfig_credentials_test.go +++ b/env0/resource_kubeconfig_credentials_test.go @@ -36,8 +36,7 @@ func TestUnitKubeconfigCredentialsResource(t *testing.T) { Type: client.KubeconfigCredentialsType, } - updatePayload := client.KubernetesCredentialsCreatePayload{ - Name: updatedCredentialsResource["name"].(string), + updatePayload := client.KubernetesCredentialsUpdatePayload{ Value: client.KubeconfigFileValue{ KubeConfig: updatedCredentialsResource["kube_config"].(string), }, @@ -58,13 +57,6 @@ func TestUnitKubeconfigCredentialsResource(t *testing.T) { Type: "AWS_....", } - updateReturnValues := client.Credentials{ - Id: "dsdsdsd-0a24-4c22-89f7-7030045de30f", - Name: returnValues.Name, - OrganizationId: "id", - Type: string(client.KubeconfigCredentialsType), - } - testCaseForCreateAndUpdate := resource.TestCase{ Steps: []resource.TestStep{ { @@ -80,7 +72,7 @@ func TestUnitKubeconfigCredentialsResource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(accessor, "name", updatedCredentialsResource["name"].(string)), resource.TestCheckResourceAttr(accessor, "kube_config", updatedCredentialsResource["kube_config"].(string)), - resource.TestCheckResourceAttr(accessor, "id", updateReturnValues.Id), + resource.TestCheckResourceAttr(accessor, "id", returnValues.Id), ), }, }, @@ -91,10 +83,9 @@ func TestUnitKubeconfigCredentialsResource(t *testing.T) { gomock.InOrder( mock.EXPECT().KubernetesCredentialsCreate(&createPayload).Times(1).Return(&returnValues, nil), mock.EXPECT().CloudCredentials(returnValues.Id).Times(2).Return(returnValues, nil), + mock.EXPECT().KubernetesCredentialsUpdate(returnValues.Id, &updatePayload).Times(1).Return(&returnValues, nil), + mock.EXPECT().CloudCredentials(returnValues.Id).Times(1).Return(returnValues, nil), mock.EXPECT().CloudCredentialsDelete(returnValues.Id).Times(1).Return(nil), - mock.EXPECT().KubernetesCredentialsCreate(&updatePayload).Times(1).Return(&updateReturnValues, nil), - mock.EXPECT().CloudCredentials(updateReturnValues.Id).Times(1).Return(updateReturnValues, nil), - mock.EXPECT().CloudCredentialsDelete(updateReturnValues.Id).Times(1).Return(nil), ) }) }) diff --git a/tests/integration/024_cloud_credentials/main.tf b/tests/integration/024_cloud_credentials/main.tf index 58351b8d..f3c3f21b 100644 --- a/tests/integration/024_cloud_credentials/main.tf +++ b/tests/integration/024_cloud_credentials/main.tf @@ -60,3 +60,21 @@ resource "env0_kubeconfig_credentials" "kubeconfig_credentials" { token: EOT } + +resource "env0_aws_eks_credentials" "aws_eks_credentials" { + name = "aws-eks-${random_string.random.result}" + cluster_name = "my-cluster" + cluster_region = "us-east-2" +} + +resource "env0_azure_aks_credentials" "azure_aks_credentials" { + name = "azure-aks-${random_string.random.result}" + cluster_name = "my-cluster" + resource_group = "rg1" +} + +resource "env0_gcp_gke_credentials" "gcp_gke_credentials" { + name = "gcp-gke-${random_string.random.result}" + cluster_name = "my-cluster" + compute_region = "us-west1" +}