From dbb559745896cd3c66dd9cead3cdb2f978316143 Mon Sep 17 00:00:00 2001 From: Arnold Bechtoldt Date: Thu, 13 Aug 2020 19:01:26 +0200 Subject: [PATCH] Add new resource opsgenie_team_membership, refs #65 Signed-off-by: Arnold Bechtoldt --- opsgenie/provider.go | 1 + opsgenie/resource_opsgenie_team_membership.go | 159 ++++++++++++++ .../resource_opsgenie_team_membershop_test.go | 194 ++++++++++++++++++ opsgenie/resource_opsgenie_user.go | 2 + opsgenie/resource_opsgenie_user_test.go | 2 +- website/docs/r/team_membership.html.markdown | 64 ++++++ website/opsgenie.erb | 3 + 7 files changed, 424 insertions(+), 1 deletion(-) create mode 100644 opsgenie/resource_opsgenie_team_membership.go create mode 100644 opsgenie/resource_opsgenie_team_membershop_test.go create mode 100644 website/docs/r/team_membership.html.markdown diff --git a/opsgenie/provider.go b/opsgenie/provider.go index 3971df9f..01688316 100644 --- a/opsgenie/provider.go +++ b/opsgenie/provider.go @@ -26,6 +26,7 @@ func Provider() terraform.ResourceProvider { "opsgenie_team": resourceOpsGenieTeam(), "opsgenie_team_routing_rule": resourceOpsGenieTeamRoutingRule(), + "opsgenie_team_membership": resourceOpsGenieTeamMembership(), "opsgenie_user": resourceOpsGenieUser(), "opsgenie_user_contact": resourceOpsGenieUserContact(), "opsgenie_notification_policy": resourceOpsGenieNotificationPolicy(), diff --git a/opsgenie/resource_opsgenie_team_membership.go b/opsgenie/resource_opsgenie_team_membership.go new file mode 100644 index 00000000..4bf5202a --- /dev/null +++ b/opsgenie/resource_opsgenie_team_membership.go @@ -0,0 +1,159 @@ +package opsgenie + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/opsgenie/opsgenie-go-sdk-v2/team" + "log" + "strings" +) + +func resourceOpsGenieTeamMembership() *schema.Resource { //TODO encode the e-mail addrs (e.g. because of +)? https://github.com/opsgenie/opsgenie-go-sdk-v2/issues/62 + return &schema.Resource{ + Create: resourceOpsGenieTeamMembershipCreate, + Read: handleNonExistentResource(resourceOpsGenieTeamMembershipRead), + //Update: resourceOpsGenieTeamMembershipUpdate, // requires https://github.com/opsgenie/opsgenie-go-sdk-v2/issues/59 + Delete: resourceOpsGenieTeamMembershipDelete, + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + Schema: map[string]*schema.Schema{ + "user_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "role": { + Type: schema.TypeString, + Optional: true, + Default: "user", + ForceNew: true, + }, + "team_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + }, + } +} + +func resourceOpsGenieTeamMembershipCreate(d *schema.ResourceData, meta interface{}) error { + + userID := d.Get("user_id").(string) + role := d.Get("role").(string) + teamID := d.Get("team_id").(string) + + log.Printf("[INFO] Adding user %q to team %q", teamID, userID) + + client, err := team.NewClient(meta.(*OpsgenieClient).client.Config) + if err != nil { + return err + } + + // add member to team + _, err = client.AddMember(context.Background(), &team.AddTeamMemberRequest{ + TeamIdentifierType: team.Id, + TeamIdentifierValue: teamID, + User: team.User{ + ID: userID, + }, + Role: role, + }) + if err != nil { + return err + } + + d.SetId(buildTwoPartID(teamID, userID)) + + return resourceOpsGenieTeamMembershipRead(d, meta) +} + +func resourceOpsGenieTeamMembershipRead(d *schema.ResourceData, meta interface{}) error { + + teamID, userID, err := parseTwoPartID(d.Id(), "teamID", "userID") + if err != nil { + return err + } + + getRequest := &team.GetTeamRequest{ + IdentifierType: team.Id, + IdentifierValue: teamID, + } + + log.Printf("[INFO] Retrieving membership of user %q in team %q", userID, teamID) + + client, err := team.NewClient(meta.(*OpsgenieClient).client.Config) + if err != nil { + return err + } + + getResponse, err := client.Get(context.Background(), getRequest) + if err != nil { + return err + } + + role, err := getUserRole(userID, teamID, getResponse.Members) + if err != nil { + return err + } + + d.Set("user_id", userID) + d.Set("role", role) + d.Set("team_id", teamID) + + return nil +} + +func resourceOpsGenieTeamMembershipDelete(d *schema.ResourceData, meta interface{}) error { + userID := d.Get("user_id").(string) + teamID := d.Get("team_id").(string) + + log.Printf("[INFO] Deleting membership of user %q in team %q", userID, teamID) + + client, err := team.NewClient(meta.(*OpsgenieClient).client.Config) + if err != nil { + return err + } + + _, err = client.RemoveMember(context.Background(), &team.RemoveTeamMemberRequest{ + TeamIdentifierType: team.Id, + TeamIdentifierValue: teamID, + MemberIdentifierType: team.Id, + MemberIdentifierValue: userID, + }) + if err != nil { + return err + } + + return nil +} + +func getUserRole(userID string, teamID string, input []team.Member) (string, error) { + role := "" + + for _, inputMember := range input { + if inputMember.User.ID == userID { + role = inputMember.Role + return role, nil + } + } + + return "", fmt.Errorf("did not found user %q in team %q (%#v)", userID, teamID, input) +} + +// format the strings into an id `a:b` +func buildTwoPartID(a, b string) string { + return fmt.Sprintf("%s:%s", a, b) +} + +// return the pieces of id `left:right` as left, right +func parseTwoPartID(id, left, right string) (string, string, error) { + parts := strings.SplitN(id, ":", 2) + if len(parts) != 2 { + return "", "", fmt.Errorf("unexpected ID format %q, expected %s:%s", id, left, right) + } + + return parts[0], parts[1], nil +} diff --git a/opsgenie/resource_opsgenie_team_membershop_test.go b/opsgenie/resource_opsgenie_team_membershop_test.go new file mode 100644 index 00000000..cf2fd4f8 --- /dev/null +++ b/opsgenie/resource_opsgenie_team_membershop_test.go @@ -0,0 +1,194 @@ +package opsgenie + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-sdk/terraform" + "github.com/opsgenie/opsgenie-go-sdk-v2/team" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" +) + +func TestAccOpsGenieTeamMembership_basic(t *testing.T) { + rString := acctest.RandString(6) + + resource.Test(t, resource.TestCase{ + Providers: testAccProviders, + + Steps: []resource.TestStep{ + { + Config: testAccOpsGenieTeamMembership_basic(rString), + Destroy: false, + Check: resource.ComposeTestCheckFunc( + testCheckOpsGenieTeamExists("opsgenie_team.monkeys"), + testCheckOpsGenieUserExists("opsgenie_user.kong"), + testCheckOpsGenieTeamMembershipExists("opsgenie_team_membership.chaos_kong", "opsgenie_team.monkeys", "opsgenie_user.kong"), + ), + }, + { + Config: testAccOpsGenieTeamMembership_basicUpdated(rString), + Check: resource.ComposeTestCheckFunc( + testCheckOpsGenieTeamExists("opsgenie_team.monkeys"), + testCheckOpsGenieUserExists("opsgenie_user.kong"), + testCheckOpsGenieTeamMembershipExists("opsgenie_team_membership.chaos_kong", "opsgenie_team.monkeys", "opsgenie_user.kong"), + ), + }, + { + Config: testAccOpsGenieTeamMembership_basicWithoutMembership(rString), + Check: resource.ComposeTestCheckFunc( + testCheckOpsGenieTeamExists("opsgenie_team.monkeys"), + testCheckOpsGenieUserExists("opsgenie_user.kong"), + testCheckOpsGenieTeamMembershipRemoved("opsgenie_team_membership.chaos_kong", "opsgenie_team.monkeys", "opsgenie_user.kong"), + ), + }, + }, + }) +} + +func testCheckOpsGenieTeamMembershipExists(membershipResource string, teamResource string, userResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + rsMembership, ok := s.RootModule().Resources[membershipResource] + if !ok { + return fmt.Errorf("not found: %s", membershipResource) + } + + rsTeam, ok := s.RootModule().Resources[teamResource] + if !ok { + return fmt.Errorf("not found: %s", teamResource) + } + teamName := rsTeam.Primary.Attributes["name"] + + rsUser, ok := s.RootModule().Resources[userResource] + if !ok { + return fmt.Errorf("not found: %s", userResource) + } + userName := rsUser.Primary.Attributes["username"] + + client, err := team.NewClient(testAccProvider.Meta().(*OpsgenieClient).client.Config) + if err != nil { + return err + } + req := team.GetTeamRequest{ + IdentifierType: team.Name, + IdentifierValue: teamName, + } + getResponse, err := client.Get(context.Background(), &req) + if err != nil { + return fmt.Errorf("failed to detect team membership for user %q in team %q: %s", userName, teamName, err) + } + + // compare what we've actually done + if len(getResponse.Members) != 1 { + return fmt.Errorf("there's no team membership at all. something went wrong :(") + } + + if getResponse.Members[0].User.Username != userName { + return fmt.Errorf("expected userName in team membership (%q) doesn't match actual username (%q)", userName, getResponse.Members[0].User.Username) + } + + if getResponse.Members[0].User.ID != rsUser.Primary.ID { + return fmt.Errorf("expected user ID in team membership (%q) doesn't match actual username (%q)", rsUser.Primary.ID, getResponse.Members[0].User.ID) + } + + if getResponse.Members[0].Role != rsMembership.Primary.Attributes["role"] { + return fmt.Errorf("expected user role in team membership (%q) doesn't match actual user role (%q)", rsMembership.Primary.Attributes["role"], getResponse.Members[0].Role) + } + + return nil + } +} + +func testCheckOpsGenieTeamMembershipRemoved(membershipResource string, teamResource string, userResource string) resource.TestCheckFunc { + return func(s *terraform.State) error { + + _, ok := s.RootModule().Resources[membershipResource] + if ok { + return fmt.Errorf("resource %s still in state. this is bad", membershipResource) + } + + rsTeam, ok := s.RootModule().Resources[teamResource] + if !ok { + return fmt.Errorf("not found: %s", teamResource) + } + teamName := rsTeam.Primary.Attributes["name"] + + client, err := team.NewClient(testAccProvider.Meta().(*OpsgenieClient).client.Config) + if err != nil { + return err + } + req := team.GetTeamRequest{ + IdentifierType: team.Name, + IdentifierValue: teamName, + } + getResponse, err := client.Get(context.Background(), &req) + if err != nil { + return fmt.Errorf("failed to verify team memberships of team %q: %s", teamName, err) + } + + // compare what we've actually done + if len(getResponse.Members) != 0 { + return fmt.Errorf("there is still an unexpected number of team membership(s) (%#v). something went wrong :(", getResponse.Members) + } + + return nil + } +} + +func testAccOpsGenieTeamMembership_basic(rString string) string { + return fmt.Sprintf(` +resource "opsgenie_team" "monkeys" { + name = "monkeys-%s" + description = "They exist." + ignore_members = true +} +resource "opsgenie_user" "kong" { + username = "kong-%s@test.example.com" + full_name = "Chaos Kong" + role = "User" +} +resource "opsgenie_team_membership" "chaos_kong" { + username = opsgenie_user.kong.username + role = "user" + team = opsgenie_team.monkeys.name +} +`, rString, rString) +} + +func testAccOpsGenieTeamMembership_basicUpdated(rString string) string { + return fmt.Sprintf(` +resource "opsgenie_team" "monkeys" { + name = "monkeys-%s" + description = "They exist." + ignore_members = true +} +resource "opsgenie_user" "kong" { + username = "kong-%s@test.example.com" + full_name = "Chaos Kong" + role = "User" +} +resource "opsgenie_team_membership" "chaos_kong" { + username = opsgenie_user.kong.username + role = "admin" + team = opsgenie_team.monkeys.name +} +`, rString, rString) +} + +func testAccOpsGenieTeamMembership_basicWithoutMembership(rString string) string { + return fmt.Sprintf(` +resource "opsgenie_team" "monkeys" { + name = "monkeys-%s" + description = "They exist." + ignore_members = true + depends_on = [opsgenie_user.kong] # Just a hack for the test to destroy resources in the right order +} +resource "opsgenie_user" "kong" { + username = "kong-%s@test.example.com" + full_name = "Chaos Kong" + role = "User" +} +`, rString, rString) +} diff --git a/opsgenie/resource_opsgenie_user.go b/opsgenie/resource_opsgenie_user.go index 3ffaeb5f..2e7b9667 100644 --- a/opsgenie/resource_opsgenie_user.go +++ b/opsgenie/resource_opsgenie_user.go @@ -53,6 +53,8 @@ func resourceOpsGenieUser() *schema.Resource { } func resourceOpsGenieUserCreate(d *schema.ResourceData, meta interface{}) error { + //TODO OGS-1629: Atlassian systems reset full_name after a certain time after creating a new OpsGenie user. + // This may lead to unexpected behaviour, e.g. when running a subsequent "tf apply" or executing our acceptance tests client, err := user.NewClient(meta.(*OpsgenieClient).client.Config) if err != nil { diff --git a/opsgenie/resource_opsgenie_user_test.go b/opsgenie/resource_opsgenie_user_test.go index 92a6e96b..26a6b8a5 100644 --- a/opsgenie/resource_opsgenie_user_test.go +++ b/opsgenie/resource_opsgenie_user_test.go @@ -154,7 +154,7 @@ func testCheckOpsGenieUserExists(name string) resource.TestCheckFunc { if err != nil { return fmt.Errorf("Bad: User %q (username: %q) does not exist", id, username) } else { - log.Printf("User found :%s ", result.Username) + log.Printf("User found: %s", result.Username) } return nil diff --git a/website/docs/r/team_membership.html.markdown b/website/docs/r/team_membership.html.markdown new file mode 100644 index 00000000..525f8e93 --- /dev/null +++ b/website/docs/r/team_membership.html.markdown @@ -0,0 +1,64 @@ +--- +layout: "opsgenie" +page_title: "Opsgenie: opsgenie_team_membership" +sidebar_current: "docs-opsgenie-resource-team-membership" +description: |- + Manages team memberships for users. +--- + +# opsgenie\_team\_membership + +Manages team memberships for users. + +## Example Usage + +```hcl +resource "opsgenie_user" "first" { + username = "user@test.example.com" + full_name = "name " + role = "User" +} + +resource "opsgenie_user" "second" { + username = "test@test.example.com" + full_name = "name " + role = "User" +} + +resource "opsgenie_team" "test" { + name = "example" + description = "This team deals with all the things" + ignore_members = true # we're using opsgenie_team_membership for it +} + +resource "opsgenie_team_membership" "first" { + user_id = opsgenie_user.first.id + team_id = opsgenie_team.test.id +} + +resource "opsgenie_team_membership" "second" { + user_id = opsgenie_user.second.id + role = "admin" + team_id = opsgenie_team.test.id +} +``` + +## Argument Reference + +The following arguments are supported: + +* `user_id` - (Required) The ID of an user. + +* `role` - (Optional) The role for the user within the Team - can be either 'admin' or 'user', defaults to 'user' if not set. + +* `team_id` - (Required) The ID of a team the user should be member of. + +## Attributes Reference + +The following attributes are exported: + +* `id` - The virtual ID of the Opsgenie Team Membership. + +## Import + +Import is not supported for team memberships. \ No newline at end of file diff --git a/website/opsgenie.erb b/website/opsgenie.erb index 281544d7..b16d8f92 100644 --- a/website/opsgenie.erb +++ b/website/opsgenie.erb @@ -44,6 +44,9 @@ > opsgenie_team + > + opsgenie_team_membership + > opsgenie_team_routing_rule