From 1a1e77608ff8efd57716708276d7d6b37ca5f637 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Thu, 22 Aug 2024 08:02:48 -0500 Subject: [PATCH 1/2] Feat: add support for 'move environment' backend --- client/api_client.go | 1 + client/api_client_mock.go | 14 +++++ client/environment.go | 53 +++++++++++----- client/environment_test.go | 30 +++++++-- env0/resource_environment.go | 10 ++- env0/resource_environment_test.go | 76 +++++++++++++++++++++++ tests/integration/012_environment/main.tf | 14 ++++- 7 files changed, 176 insertions(+), 22 deletions(-) diff --git a/client/api_client.go b/client/api_client.go index 41436a7e..e3d55773 100644 --- a/client/api_client.go +++ b/client/api_client.go @@ -71,6 +71,7 @@ type ApiClientInterface interface { EnvironmentUpdate(id string, payload EnvironmentUpdate) (Environment, error) EnvironmentDeploy(id string, payload DeployRequest) (EnvironmentDeployResponse, error) EnvironmentUpdateTTL(id string, payload TTL) (Environment, error) + EnvironmentMove(id string, projectId string) error EnvironmentScheduling(environmentId string) (EnvironmentScheduling, error) EnvironmentSchedulingUpdate(environmentId string, payload EnvironmentScheduling) (EnvironmentScheduling, error) EnvironmentSchedulingDelete(environmentId string) error diff --git a/client/api_client_mock.go b/client/api_client_mock.go index 808f385c..8f275e10 100644 --- a/client/api_client_mock.go +++ b/client/api_client_mock.go @@ -896,6 +896,20 @@ func (mr *MockApiClientInterfaceMockRecorder) EnvironmentMarkAsArchived(arg0 any return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvironmentMarkAsArchived", reflect.TypeOf((*MockApiClientInterface)(nil).EnvironmentMarkAsArchived), arg0) } +// EnvironmentMove mocks base method. +func (m *MockApiClientInterface) EnvironmentMove(arg0, arg1 string) error { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "EnvironmentMove", arg0, arg1) + ret0, _ := ret[0].(error) + return ret0 +} + +// EnvironmentMove indicates an expected call of EnvironmentMove. +func (mr *MockApiClientInterfaceMockRecorder) EnvironmentMove(arg0, arg1 any) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "EnvironmentMove", reflect.TypeOf((*MockApiClientInterface)(nil).EnvironmentMove), arg0, arg1) +} + // EnvironmentScheduling mocks base method. func (m *MockApiClientInterface) EnvironmentScheduling(arg0 string) (EnvironmentScheduling, error) { m.ctrl.T.Helper() diff --git a/client/environment.go b/client/environment.go index 03f9b99f..24e18a47 100644 --- a/client/environment.go +++ b/client/environment.go @@ -3,6 +3,7 @@ package client import ( "encoding/json" "fmt" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" ) @@ -10,23 +11,26 @@ type ConfigurationVariableType int func (c *ConfigurationVariableType) ReadResourceData(fieldName string, d *schema.ResourceData) error { val := d.Get(fieldName).(string) - intVal, ok := VariableTypes[val] - if !ok { - return fmt.Errorf("unknown configuration variable type %s", val) + + intVal, err := GetConfigurationVariableType(val) + if err != nil { + return err } + *c = intVal return nil } func (c *ConfigurationVariableType) WriteResourceData(fieldName string, d *schema.ResourceData) error { - val := *c - valStr := "" - if val == 0 { + var valStr string + + switch val := *c; val { + case 0: valStr = "environment" - } else if val == 1 { + case 1: valStr = "terraform" - } else { + default: return fmt.Errorf("unknown configuration variable type %d", val) } @@ -40,11 +44,6 @@ const ( type ConfigurationChanges []ConfigurationVariable -var VariableTypes = map[string]ConfigurationVariableType{ - "terraform": ConfigurationVariableTypeTerraform, - "environment": ConfigurationVariableTypeEnvironment, -} - type TTL struct { Type TTLType `json:"type"` Value string `json:"value,omitempty"` @@ -159,6 +158,21 @@ type EnvironmentCreateWithoutTemplate struct { TemplateCreate TemplateCreatePayload } +type EnvironmentMoveRequest struct { + ProjectId string `json:"projectId"` +} + +func GetConfigurationVariableType(variableType string) (ConfigurationVariableType, error) { + switch variableType { + case "terraform": + return ConfigurationVariableTypeTerraform, nil + case "environment": + return ConfigurationVariableTypeEnvironment, nil + default: + return 0, fmt.Errorf("unknown configuration variable type %s", variableType) + } +} + // The custom marshalJSON is used to return a flat JSON. func (create EnvironmentCreateWithoutTemplate) MarshalJSON() ([]byte, error) { // 1. Marshal to JSON both structs. @@ -173,9 +187,11 @@ func (create EnvironmentCreateWithoutTemplate) MarshalJSON() ([]byte, error) { // 2. Unmarshal both JSON byte arrays to two maps. var ecm, tcm map[string]interface{} + if err := json.Unmarshal(ecb, &ecm); err != nil { return nil, err } + if err := json.Unmarshal(tcb, &tcm); err != nil { return nil, err } @@ -254,8 +270,9 @@ func (client *ApiClient) EnvironmentCreateWithoutTemplate(payload EnvironmentCre organizationId, err := client.OrganizationId() if err != nil { - return result, nil + return result, err } + payload.TemplateCreate.OrganizationId = organizationId if err := client.http.Post("/environments/without-template", payload, &result); err != nil { @@ -311,3 +328,11 @@ func (client *ApiClient) EnvironmentDeploy(id string, payload DeployRequest) (En } return result, nil } + +func (client *ApiClient) EnvironmentMove(id string, projectId string) error { + payload := &EnvironmentMoveRequest{ + ProjectId: projectId, + } + + return client.http.Post("/environments/"+id+"/move", payload, nil) +} diff --git a/client/environment_test.go b/client/environment_test.go index 393ee15e..76747cd0 100644 --- a/client/environment_test.go +++ b/client/environment_test.go @@ -279,13 +279,15 @@ var _ = Describe("Environment Client", func() { }) Describe("EnvironmentDelete", func() { + var err error + BeforeEach(func() { - httpCall = mockHttpClient.EXPECT().Post("/environments/"+mockEnvironment.Id+"/destroy", nil, gomock.Any()) - apiClient.EnvironmentDestroy(mockEnvironment.Id) + httpCall = mockHttpClient.EXPECT().Post("/environments/"+mockEnvironment.Id+"/destroy", nil, gomock.Any()).Times(1) + _, err = apiClient.EnvironmentDestroy(mockEnvironment.Id) }) - It("Should send a destroy request", func() { - httpCall.Times(1) + It("Should not return error", func() { + Expect(err).To(BeNil()) }) }) @@ -416,6 +418,26 @@ var _ = Describe("Environment Client", func() { }) }) }) + + Describe("Environment Move", func() { + var err error + + environmentId := "envid" + projectId := "projid" + + request := EnvironmentMoveRequest{ + ProjectId: projectId, + } + + BeforeEach(func() { + httpCall = mockHttpClient.EXPECT().Post("/environments/"+environmentId+"/move", &request, nil).Times(1) + err = apiClient.EnvironmentMove(environmentId, projectId) + }) + + It("Should not return an error", func() { + Expect(err).To(BeNil()) + }) + }) }) func TestMarshalEnvironmentCreateWithoutTemplate(t *testing.T) { diff --git a/env0/resource_environment.go b/env0/resource_environment.go index ea7e6e77..3eadea5a 100644 --- a/env0/resource_environment.go +++ b/env0/resource_environment.go @@ -167,7 +167,6 @@ func resourceEnvironment() *schema.Resource { Type: schema.TypeString, Description: "project id of the environment", Required: true, - ForceNew: true, }, "template_id": { Type: schema.TypeString, @@ -718,6 +717,13 @@ func resourceEnvironmentRead(ctx context.Context, d *schema.ResourceData, meta i func resourceEnvironmentUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) + if d.HasChange("project_id") { + newProjectId := d.Get("project_id").(string) + if err := apiClient.EnvironmentMove(d.Id(), newProjectId); err != nil { + return diag.Errorf("failed to move environment to project id '%s': %s", newProjectId, err) + } + } + if shouldUpdate(d) { if err := update(d, apiClient); err != nil { return err @@ -1274,7 +1280,7 @@ func typeEqual(variable client.ConfigurationVariable, search client.Configuratio } func getConfigurationVariableFromSchema(variable map[string]interface{}) client.ConfigurationVariable { - varType := client.VariableTypes[variable["type"].(string)] + varType, _ := client.GetConfigurationVariableType(variable["type"].(string)) configurationVariable := client.ConfigurationVariable{ Name: variable["name"].(string), diff --git a/env0/resource_environment_test.go b/env0/resource_environment_test.go index 5ad62f1f..6cdc277c 100644 --- a/env0/resource_environment_test.go +++ b/env0/resource_environment_test.go @@ -215,6 +215,82 @@ func TestUnitEnvironmentResource(t *testing.T) { }) }) + t.Run("move environment", func(t *testing.T) { + newProjectId := "new-project-id" + + environment := client.Environment{ + Id: uuid.New().String(), + Name: "name", + ProjectId: "project-id", + LatestDeploymentLog: client.DeploymentLog{ + BlueprintId: templateId, + }, + } + + environmentCreate := client.EnvironmentCreate{ + Name: environment.Name, + ProjectId: environment.ProjectId, + DeployRequest: &client.DeployRequest{ + BlueprintId: environment.LatestDeploymentLog.BlueprintId, + }, + } + + movedEnvironment := environment + movedEnvironment.ProjectId = newProjectId + + testCase := resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": environment.Name, + "project_id": environment.ProjectId, + "template_id": templateId, + "force_destroy": true, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environment.Id), + resource.TestCheckResourceAttr(accessor, "project_id", environment.ProjectId), + ), + }, + { + Config: resourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "name": movedEnvironment.Name, + "project_id": movedEnvironment.ProjectId, + "template_id": templateId, + "force_destroy": true, + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "id", environment.Id), + resource.TestCheckResourceAttr(accessor, "project_id", movedEnvironment.ProjectId), + ), + }, + }, + } + + runUnitTest(t, testCase, func(mock *client.MockApiClientInterface) { + gomock.InOrder( + mock.EXPECT().Template(environment.LatestDeploymentLog.BlueprintId).Times(1).Return(template, nil), + mock.EXPECT().EnvironmentCreate(environmentCreate).Times(1).Return(environment, nil), + + mock.EXPECT().Environment(environment.Id).Times(1).Return(environment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + + mock.EXPECT().Environment(environment.Id).Times(1).Return(environment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, environment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().ConfigurationSetsAssignments("ENVIRONMENT", environment.Id).Times(1).Return(nil, nil), + + mock.EXPECT().EnvironmentMove(environment.Id, newProjectId).Return(nil), + + mock.EXPECT().Environment(movedEnvironment.Id).Times(1).Return(movedEnvironment, nil), + mock.EXPECT().ConfigurationVariablesByScope(client.ScopeEnvironment, movedEnvironment.Id).Times(1).Return(client.ConfigurationChanges{}, nil), + mock.EXPECT().ConfigurationSetsAssignments("ENVIRONMENT", movedEnvironment.Id).Times(1).Return(nil, nil), + + mock.EXPECT().EnvironmentDestroy(movedEnvironment.Id).Times(1), + ) + }) + }) + t.Run("vcs pr comments enabled", func(t *testing.T) { testCase := resource.TestCase{ Steps: []resource.TestStep{ diff --git a/tests/integration/012_environment/main.tf b/tests/integration/012_environment/main.tf index 3270e6df..025a6bd8 100644 --- a/tests/integration/012_environment/main.tf +++ b/tests/integration/012_environment/main.tf @@ -11,6 +11,11 @@ resource "env0_project" "test_project" { force_destroy = true } +resource "env0_project" "test_project2" { + name = "Test-Project2-for-environment-${random_string.random.result}" + force_destroy = true +} + data "env0_template" "github_template_for_environment" { name = "Github Integrated Template" } @@ -29,6 +34,11 @@ resource "env0_template_project_assignment" "assignment" { project_id = env0_project.test_project.id } +resource "env0_template_project_assignment" "assignment2" { + template_id = env0_template.template.id + project_id = env0_project.test_project2.id +} + resource "env0_environment" "auto_glob_envrironment" { depends_on = [env0_template_project_assignment.assignment] name = "environment-auto-glob-${random_string.random.result}" @@ -41,11 +51,11 @@ resource "env0_environment" "auto_glob_envrironment" { force_destroy = true } -resource "env0_environment" "example" { +resource "env0_environment" "environment" { depends_on = [env0_template_project_assignment.assignment] force_destroy = true name = "environment-${random_string.random.result}" - project_id = env0_project.test_project.id + project_id = var.second_run ? env0_project.test_project2.id : env0_project.test_project.id template_id = env0_template.template.id configuration { name = "environment configuration variable" From e9573da88f01b16b1c52fb539a26673249d4cd7a Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Thu, 22 Aug 2024 08:09:31 -0500 Subject: [PATCH 2/2] fix tests --- tests/integration/012_environment/main.tf | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/012_environment/main.tf b/tests/integration/012_environment/main.tf index 025a6bd8..10ae4797 100644 --- a/tests/integration/012_environment/main.tf +++ b/tests/integration/012_environment/main.tf @@ -51,7 +51,7 @@ resource "env0_environment" "auto_glob_envrironment" { force_destroy = true } -resource "env0_environment" "environment" { +resource "env0_environment" "example" { depends_on = [env0_template_project_assignment.assignment] force_destroy = true name = "environment-${random_string.random.result}"