Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: add env0_projects (plural) data resource #787

Merged
merged 3 commits into from
Feb 2, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 54 additions & 0 deletions env0/data_projects.go
Original file line number Diff line number Diff line change
@@ -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": {
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

decided to add name and id.
If anything else is required (or removed) let me know.

Copy link
Contributor

@razbensimon razbensimon Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we need to provide also those

isArchived?: boolean;
hierarchy: string;
parentProjectId?: string;

The last two are for creating custom logic around sub-projects.
For the first one (isArchived) we may want to filter by it by default. Can we?
and if possible add a parameter to include_archived_projects.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that we don't have the ability to filter via the API.
So the filter of include_archived_projects should be locally in the provider.
WDYS?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the API:
https://docs.env0.com/reference/projects-find-by-organization-id

Will it return archived projects?
What should be the default of include_archived_projects true or false?

I can return the 3 additional values if needed. Just want to be sure there's a use-case for that. If you believe there is. happy to do so. Please confirm.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will it return archived projects?

Yes

What should be the default of include_archived_projects true or false?

false. Meaning we should filter them locally by default.
Add them to the state only if include_archived_projects is true
This makes more sense because I don't see users use the archived ones so much.

Copy link
Contributor

@razbensimon razbensimon Feb 1, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BTW
hierarchy looks like uuid|uuid|.....
and parentProjectId is just uuid.

parentProjectId returned only on sub-projects.
hierarchy by default equals its own id (if not part of a tree).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@razbensimon - I have added the additional code you requested.

52f69fa

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")
razbensimon marked this conversation as resolved.
Show resolved Hide resolved

return nil
}
81 changes: 81 additions & 0 deletions env0/data_projects_test.go
Original file line number Diff line number Diff line change
@@ -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"),
)
})
}
1 change: 1 addition & 0 deletions env0/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
1 change: 1 addition & 0 deletions examples/data-sources/env0_projects/data-source.tf
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
data "env0_projects" "list_of_projects" {}
2 changes: 2 additions & 0 deletions tests/integration/002_project/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -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, "")
}
Expand Down
Loading