Skip to content

Commit

Permalink
Feat: ability to filter for parent project name in data project resou…
Browse files Browse the repository at this point in the history
…rce (#639)

* Feat: ability to filter for parent project name in data project resource

* some minor refactoring

* removed computed

* some minor changes based on PR feedback

* minor fixes

* always filter based on parent name if exist
  • Loading branch information
TomerHeber authored May 4, 2023
1 parent 5ea92eb commit ead551f
Show file tree
Hide file tree
Showing 4 changed files with 143 additions and 6 deletions.
43 changes: 39 additions & 4 deletions env0/data_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ func dataProject() *schema.Resource {
ExactlyOneOf: []string{"name", "id"},
Computed: true,
},
"parent_project_name": {
Type: schema.TypeString,
Description: "the name of the parent project. Can be used when there are multiple subprojects with the same name under different parent projects",
Optional: true,
},
"created_by": {
Type: schema.TypeString,
Description: "textual description of the entity who created the project",
Expand Down Expand Up @@ -68,7 +73,7 @@ func dataProjectRead(ctx context.Context, d *schema.ResourceData, meta interface
if !ok {
return diag.Errorf("either 'name' or 'id' must be specified")
}
project, err = getProjectByName(name.(string), meta)
project, err = getProjectByName(name.(string), d.Get("parent_project_name").(string), meta)
if err != nil {
return diag.Errorf("%v", err)
}
Expand All @@ -81,7 +86,27 @@ func dataProjectRead(ctx context.Context, d *schema.ResourceData, meta interface
return nil
}

func getProjectByName(name interface{}, meta interface{}) (client.Project, error) {
func filterByParentProjectName(name string, parentName string, projects []client.Project, meta interface{}) ([]client.Project, error) {
filteredProjects := make([]client.Project, 0)
for _, project := range projects {
if len(project.ParentProjectId) == 0 {
continue
}

parentProject, err := getProjectById(project.ParentProjectId, meta)
if err != nil {
return nil, err
}

if parentProject.Name == parentName {
filteredProjects = append(filteredProjects, project)
}
}

return filteredProjects, nil
}

func getProjectByName(name string, parentName string, meta interface{}) (client.Project, error) {
apiClient := meta.(client.ApiClientInterface)
projects, err := apiClient.Projects()
if err != nil {
Expand All @@ -90,17 +115,27 @@ func getProjectByName(name interface{}, meta interface{}) (client.Project, error

projectsByName := make([]client.Project, 0)
for _, candidate := range projects {
if candidate.Name == name.(string) && !candidate.IsArchived {
if candidate.Name == name && !candidate.IsArchived {
projectsByName = append(projectsByName, candidate)
}
}

if len(parentName) > 0 {
// Too many results. Use parentName filter to reduce the results.
projectsByName, err = filterByParentProjectName(name, parentName, projectsByName, meta)
if err != nil {
return client.Project{}, err
}
}

if len(projectsByName) > 1 {
return client.Project{}, fmt.Errorf("found multiple Projects for name: %s. Use ID instead or make sure Project names are unique %v", name, projectsByName)
return client.Project{}, fmt.Errorf("found multiple projects for name: %s. Use id or parent_name or make sure project names are unique %v", name, projectsByName)
}

if len(projectsByName) == 0 {
return client.Project{}, fmt.Errorf("could not find a project with name: %s", name)
}

return projectsByName[0], nil
}

Expand Down
88 changes: 87 additions & 1 deletion env0/data_project_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/env0/terraform-provider-env0/client"
"github.com/env0/terraform-provider-env0/client/http"
"github.com/golang/mock/gomock"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

Expand All @@ -28,6 +29,40 @@ func TestProjectDataSource(t *testing.T) {
IsArchived: true,
}

parentProject := client.Project{
Id: "id_parent1",
Name: "parent_project_name",
CreatedBy: "env0",
Role: "role0",
Description: "A project's description",
}

otherParentProject := client.Project{
Id: "id_parent2",
Name: "other_parent_project_name",
CreatedBy: "env0",
Role: "role0",
Description: "A project's description",
}

projectWithParent := client.Project{
Id: "id123",
Name: "same_name",
CreatedBy: "env0",
Role: "role0",
Description: "A project's description",
ParentProjectId: parentProject.Id,
}

otherProjectWithParent := client.Project{
Id: "id234",
Name: "same_name",
CreatedBy: "env0",
Role: "role0",
Description: "A project's description",
ParentProjectId: otherParentProject.Id,
}

projectDataByName := map[string]interface{}{"name": project.Name}
projectDataById := map[string]interface{}{"id": project.Id}

Expand Down Expand Up @@ -96,6 +131,31 @@ func TestProjectDataSource(t *testing.T) {
)
})

t.Run("By Name with Parent Name", func(t *testing.T) {
runUnitTest(t,
resource.TestCase{
Steps: []resource.TestStep{
{
Config: dataSourceConfigCreate(resourceType, resourceName, map[string]interface{}{"name": projectWithParent.Name, "parent_project_name": parentProject.Name}),
Check: resource.ComposeAggregateTestCheckFunc(
resource.TestCheckResourceAttr(accessor, "id", projectWithParent.Id),
resource.TestCheckResourceAttr(accessor, "name", projectWithParent.Name),
resource.TestCheckResourceAttr(accessor, "created_by", projectWithParent.CreatedBy),
resource.TestCheckResourceAttr(accessor, "role", projectWithParent.Role),
resource.TestCheckResourceAttr(accessor, "description", projectWithParent.Description),
resource.TestCheckResourceAttr(accessor, "parent_project_id", projectWithParent.ParentProjectId),
),
},
},
},
func(mock *client.MockApiClientInterface) {
mock.EXPECT().Projects().AnyTimes().Return([]client.Project{projectWithParent, otherProjectWithParent}, nil)
mock.EXPECT().Project(projectWithParent.ParentProjectId).AnyTimes().Return(parentProject, nil)
mock.EXPECT().Project(otherProjectWithParent.ParentProjectId).AnyTimes().Return(otherParentProject, nil)
},
)
})

t.Run("Throw error when no name or id is supplied", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(map[string]interface{}{}, "one of `id,name` must be specified"),
Expand All @@ -105,11 +165,25 @@ func TestProjectDataSource(t *testing.T) {

t.Run("Throw error when by name and more than one project exists", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(projectDataByName, "found multiple Projects for name"),
getErrorTestCase(projectDataByName, "found multiple projects for name"),
mockListProjectsCall([]client.Project{project, project}),
)
})

t.Run("Throw error when by name and more than one project and parent project name exist", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(map[string]interface{}{"name": projectWithParent.Name, "parent_project_name": parentProject.Name}, "found multiple projects for name"),
func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().Projects().Times(1).Return([]client.Project{projectWithParent, otherProjectWithParent, projectWithParent}, nil),
mock.EXPECT().Project(projectWithParent.ParentProjectId).Times(1).Return(parentProject, nil),
mock.EXPECT().Project(otherProjectWithParent.ParentProjectId).Times(1).Return(otherParentProject, nil),
mock.EXPECT().Project(projectWithParent.ParentProjectId).Times(1).Return(parentProject, nil),
)
},
)
})

t.Run("Throw error when by name and no projects found at all", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(projectDataByName, "could not find a project with name"),
Expand All @@ -125,6 +199,18 @@ func TestProjectDataSource(t *testing.T) {
)
})

t.Run("Throw error when by name and no projects found with that name and parent name", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(map[string]interface{}{"name": projectWithParent.Name, "parent_project_name": "noooo"}, "could not find a project with name"),
func(mock *client.MockApiClientInterface) {
gomock.InOrder(
mock.EXPECT().Projects().Times(1).Return([]client.Project{projectWithParent}, nil),
mock.EXPECT().Project(projectWithParent.ParentProjectId).Times(1).Return(parentProject, nil),
)
},
)
})

t.Run("Throw error when by id not found", func(t *testing.T) {
runUnitTest(t,
getErrorTestCase(projectDataById, "could not find a project with id: id0"),
Expand Down
2 changes: 1 addition & 1 deletion env0/resource_project.go
Original file line number Diff line number Diff line change
Expand Up @@ -215,7 +215,7 @@ func resourceProjectImport(ctx context.Context, d *schema.ResourceData, meta int
} else {
log.Println("[INFO] Resolving Project by name: ", id)

if project, err = getProjectByName(id, meta); err != nil {
if project, err = getProjectByName(id, "", meta); err != nil {
return nil, err
}
}
Expand Down
16 changes: 16 additions & 0 deletions tests/integration/002_project/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,22 @@ data "env0_project" "data_by_id" {
id = env0_project.test_project.id
}

resource "env0_project" "test_project_other" {
name = "Test-Project-${random_string.random.result}-other"
description = "Test Description"
}

resource "env0_project" "test_sub_project_other" {
name = "Test-Sub-Project-${random_string.random.result}"
description = "Test Description"
parent_project_id = env0_project.test_project_other.id
}

data "env0_project" "data_by_name_with_parent_name" {
name = env0_project.test_sub_project_other.name
parent_project_name = env0_project.test_project_other.name
}

output "test_project_name" {
value = replace(env0_project.test_project.name, random_string.random.result, "")
}
Expand Down

0 comments on commit ead551f

Please sign in to comment.