Skip to content

Commit

Permalink
Feat: user team assignment (#367)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomerHeber committed May 12, 2022
1 parent d61df18 commit 5a72516
Show file tree
Hide file tree
Showing 5 changed files with 527 additions and 2 deletions.
6 changes: 4 additions & 2 deletions client/team.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,17 @@ type TeamCreatePayload struct {
}

type TeamUpdatePayload struct {
Name string `json:"name"`
Description string `json:"description"`
Name string `json:"name"`
Description string `json:"description"`
UserIds []string `json:"userIds,omitempty"`
}

type Team struct {
Id string `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
OrganizationId string `json:"organizationId"`
Users []User `json:"users"`
}

func (client *ApiClient) TeamCreate(payload TeamCreatePayload) (Team, error) {
Expand Down
1 change: 1 addition & 0 deletions env0/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ func Provider(version string) plugin.ProviderFunc {
"env0_api_key": resourceApiKey(),
"env0_organization_policy": resourceOrganizationPolicy(),
"env0_agent_project_assignment": resourceAgentProjectAssignment(),
"env0_user_team_assignment": resourceUserTeamAssignment(),
},
}

Expand Down
219 changes: 219 additions & 0 deletions env0/resource_user_team_assignment.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
package env0

import (
"context"
"fmt"
"log"
"strings"
"sync"

"github.com/env0/terraform-provider-env0/client"
"github.com/env0/terraform-provider-env0/client/http"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

// Updating a user team assignment overrides all user team assignments
// Therefore, must extract all existing users and append/remove the created/deleted user.
// Since Terraform may run the assignments in parallel a mutex is required.
var utaLock sync.Mutex

// id is <user_id>_<team_id>

type UserTeamAssignment struct {
UserId string `json:"user_id"`
TeamId string `json:"team_id"`
}

func GetUserTeamAssignmentId(userId string, teamId string) string {
return userId + "_" + teamId
}

func (a *UserTeamAssignment) GetId() string {
return GetUserTeamAssignmentId(a.UserId, a.TeamId)
}

func GetUserTeamAssignmentFromId(id string) (*UserTeamAssignment, error) {
splitUserTeam := strings.Split(id, "_")
if len(splitUserTeam) != 2 {
return nil, fmt.Errorf("the id %v is invalid must be <user_id>_<team_id>", id)
}
return &UserTeamAssignment{
UserId: splitUserTeam[0],
TeamId: splitUserTeam[1],
}, nil
}

func resourceUserTeamAssignment() *schema.Resource {
return &schema.Resource{
CreateContext: resourceUserTeamAssignmentCreate,
ReadContext: resourceUserTeamAssignmentRead,
DeleteContext: resourceUserTeamAssignmentDelete,

Importer: &schema.ResourceImporter{StateContext: resourceUserTeamAssignmentImport},

Schema: map[string]*schema.Schema{
"user_id": {
Type: schema.TypeString,
Description: "id of the user",
Required: true,
ForceNew: true,
},
"team_id": {
Type: schema.TypeString,
Description: "id of the team",
Required: true,
ForceNew: true,
},
},
}
}

func resourceUserTeamAssignmentCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var newAssignment UserTeamAssignment
if err := readResourceData(&newAssignment, d); err != nil {
return diag.Errorf("schema resource data deserialization failed: %v", err)
}

apiClient := meta.(client.ApiClientInterface)

utaLock.Lock()
defer utaLock.Unlock()

team, err := apiClient.Team(newAssignment.TeamId)
if err != nil {
return diag.Errorf("could not get team: %v", err)
}

userIds := []string{newAssignment.UserId}

for _, user := range team.Users {
if user.UserId == newAssignment.UserId {
return diag.Errorf("assignment for user id %v and team id %v already exist", newAssignment.UserId, newAssignment.TeamId)
}
userIds = append(userIds, user.UserId)
}

if _, err := apiClient.TeamUpdate(team.Id, client.TeamUpdatePayload{
Name: team.Name,
Description: team.Description,
UserIds: userIds,
}); err != nil {
return diag.Errorf("could not update team with new assignment: %v", err)
}

d.SetId(newAssignment.GetId())

return nil
}

func resourceUserTeamAssignmentRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
assignment, err := GetUserTeamAssignmentFromId(d.Id())
if err != nil {
return diag.Errorf("%v", err)
}

apiClient := meta.(client.ApiClientInterface)

utaLock.Lock()
defer utaLock.Unlock()

team, err := apiClient.Team(assignment.TeamId)
if err != nil {
return ResourceGetFailure("team", d, err)
}

found := false
for _, user := range team.Users {
if user.UserId == assignment.UserId {
found = true
break
}
}

if !found {
log.Printf("[WARN] Drift Detected: Terraform will remove %s from state", d.Id())
d.SetId("")
return nil
}

if err := writeResourceData(assignment, d); err != nil {
diag.Errorf("schema resource data serialization failed: %v", err)
}

return nil
}

func resourceUserTeamAssignmentDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
var assignment UserTeamAssignment
if err := readResourceData(&assignment, d); err != nil {
return diag.Errorf("schema resource data deserialization failed: %v", err)
}

apiClient := meta.(client.ApiClientInterface)

utaLock.Lock()
defer utaLock.Unlock()

team, err := apiClient.Team(assignment.TeamId)
if err != nil {
return diag.Errorf("could not get team: %v", err)
}

userIds := []string{}

for _, user := range team.Users {
if user.UserId == assignment.UserId {
continue
}
userIds = append(userIds, user.UserId)
}

if _, err := apiClient.TeamUpdate(team.Id, client.TeamUpdatePayload{
Name: team.Name,
Description: team.Description,
UserIds: userIds,
}); err != nil {
return diag.Errorf("could not update team with removed assignment: %v", err)
}

return nil
}

func resourceUserTeamAssignmentImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
assignment, err := GetUserTeamAssignmentFromId(d.Id())
if err != nil {
return nil, fmt.Errorf("%v", err)
}

apiClient := meta.(client.ApiClientInterface)

utaLock.Lock()
defer utaLock.Unlock()

team, err := apiClient.Team(assignment.TeamId)
if err != nil {
if frerr, ok := err.(*http.FailedResponseError); ok && frerr.NotFound() {
return nil, fmt.Errorf("team %v not found", assignment.TeamId)
}
return nil, err
}

found := false
for _, user := range team.Users {
if user.UserId == assignment.UserId {
found = true
break
}
}

if !found {
return nil, fmt.Errorf("user %v not assigned to team %v", assignment.UserId, assignment.TeamId)
}

if err := writeResourceData(assignment, d); err != nil {
diag.Errorf("schema resource data serialization failed: %v", err)
}

return []*schema.ResourceData{d}, nil
}
Loading

0 comments on commit 5a72516

Please sign in to comment.