Skip to content

Commit

Permalink
Merge pull request #144 from OpsLevel/member-roles
Browse files Browse the repository at this point in the history
Assign members with roles
  • Loading branch information
taimoor ahmad authored Nov 9, 2023
2 parents 1d3b343 + 96de972 commit 2335d46
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 67 deletions.
3 changes: 3 additions & 0 deletions .changes/unreleased/Feature-20231109-111446.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Feature
body: Add support for adding members/memberships to 'opslevel_team' resources
time: 2023-11-09T11:14:46.698003-05:00
3 changes: 3 additions & 0 deletions .changes/unreleased/Removed-20231109-113736.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Removed
body: Remove out of date 'manager_email' field on resource 'opslevel_team'
time: 2023-11-09T11:37:36.282205-05:00
10 changes: 9 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,16 @@ provider "opslevel" {
resource "opslevel_team" "foo" {
name = "foo"
manager_email = "[email protected]"
responsibilities = "Responsible for foo frontend and backend"
member {
email = "[email protected]"
role = "manager"
}
member {
email = "[email protected]"
role = "contributor"
}
}
resource "opslevel_service" "foo-frontend" {
Expand Down
6 changes: 5 additions & 1 deletion examples/provider/provider.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,12 @@ provider "opslevel" {

resource "opslevel_team" "foo" {
name = "foo"
manager_email = "[email protected]"
responsibilities = "Responsible for foo frontend and backend"

member {
email = "[email protected]"
role = "manager"
}
}

resource "opslevel_service" "foo-frontend" {
Expand Down
6 changes: 5 additions & 1 deletion examples/resources/opslevel_service/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,13 @@ data "opslevel_tier" "tier3" {

resource "opslevel_team" "foo" {
name = "foo"
manager_email = "[email protected]"
responsibilities = "Responsible for foo frontend and backend"
aliases = ["bar", "baz"]

member {
email = "[email protected]"
role = "manager"
}
}

resource "opslevel_service" "foo" {
Expand Down
11 changes: 9 additions & 2 deletions examples/resources/opslevel_team/resource.tf
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,18 @@ data "opslevel_team" "parent" {

resource "opslevel_team" "example" {
name = "foo"
manager_email = "[email protected]"
members = ["[email protected]", "[email protected]"]
responsibilities = "Responsible for foo frontend and backend"
aliases = ["bar", "baz"]
parent = data.opslevel_team.parent.id

member {
email = "[email protected]"
role = "manager"
}
member {
email = "[email protected]"
role = "contributor"
}
}

output "team" {
Expand Down
139 changes: 78 additions & 61 deletions opslevel/resource_opslevel_team.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,12 +36,6 @@ func resourceTeam() *schema.Resource {
ForceNew: false,
Required: true,
},
"manager_email": {
Type: schema.TypeString,
Description: "The email of the user who manages the team.",
ForceNew: false,
Optional: true,
},
"responsibilities": {
Type: schema.TypeString,
Description: "A description of what the team is responsible for.",
Expand All @@ -62,12 +56,24 @@ func resourceTeam() *schema.Resource {
ForceNew: false,
Optional: true,
},
"members": {
Type: schema.TypeSet,
Description: "List of user emails that belong to the team. This list must contain the 'manager_email' value.",
Elem: &schema.Schema{Type: schema.TypeString},
ForceNew: false,
"member": {
Type: schema.TypeList,
Description: "List of members in the team with email address and role.",
Optional: true,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"email": {
Type: schema.TypeString,
Description: "The email address or ID of the user to add to a team.",
Required: true,
},
"role": {
Type: schema.TypeString,
Description: "The type of relationship this membership implies.",
Required: true,
},
},
},
},
"parent": {
Type: schema.TypeString,
Expand Down Expand Up @@ -108,70 +114,90 @@ func reconcileTeamAliases(d *schema.ResourceData, team *opslevel.Team, client *o
return nil
}

func collectMembersFromTeam(team *opslevel.Team) []string {
members := []string{}
func collectMembersFromTeam(team *opslevel.Team) []opslevel.TeamMembershipUserInput {
members := []opslevel.TeamMembershipUserInput{}

for _, user := range team.Members.Nodes {
members = append(members, user.Email)
for _, user := range team.Memberships.Nodes {
member := opslevel.TeamMembershipUserInput{
User: opslevel.UserIdentifierInput{
Email: user.User.Email,
},
Role: string(user.Role),
}
members = append(members, member)
}
return members
}

func memberInArray(member opslevel.TeamMembershipUserInput, array []opslevel.TeamMembershipUserInput) bool {
for _, m := range array {
if m.User.Email == member.User.Email && m.Role == member.Role {
return true
}
}
return false
}

func reconcileTeamMembership(d *schema.ResourceData, team *opslevel.Team, client *opslevel.Client) error {
expectedMembers := expandStringArray(d.Get("members").(*schema.Set).List())
expectedMembers := []opslevel.TeamMembershipUserInput{}
existingMembers := collectMembersFromTeam(team)

membersToRemove := []string{}
membersToAdd := []string{}
if members, ok := d.GetOk("member"); ok {
membersInput := members.([]interface{})

for _, m := range membersInput {
memberInput := m.(map[string]interface{})
member := opslevel.TeamMembershipUserInput{
User: opslevel.UserIdentifierInput{
Email: memberInput["email"].(string),
},
Role: memberInput["role"].(string),
}
expectedMembers = append(expectedMembers, member)
}
}

membersToRemove := []opslevel.TeamMembershipUserInput{}
membersToAdd := []opslevel.TeamMembershipUserInput{}

for _, existingMember := range existingMembers {
if stringInArray(existingMember, expectedMembers) {
if memberInArray(existingMember, expectedMembers) {
continue
}

membersToRemove = append(membersToRemove, existingMember)
}

for _, expectedMember := range expectedMembers {

if stringInArray(expectedMember, existingMembers) {
if memberInArray(expectedMember, existingMembers) {
continue
}
membersToAdd = append(membersToAdd, expectedMember)
}

if len(membersToAdd) != 0 {
_, err := client.AddMembers(&team.TeamId, membersToAdd)
// warning: must remove memberships before adding them.
// this prevents a bug where the role of a user changes
// but the user isn't added back and disappears.
if len(membersToRemove) != 0 {
_, err := client.RemoveMemberships(&team.TeamId, membersToRemove...)
if err != nil {
return err
}
}

if len(membersToRemove) != 0 {
_, err := client.RemoveMembers(&team.TeamId, membersToRemove)
if len(membersToAdd) != 0 {
_, err := client.AddMemberships(&team.TeamId, membersToAdd...)
if err != nil {
return err
}
}
return nil
}

func validateMembershipState(d *schema.ResourceData) error {
if membersSet, ok := d.GetOk("members"); ok {
if managerEmail, ok := d.GetOk("manager_email"); ok {
memberEmails := expandStringArray(membersSet.(*schema.Set).List())
if !stringInArray(managerEmail.(string), memberEmails) {
return errors.New("The 'manager_email' value is required as a member")
}
}
}
return nil
}

func resourceTeamCreate(d *schema.ResourceData, client *opslevel.Client) error {
input := opslevel.TeamCreateInput{
Name: d.Get("name").(string),
ManagerEmail: d.Get("manager_email").(string),
Responsibilities: d.Get("responsibilities").(string),
}
if _, ok := d.GetOk("group"); ok {
Expand All @@ -181,11 +207,6 @@ func resourceTeamCreate(d *schema.ResourceData, client *opslevel.Client) error {
input.ParentTeam = opslevel.NewIdentifier(parentTeam.(string))
}

membershipValidationErr := validateMembershipState(d)
if membershipValidationErr != nil {
return membershipValidationErr
}

resource, err := client.CreateTeam(input)
if err != nil {
return err
Expand All @@ -197,11 +218,9 @@ func resourceTeamCreate(d *schema.ResourceData, client *opslevel.Client) error {
return aliasesErr
}

if _, ok := d.GetOk("members"); ok {
membersErr := reconcileTeamMembership(d, resource, client)
if membersErr != nil {
return membersErr
}
membersErr := reconcileTeamMembership(d, resource, client)
if membersErr != nil {
return membersErr
}

return resourceTeamRead(d, client)
Expand All @@ -221,9 +240,6 @@ func resourceTeamRead(d *schema.ResourceData, client *opslevel.Client) error {
if err := d.Set("name", resource.Name); err != nil {
return err
}
if err := d.Set("manager_email", resource.Manager.Email); err != nil {
return err
}
if err := d.Set("responsibilities", resource.Responsibilities); err != nil {
return err
}
Expand Down Expand Up @@ -254,8 +270,17 @@ func resourceTeamRead(d *schema.ResourceData, client *opslevel.Client) error {
}
}

if _, ok := d.GetOk("members"); ok {
if err := d.Set("members", collectMembersFromTeam(resource)); err != nil {
if _, ok := d.GetOk("member"); ok {
members := collectMembersFromTeam(resource)
memberOutput := []map[string]interface{}{}
for _, m := range members {
mOutput := make(map[string]interface{})
mOutput["email"] = m.User.Email
mOutput["role"] = m.Role
memberOutput = append(memberOutput, mOutput)
}

if err := d.Set("member", memberOutput); err != nil {
return err
}
}
Expand All @@ -269,17 +294,9 @@ func resourceTeamUpdate(d *schema.ResourceData, client *opslevel.Client) error {
Id: opslevel.ID(id),
}

membershipValidationErr := validateMembershipState(d)
if membershipValidationErr != nil {
return membershipValidationErr
}

if d.HasChange("name") {
input.Name = d.Get("name").(string)
}
if d.HasChange("manager_email") {
input.ManagerEmail = d.Get("manager_email").(string)
}
if d.HasChange("responsibilities") {
input.Responsibilities = d.Get("responsibilities").(string)
}
Expand All @@ -306,7 +323,7 @@ func resourceTeamUpdate(d *schema.ResourceData, client *opslevel.Client) error {
}
}

if d.HasChange("members") {
if d.HasChange("member") {
membersErr := reconcileTeamMembership(d, resource, client)
if membersErr != nil {
return membersErr
Expand Down

0 comments on commit 2335d46

Please sign in to comment.