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 new file mode 100644 index 00000000..3c0271e4 --- /dev/null +++ b/env0/data_projects.go @@ -0,0 +1,84 @@ +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{ + "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", + 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, + }, + "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, + }, + }, + }, + }, + }, + } +} + +func dataProjectsRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + apiClient := meta.(client.ApiClientInterface) + + includeArchivedProjects := d.Get("include_archived_projects").(bool) + + projects, err := apiClient.Projects() + if err != nil { + return diag.Errorf("failed to get list of projects: %v", err) + } + + filteredProjects := []client.Project{} + for _, project := range projects { + if includeArchivedProjects || !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) + } + + 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..813be677 --- /dev/null +++ b/env0/data_projects_test.go @@ -0,0 +1,127 @@ +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", + ParentProjectId: "p1", + Hierarchy: "adsas|fdsfsd", + } + + project2 := client.Project{ + Id: "id1", + Name: "my-project-2", + } + + project3 := client.Project{ + Id: "id1", + Name: "my-project-2", + IsArchived: true, + } + + projects := []client.Project{project1, project2, project3} + + 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.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"), + ), + }, + }, + } + } + + 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("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"), + 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, "") }