diff --git a/client/api_client.go b/client/api_client.go index 2d88ba26..2cf9c023 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -37,6 +37,9 @@ type ApiClientInterface interface { AwsCredentialsList() ([]ApiKey, error) AwsCredentialsCreate(request AwsCredentialsCreatePayload) (ApiKey, error) AwsCredentialsDelete(id string) error + AssignCloudCredentialsToProject(projectId string, credentialId string) (CloudCredentialsProjectAssignment, error) + RemoveCloudCredentialsFromProject(credentialId string, projectId string) error + CloudCredentialIdsInProject(projectId string) ([]string, error) } func NewApiClient(client http.HttpClientInterface) ApiClientInterface { diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 488922c5..15895179 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -33,6 +33,21 @@ func (m *MockApiClientInterface) EXPECT() *MockApiClientInterfaceMockRecorder { return m.recorder } +// AssignCloudCredentialsToProject mocks base method. +func (m *MockApiClientInterface) AssignCloudCredentialsToProject(arg0, arg1 string) (CloudCredentialsProjectAssignment, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "AssignCloudCredentialsToProject", arg0, arg1) + ret0, _ := ret[0].(CloudCredentialsProjectAssignment) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AssignCloudCredentialsToProject indicates an expected call of AssignCloudCredentialsToProject. +func (mr *MockApiClientInterfaceMockRecorder) AssignCloudCredentialsToProject(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AssignCloudCredentialsToProject", reflect.TypeOf((*MockApiClientInterface)(nil).AssignCloudCredentialsToProject), arg0, arg1) +} + // AssignTemplateToProject mocks base method. func (m *MockApiClientInterface) AssignTemplateToProject(arg0 string, arg1 TemplateAssignmentToProjectPayload) (Template, error) { m.ctrl.T.Helper() @@ -107,6 +122,21 @@ func (mr *MockApiClientInterfaceMockRecorder) AwsCredentialsList() *gomock.Call return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AwsCredentialsList", reflect.TypeOf((*MockApiClientInterface)(nil).AwsCredentialsList)) } +// CloudCredentialIdsInProject mocks base method. +func (m *MockApiClientInterface) CloudCredentialIdsInProject(arg0 string) ([]string, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "CloudCredentialIdsInProject", arg0) + ret0, _ := ret[0].([]string) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// CloudCredentialIdsInProject indicates an expected call of CloudCredentialIdsInProject. +func (mr *MockApiClientInterfaceMockRecorder) CloudCredentialIdsInProject(arg0 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CloudCredentialIdsInProject", reflect.TypeOf((*MockApiClientInterface)(nil).CloudCredentialIdsInProject), arg0) +} + // ConfigurationVariableCreate mocks base method. func (m *MockApiClientInterface) ConfigurationVariableCreate(arg0, arg1 string, arg2 bool, arg3 Scope, arg4 string, arg5 ConfigurationVariableType, arg6 []string) (ConfigurationVariable, error) { m.ctrl.T.Helper() @@ -255,6 +285,20 @@ func (mr *MockApiClientInterfaceMockRecorder) Projects() *gomock.Call { return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Projects", reflect.TypeOf((*MockApiClientInterface)(nil).Projects)) } +// RemoveCloudCredentialsFromProject mocks base method. +func (m *MockApiClientInterface) RemoveCloudCredentialsFromProject(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "RemoveCloudCredentialsFromProject", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveCloudCredentialsFromProject indicates an expected call of RemoveCloudCredentialsFromProject. +func (mr *MockApiClientInterfaceMockRecorder) RemoveCloudCredentialsFromProject(arg0, arg1 interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveCloudCredentialsFromProject", reflect.TypeOf((*MockApiClientInterface)(nil).RemoveCloudCredentialsFromProject), arg0, arg1) +} + // RemoveTemplateFromProject mocks base method. func (m *MockApiClientInterface) RemoveTemplateFromProject(arg0, arg1 string) error { m.ctrl.T.Helper() diff --git a/client/cloud_credentials_project_assignment.go b/client/cloud_credentials_project_assignment.go new file mode 100644 index 00000000..23eee413 --- /dev/null +++ b/client/cloud_credentials_project_assignment.go @@ -0,0 +1,25 @@ +package client + +func (self *ApiClient) AssignCloudCredentialsToProject(projectId string, credentialId string) (CloudCredentialsProjectAssignment, error) { + var result CloudCredentialsProjectAssignment + + err := self.http.Put("/credentials/deployment/"+credentialId+"/project/"+projectId, nil, &result) + if err != nil { + return result, err + } + return result, nil +} + +func (self *ApiClient) RemoveCloudCredentialsFromProject(credentialId string, projectId string) error { + return self.http.Delete("/credentials/deployment/" + credentialId + "/project/" + projectId) +} + +func (self *ApiClient) CloudCredentialIdsInProject(projectId string) ([]string, error) { + var result CloudCredentialIdsInProjectResponse + err := self.http.Get("/credentials/deployment/project/"+projectId, nil, &result) + + if err != nil { + return nil, err + } + return result.CredentialIds, nil +} diff --git a/client/cloud_credentials_project_assignment_test.go b/client/cloud_credentials_project_assignment_test.go new file mode 100644 index 00000000..c4461702 --- /dev/null +++ b/client/cloud_credentials_project_assignment_test.go @@ -0,0 +1,115 @@ +package client_test + +import ( + "errors" + . "github.com/env0/terraform-provider-env0/client" + "github.com/golang/mock/gomock" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" +) + +var _ = Describe("Credentials Project Assignment", func() { + projectId := "projectId" + credentialId := "credentialId" + + Describe("AssignCloudCredentialsToProject", func() { + expectedResponse := CloudCredentialsProjectAssignment{ + Id: "assigment id", + CredentialId: "credentialId", + ProjectId: projectId, + } + Describe("Successful", func() { + var actualResult CloudCredentialsProjectAssignment + BeforeEach(func() { + + httpCall = mockHttpClient.EXPECT(). + Put("/credentials/deployment/"+credentialId+"/project/"+projectId, nil, gomock.Any()). + Do(func(path string, request interface{}, response *CloudCredentialsProjectAssignment) { + *response = expectedResponse + }).Times(1) + actualResult, _ = apiClient.AssignCloudCredentialsToProject(projectId, credentialId) + + }) + + It("should return the PUT result", func() { + Expect(actualResult).To(Equal(expectedResponse)) + }) + }) + Describe("On Error", func() { + errorInfo := "error" + var actualError error + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Put("/credentials/deployment/"+credentialId+"/project/"+projectId, nil, gomock.Any()). + Return(errors.New(errorInfo)). + Times(1) + _, actualError = apiClient.AssignCloudCredentialsToProject(projectId, credentialId) + + }) + + It("should return the error from the api call", func() { + Expect(actualError).ShouldNot(BeNil()) + Expect(actualError.Error()).Should(Equal(errorInfo)) + }) + }) + }) + + Describe("RemoveCloudCredentialsFromProject", func() { + errorInfo := "error" + var actualError error + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Delete("/credentials/deployment/" + credentialId + "/project/" + projectId). + Return(errors.New(errorInfo)). + Times(1) + actualError = apiClient.RemoveCloudCredentialsFromProject(credentialId, projectId) + + }) + + It("should return the error from the api call", func() { + Expect(actualError).ShouldNot(BeNil()) + Expect(actualError.Error()).Should(Equal(errorInfo)) + }) + }) + + Describe("CloudCredentialIdsInProject", func() { + Describe("Successful", func() { + var actualResult []string + + expectedResponse := CloudCredentialIdsInProjectResponse{ + CredentialIds: []string{"credentialId"}, + } + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/credentials/deployment/project/"+projectId, nil, gomock.Any()). + Do(func(path string, request interface{}, response *CloudCredentialIdsInProjectResponse) { + *response = expectedResponse + }).Times(1) + actualResult, _ = apiClient.CloudCredentialIdsInProject(projectId) + + }) + + It("should return the GET result", func() { + Expect(actualResult).To(Equal(expectedResponse.CredentialIds)) + }) + }) + Describe("On Error", func() { + errorInfo := "error" + var actualError error + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT(). + Get("/credentials/deployment/project/"+projectId, nil, gomock.Any()). + Return(errors.New(errorInfo)). + Times(1) + _, actualError = apiClient.CloudCredentialIdsInProject(projectId) + + }) + + It("should return the error from the api call", func() { + Expect(actualError).ShouldNot(BeNil()) + Expect(actualError.Error()).Should(Equal(errorInfo)) + }) + }) + + }) +}) diff --git a/client/model.go b/client/model.go index 46b5ffce..fa843f17 100644 --- a/client/model.go +++ b/client/model.go @@ -123,6 +123,20 @@ type TemplateAssignmentToProject struct { ProjectId string `json:"projectId"` } +type CloudCredentialIdsInProjectResponse struct { + CredentialIds []string `json:"credentialIds"` +} + +type CloudCredentialsProjectAssignmentPatchPayload struct { + CredentialIds []string `json:"credentialIds"` +} + +type CloudCredentialsProjectAssignment struct { + Id string `json:"id"` + CredentialId string `json:"credentialId"` + ProjectId string `json:"projectId"` +} + type Template struct { Author User `json:"author"` AuthorId string `json:"authorId"` diff --git a/docs/resources/aws_credentials.md b/docs/resources/aws_credentials.md index d360fe94..bfa290e6 100644 --- a/docs/resources/aws_credentials.md +++ b/docs/resources/aws_credentials.md @@ -13,7 +13,7 @@ description: |- ## Example Usage ```terraform -resource "env0_aws_credentials" "example" { +resource "env0_aws_credentials" "credentials" { name = "example" arn = "Example role ARN" external_id = "Example external id" diff --git a/docs/resources/cloud_credentials_project_assignment.md b/docs/resources/cloud_credentials_project_assignment.md new file mode 100644 index 00000000..3b2b4626 --- /dev/null +++ b/docs/resources/cloud_credentials_project_assignment.md @@ -0,0 +1,44 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "env0_cloud_credentials_project_assignment Resource - terraform-provider-env0" +subcategory: "" +description: |- + +--- + +# env0_cloud_credentials_project_assignment (Resource) + + + +## Example Usage + +```terraform +resource "env0_aws_credentials" "credentials" { + name = "example" + arn = "Example role ARN" + external_id = "Example external id" +} + +data "env0_project" "project" { + name = "Default Organization Project" +} + +resource "env0_cloud_credentials_project_assignment" "example" { + credential_id = data.env0_aws_credentials.credentials.id + project_id = data.env0_project.project.id +} +``` + + +## Schema + +### Required + +- **credential_id** (String) id of cloud credentials +- **project_id** (String) id of the project + +### Optional + +- **id** (String) The ID of this resource. + + diff --git a/env0/provider.go b/env0/provider.go index 2fda8b82..e1041af2 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -42,12 +42,13 @@ func Provider() *schema.Provider { "env0_aws_credentials": dataAwsCredentials(), }, ResourcesMap: map[string]*schema.Resource{ - "env0_project": resourceProject(), - "env0_configuration_variable": resourceConfigurationVariable(), - "env0_template": resourceTemplate(), - "env0_ssh_key": resourceSshKey(), - "env0_aws_credentials": resourceAwsCredentials(), - "env0_template_project_assignment": resourceTemplateProjectAssignment(), + "env0_project": resourceProject(), + "env0_configuration_variable": resourceConfigurationVariable(), + "env0_template": resourceTemplate(), + "env0_ssh_key": resourceSshKey(), + "env0_aws_credentials": resourceAwsCredentials(), + "env0_template_project_assignment": resourceTemplateProjectAssignment(), + "env0_cloud_credentials_project_assignment": resourceCloudCredentialsProjectAssignment(), }, ConfigureContextFunc: configureProvider, } diff --git a/env0/resource_cloud_credentials_project_assignment.go b/env0/resource_cloud_credentials_project_assignment.go new file mode 100644 index 00000000..6d75db88 --- /dev/null +++ b/env0/resource_cloud_credentials_project_assignment.go @@ -0,0 +1,85 @@ +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 resourceCloudCredentialsProjectAssignment() *schema.Resource { + return &schema.Resource{ + CreateContext: resourceCloudCredentialsProjectAssignmentCreate, + ReadContext: resourceCloudCredentialsProjectAssignmentRead, + DeleteContext: resourceCloudCredentialsProjectAssignmentDelete, + + Schema: map[string]*schema.Schema{ + "credential_id": { + Type: schema.TypeString, + Description: "id of cloud credentials", + Required: true, + ForceNew: true, + }, + "project_id": { + Type: schema.TypeString, + Description: "id of the project", + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceCloudCredentialsProjectAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*client.ApiClient) + + credentialId := d.Get("credential_id").(string) + projectId := d.Get("project_id").(string) + result, err := apiClient.AssignCloudCredentialsToProject(projectId, credentialId) + if err != nil { + return diag.Errorf("could not assign cloud credentials to project: %v", err) + } + d.SetId(getResourceId(result.CredentialId, result.ProjectId)) + return nil +} + +func resourceCloudCredentialsProjectAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*client.ApiClient) + + credentialId := d.Get("credential_id").(string) + projectId := d.Get("project_id").(string) + credentialsList, err := apiClient.CloudCredentialIdsInProject(projectId) + if err != nil { + return diag.Errorf("could not get cloud_credentials: %v", err) + } + found := false + for _, candidate := range credentialsList { + if candidate == credentialId { + found = true + } + } + if !found { + return diag.Errorf("could not find cloud credential project assignment.\n project id = %v, cloud credentials id = %v", projectId, credentialId) + } + + d.SetId(getResourceId(credentialId, projectId)) + + return nil +} + +func getResourceId(credentialId string, projectId string) string { + return credentialId + "|" + projectId +} + +func resourceCloudCredentialsProjectAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(*client.ApiClient) + + credential_id := d.Get("credential_id").(string) + projectId := d.Get("project_id").(string) + err := apiClient.RemoveCloudCredentialsFromProject(credential_id, projectId) + if err != nil { + return diag.Errorf("could not delete cloud credentials from project: %v", err) + } + return nil +} diff --git a/examples/resources/env0_aws_credentials/resource.tf b/examples/resources/env0_aws_credentials/resource.tf index 9558d694..da502c99 100644 --- a/examples/resources/env0_aws_credentials/resource.tf +++ b/examples/resources/env0_aws_credentials/resource.tf @@ -1,5 +1,5 @@ -resource "env0_aws_credentials" "example" { +resource "env0_aws_credentials" "credentials" { name = "example" arn = "Example role ARN" external_id = "Example external id" -} +} \ No newline at end of file diff --git a/examples/resources/env0_cloud_credentials_project_assignment/resource.tf b/examples/resources/env0_cloud_credentials_project_assignment/resource.tf new file mode 100644 index 00000000..8e27ef53 --- /dev/null +++ b/examples/resources/env0_cloud_credentials_project_assignment/resource.tf @@ -0,0 +1,14 @@ +resource "env0_aws_credentials" "credentials" { + name = "example" + arn = "Example role ARN" + external_id = "Example external id" +} + +data "env0_project" "project" { + name = "Default Organization Project" +} + +resource "env0_cloud_credentials_project_assignment" "example" { + credential_id = env0_aws_credentials.credentials.id + project_id = data.env0_project.project.id +} \ No newline at end of file diff --git a/tests/integration/008_cloud_credentials_project_assignment/conf.tf b/tests/integration/008_cloud_credentials_project_assignment/conf.tf new file mode 100644 index 00000000..58dc45e8 --- /dev/null +++ b/tests/integration/008_cloud_credentials_project_assignment/conf.tf @@ -0,0 +1,13 @@ +terraform { + backend "local" { + } + required_providers { + env0 = { + source = "terraform-registry.env0.com/env0/env0" + } + } +} + +provider "env0" {} + +variable "second_run" {} diff --git a/tests/integration/008_cloud_credentials_project_assignment/expected_outputs.json b/tests/integration/008_cloud_credentials_project_assignment/expected_outputs.json new file mode 100644 index 00000000..0967ef42 --- /dev/null +++ b/tests/integration/008_cloud_credentials_project_assignment/expected_outputs.json @@ -0,0 +1 @@ +{} diff --git a/tests/integration/008_cloud_credentials_project_assignment/main.tf b/tests/integration/008_cloud_credentials_project_assignment/main.tf new file mode 100644 index 00000000..f198498b --- /dev/null +++ b/tests/integration/008_cloud_credentials_project_assignment/main.tf @@ -0,0 +1,15 @@ +resource "env0_cloud_credentials_project_assignment" "example" { + credential_id = env0_aws_credentials.credentials.id + project_id = env0_project.test_project.id +} + +resource "env0_project" "test_project" { + name = "Test-Project" + description = "Test Description" +} + +resource "env0_aws_credentials" "credentials" { + name = "example" + arn = "Example role ARN" + external_id = "Example external id" +}