From d7012058672787247d63ba5e0e541064acdfc4ad Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Tue, 30 Jan 2024 11:24:22 -0600 Subject: [PATCH 1/3] Feat: add env0_projects (plural) data resource --- env0/data_projects.go | 54 +++++++++++++ env0/data_projects_test.go | 81 +++++++++++++++++++ env0/provider.go | 1 + .../data-sources/env0_projects/data-source.tf | 1 + tests/integration/002_project/main.tf | 2 + 5 files changed, 139 insertions(+) create mode 100644 env0/data_projects.go create mode 100644 env0/data_projects_test.go create mode 100644 examples/data-sources/env0_projects/data-source.tf diff --git a/env0/data_projects.go b/env0/data_projects.go new file mode 100644 index 00000000..ec3ec9a2 --- /dev/null +++ b/env0/data_projects.go @@ -0,0 +1,54 @@ +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 dataProjects() *schema.Resource { + return &schema.Resource{ + ReadContext: dataProjectsRead, + + Schema: map[string]*schema.Schema{ + "projects": { + Type: schema.TypeList, + Description: "list of projects", + Computed: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Type: schema.TypeString, + Description: "the name of the project", + Computed: true, + }, + "id": { + Type: schema.TypeString, + Description: "id of the project", + Computed: true, + }, + }, + }, + }, + }, + } +} + +func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + projects, err := apiClient.Projects() + if err != nil { + return diag.Errorf("failed to get list of projects: %v", err) + } + + if err := writeResourceDataSlice(projects, "projects", d); err != nil { + return diag.Errorf("schema slice resource data serialization failed: %v", err) + } + + d.SetId("projects") + + return nil +} diff --git a/env0/data_projects_test.go b/env0/data_projects_test.go new file mode 100644 index 00000000..ba9ce707 --- /dev/null +++ b/env0/data_projects_test.go @@ -0,0 +1,81 @@ +package env0 + +import ( + "errors" + "regexp" + "testing" + + "github.com/env0/terraform-provider-env0/client" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestProjectsDataSource(t *testing.T) { + project1 := client.Project{ + Id: "id0", + Name: "my-project-1", + } + + project2 := client.Project{ + Id: "id1", + Name: "my-project-2", + } + + projects := []client.Project{project1, project2} + + resourceType := "env0_projects" + resourceName := "test_projects" + accessor := dataSourceAccessor(resourceType, resourceName) + + getValidTestCase := func() resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{}), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "projects.0.id", project1.Id), + resource.TestCheckResourceAttr(accessor, "projects.0.name", project1.Name), + resource.TestCheckResourceAttr(accessor, "projects.1.id", project2.Id), + resource.TestCheckResourceAttr(accessor, "projects.1.name", project2.Name), + ), + }, + }, + } + } + + getErrorTestCase := func(expectedError string) resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{}), + ExpectError: regexp.MustCompile(expectedError), + }, + }, + } + } + + mockGetProjectsCall := func(returnValue []client.Project) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().Projects().AnyTimes().Return(returnValue, nil) + } + } + + mockGetProjectsCallFailed := func(err string) func(mockFunc *client.MockApiClientInterface) { + return func(mock *client.MockApiClientInterface) { + mock.EXPECT().Projects().AnyTimes().Return([]client.Project{}, errors.New(err)) + } + } + + t.Run("get all projects", func(t *testing.T) { + runUnitTest(t, + getValidTestCase(), + mockGetProjectsCall(projects), + ) + }) + + t.Run("Error when API call fails", func(t *testing.T) { + runUnitTest(t, + getErrorTestCase("failed to get list of projects: error"), + mockGetProjectsCallFailed("error"), + ) + }) +} diff --git a/env0/provider.go b/env0/provider.go index b5b81155..9c7e895a 100644 --- a/env0/provider.go +++ b/env0/provider.go @@ -96,6 +96,7 @@ func Provider(version string) plugin.ProviderFunc { "env0_gpg_key": dataGpgKey(), "env0_provider": dataProvider(), "env0_custom_flow": dataCustomFlow(), + "env0_projects": dataProjects(), }, ResourcesMap: map[string]*schema.Resource{ "env0_project": resourceProject(), diff --git a/examples/data-sources/env0_projects/data-source.tf b/examples/data-sources/env0_projects/data-source.tf new file mode 100644 index 00000000..317a9d32 --- /dev/null +++ b/examples/data-sources/env0_projects/data-source.tf @@ -0,0 +1 @@ +data "env0_projects" "list_of_projects" {} diff --git a/tests/integration/002_project/main.tf b/tests/integration/002_project/main.tf index 6dc53db8..f2c6b09d 100644 --- a/tests/integration/002_project/main.tf +++ b/tests/integration/002_project/main.tf @@ -64,6 +64,8 @@ data "env0_project" "data_by_name_with_parent_id" { parent_project_id = env0_project.test_project_other.id } +data "env0_projects" "list_of_projects" {} + output "test_project_name" { value = replace(env0_project.test_project.name, random_string.random.result, "") } From 52f69fa6368c657f6f90eae34e08a8ee4ff0e7f0 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Thu, 1 Feb 2024 17:10:05 -0600 Subject: [PATCH 2/3] added more features --- client/project.go | 1 + env0/data_project.go | 5 ++++ env0/data_projects.go | 32 ++++++++++++++++++++++- env0/data_projects_test.go | 52 +++++++++++++++++++++++++++++++++++--- 4 files changed, 86 insertions(+), 4 deletions(-) diff --git a/client/project.go b/client/project.go index 22171ce6..33347891 100644 --- a/client/project.go +++ b/client/project.go @@ -12,6 +12,7 @@ type Project struct { CreatedByUser User `json:"createdByUser"` Description string `json:"description"` ParentProjectId string `json:"parentProjectId,omitempty" tfschema:",omitempty"` + Hierarchy string `json:"hierarchy"` } type ProjectCreatePayload struct { diff --git a/env0/data_project.go b/env0/data_project.go index b72d748c..6e2534d3 100644 --- a/env0/data_project.go +++ b/env0/data_project.go @@ -55,6 +55,11 @@ func dataProject() *schema.Resource { Description: "textual description of the project", Computed: true, }, + "hierarchy": { + Type: schema.TypeString, + Description: "the hierarchy of the project", + Computed: true, + }, }, } } diff --git a/env0/data_projects.go b/env0/data_projects.go index ec3ec9a2..301e1c8e 100644 --- a/env0/data_projects.go +++ b/env0/data_projects.go @@ -13,6 +13,12 @@ func dataProjects() *schema.Resource { ReadContext: dataProjectsRead, Schema: map[string]*schema.Schema{ + "include_archived_projects": { + Type: schema.TypeBool, + Description: "set to 'true' to include archived projects (defaults to 'false')", + Optional: true, + Default: false, + }, "projects": { Type: schema.TypeList, Description: "list of projects", @@ -29,6 +35,21 @@ func dataProjects() *schema.Resource { Description: "id of the project", Computed: true, }, + "is_archived": { + Type: schema.TypeBool, + Description: "'true' if the project is archived", + Computed: true, + }, + "parent_project_id": { + Type: schema.TypeString, + Description: "the parent project id (if one exist)", + Computed: true, + }, + "hierarchy": { + Type: schema.TypeString, + Description: "the project hierarchy (e.g. uuid1|uuid2|...)", + Computed: true, + }, }, }, }, @@ -39,12 +60,21 @@ func dataProjects() *schema.Resource { func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) + includedArchivedProjects := d.Get("include_archived_projects").(bool) + projects, err := apiClient.Projects() if err != nil { return diag.Errorf("failed to get list of projects: %v", err) } - if err := writeResourceDataSlice(projects, "projects", d); err != nil { + filteredProjects := []client.Project{} + for _, project := range projects { + if includedArchivedProjects || !project.IsArchived { + filteredProjects = append(filteredProjects, project) + } + } + + if err := writeResourceDataSlice(filteredProjects, "projects", d); err != nil { return diag.Errorf("schema slice resource data serialization failed: %v", err) } diff --git a/env0/data_projects_test.go b/env0/data_projects_test.go index ba9ce707..813be677 100644 --- a/env0/data_projects_test.go +++ b/env0/data_projects_test.go @@ -11,8 +11,10 @@ import ( func TestProjectsDataSource(t *testing.T) { project1 := client.Project{ - Id: "id0", - Name: "my-project-1", + Id: "id0", + Name: "my-project-1", + ParentProjectId: "p1", + Hierarchy: "adsas|fdsfsd", } project2 := client.Project{ @@ -20,7 +22,13 @@ func TestProjectsDataSource(t *testing.T) { Name: "my-project-2", } - projects := []client.Project{project1, project2} + project3 := client.Project{ + Id: "id1", + Name: "my-project-2", + IsArchived: true, + } + + projects := []client.Project{project1, project2, project3} resourceType := "env0_projects" resourceName := "test_projects" @@ -34,8 +42,39 @@ func TestProjectsDataSource(t *testing.T) { Check: resource.ComposeAggregateTestCheckFunc( resource.TestCheckResourceAttr(accessor, "projects.0.id", project1.Id), resource.TestCheckResourceAttr(accessor, "projects.0.name", project1.Name), + resource.TestCheckResourceAttr(accessor, "projects.0.parent_project_id", project1.ParentProjectId), + resource.TestCheckResourceAttr(accessor, "projects.0.hierarchy", project1.Hierarchy), + resource.TestCheckResourceAttr(accessor, "projects.0.is_archived", "false"), resource.TestCheckResourceAttr(accessor, "projects.1.id", project2.Id), resource.TestCheckResourceAttr(accessor, "projects.1.name", project2.Name), + resource.TestCheckResourceAttr(accessor, "projects.1.is_archived", "false"), + resource.TestCheckResourceAttr(accessor, "projects.#", "2"), + ), + }, + }, + } + } + + getValidTestCaseWithArchived := func() resource.TestCase { + return resource.TestCase{ + Steps: []resource.TestStep{ + { + Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{ + "include_archived_projects": "true", + }), + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr(accessor, "projects.0.id", project1.Id), + resource.TestCheckResourceAttr(accessor, "projects.0.name", project1.Name), + resource.TestCheckResourceAttr(accessor, "projects.0.parent_project_id", project1.ParentProjectId), + resource.TestCheckResourceAttr(accessor, "projects.0.hierarchy", project1.Hierarchy), + resource.TestCheckResourceAttr(accessor, "projects.0.is_archived", "false"), + resource.TestCheckResourceAttr(accessor, "projects.1.id", project2.Id), + resource.TestCheckResourceAttr(accessor, "projects.1.name", project2.Name), + resource.TestCheckResourceAttr(accessor, "projects.1.is_archived", "false"), + resource.TestCheckResourceAttr(accessor, "projects.2.id", project2.Id), + resource.TestCheckResourceAttr(accessor, "projects.2.name", project2.Name), + resource.TestCheckResourceAttr(accessor, "projects.2.is_archived", "true"), + resource.TestCheckResourceAttr(accessor, "projects.#", "3"), ), }, }, @@ -72,6 +111,13 @@ func TestProjectsDataSource(t *testing.T) { ) }) + t.Run("get all projects including archived", func(t *testing.T) { + runUnitTest(t, + getValidTestCaseWithArchived(), + mockGetProjectsCall(projects), + ) + }) + t.Run("Error when API call fails", func(t *testing.T) { runUnitTest(t, getErrorTestCase("failed to get list of projects: error"), From 8bc484f219df922fe041249a98c593d8050b19b9 Mon Sep 17 00:00:00 2001 From: Tomer Heber Date: Thu, 1 Feb 2024 17:12:34 -0600 Subject: [PATCH 3/3] fix typo --- env0/data_projects.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/env0/data_projects.go b/env0/data_projects.go index 301e1c8e..3c0271e4 100644 --- a/env0/data_projects.go +++ b/env0/data_projects.go @@ -60,7 +60,7 @@ func dataProjects() *schema.Resource { func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { apiClient := meta.(client.ApiClientInterface) - includedArchivedProjects := d.Get("include_archived_projects").(bool) + includeArchivedProjects := d.Get("include_archived_projects").(bool) projects, err := apiClient.Projects() if err != nil { @@ -69,7 +69,7 @@ func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interfac filteredProjects := []client.Project{} for _, project := range projects { - if includedArchivedProjects || !project.IsArchived { + if includeArchivedProjects || !project.IsArchived { filteredProjects = append(filteredProjects, project) } }