Skip to content

Adding team resource functionality #120

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

Merged
merged 11 commits into from
Oct 14, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
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
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/terraform-providers/terraform-provider-grafana
go 1.14

require (
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/terraform v0.12.2
)
10 changes: 2 additions & 8 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01 h1:OgCNGSnEalfkR
github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56 h1:+XIU6AjxfFIu3yOHC+M1Hwz7dUAs5zc/ugJ9cCfvUNA=
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56/go.mod h1:jFjwT3lvwl4JKqCw3guRJvlQ1/fmhER1h3Zgix3z7jw=
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea h1:DB+ppVl+w8B5LtFmpjyuPf9zT6t5j95RzwuP/ypYZeU=
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea/go.mod h1:jFjwT3lvwl4JKqCw3guRJvlQ1/fmhER1h3Zgix3z7jw=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
Expand Down Expand Up @@ -298,12 +298,6 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
github.com/nytm/go-grafana-api v0.2.0 h1:Aa5h2zMKDGjY1gS0bfesAfwSeRdTZG1GF8it+Nkd6l0=
github.com/nytm/go-grafana-api v0.2.0/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
github.com/nytm/go-grafana-api v0.3.1 h1:UMQYx88ZIpAxW2GunD2JgNve4QJt/T31PQXIyEtxzEs=
github.com/nytm/go-grafana-api v0.3.1/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
github.com/nytm/go-grafana-api v0.5.0 h1:8pIbNPNDguBa4aUNxcYl0GN247W6PXMvsOwiRBmk1sE=
github.com/nytm/go-grafana-api v0.5.0/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
Expand Down
1 change: 1 addition & 0 deletions grafana/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ func Provider() terraform.ResourceProvider {
"grafana_data_source": ResourceDataSource(),
"grafana_folder": ResourceFolder(),
"grafana_organization": ResourceOrganization(),
"grafana_team": ResourceTeam(),
"grafana_user": ResourceUser(),
},

Expand Down
262 changes: 262 additions & 0 deletions grafana/resource_team.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
package grafana

import (
"errors"
"fmt"
"log"
"strconv"
"strings"

gapi "github.com/grafana/grafana-api-golang-client"
"github.com/hashicorp/terraform/helper/schema"
)

type TeamMember struct {
ID int64
Email string
}

type MemberChange struct {
Type ChangeMemberType
Member TeamMember
}

type ChangeMemberType int8

const (
AddMember ChangeMemberType = iota
RemoveMember
)

func ResourceTeam() *schema.Resource {
return &schema.Resource{
Create: CreateTeam,
Read: ReadTeam,
Update: UpdateTeam,
Delete: DeleteTeam,
Exists: ExistsTeam,
Importer: &schema.ResourceImporter{
State: ImportTeam,
},

Schema: map[string]*schema.Schema{
"team_id": {
Type: schema.TypeInt,
Computed: true,
},
"name": {
Type: schema.TypeString,
Required: true,
},
"email": {
Type: schema.TypeString,
Optional: true,
},
"members": {
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}

func CreateTeam(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gapi.Client)
name := d.Get("name").(string)
email := d.Get("email").(string)
teamID, err := client.AddTeam(name, email)
if err != nil {
return err
}

d.SetId(strconv.FormatInt(teamID, 10))
return UpdateMembers(d, meta)
}

func ReadTeam(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gapi.Client)
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
resp, err := client.Team(teamID)
if err != nil && strings.HasPrefix(err.Error(), "status: 404") {
log.Printf("[WARN] removing team %s from state because it no longer exists in grafana", d.Id())
d.SetId("")
return nil
}
if err != nil {
return err
}
d.Set("name", resp.Name)
d.Set("email", resp.Email)
if err := ReadMembers(d, meta); err != nil {
return err
}
return nil
}

func UpdateTeam(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gapi.Client)
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
if d.HasChange("name") || d.HasChange("email") {
name := d.Get("name").(string)
email := d.Get("email").(string)
err := client.UpdateTeam(teamID, name, email)
if err != nil {
return err
}
}
return UpdateMembers(d, meta)
}

func DeleteTeam(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gapi.Client)
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
return client.DeleteTeam(teamID)
}

func ExistsTeam(d *schema.ResourceData, meta interface{}) (bool, error) {
client := meta.(*gapi.Client)
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
_, err := client.Team(teamID)
if err != nil && strings.HasPrefix(err.Error(), "status: 404") {
return false, nil
}
if err != nil {
return false, err
}
return true, err
}

func ImportTeam(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
exists, err := ExistsTeam(d, meta)
if err != nil || !exists {
return nil, errors.New(fmt.Sprintf("Error: Unable to import Grafana Team: %s.", err))
}
err = ReadTeam(d, meta)
if err != nil {
return nil, err
}
return []*schema.ResourceData{d}, nil
}

func ReadMembers(d *schema.ResourceData, meta interface{}) error {
client := meta.(*gapi.Client)
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
teamMembers, err := client.TeamMembers(teamID)
if err != nil {
return err
}
memberSlice := []string{}
for _, teamMember := range teamMembers {
memberSlice = append(memberSlice, teamMember.Email)
}
d.Set("members", memberSlice)

return nil
}

func UpdateMembers(d *schema.ResourceData, meta interface{}) error {
stateMembers, configMembers, err := collectMembers(d)
if err != nil {
return err
}
//compile the list of differences between current state and config
changes := memberChanges(stateMembers, configMembers)
//retrieves the corresponding user IDs based on the email provided
changes, err = addMemberIdsToChanges(meta, changes)
if err != nil {
return err
}
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
//now we can make the corresponding updates so current state matches config
return applyMemberChanges(meta, teamID, changes)
}

func collectMembers(d *schema.ResourceData) (map[string]TeamMember, map[string]TeamMember, error) {
stateMembers, configMembers := make(map[string]TeamMember), make(map[string]TeamMember)

// Get the lists of team members read in from Grafana state (old) and configured (new)
state, config := d.GetChange("members")
for _, u := range state.([]interface{}) {
login := u.(string)
// Sanity check that a member isn't specified twice within a team
if _, ok := stateMembers[login]; ok {
return nil, nil, errors.New(fmt.Sprintf("Error: Team Member '%s' cannot be specified multiple times.", login))
}
stateMembers[login] = TeamMember{0, login}
}
for _, u := range config.([]interface{}) {
login := u.(string)
// Sanity check that a member isn't specified twice within a team
if _, ok := configMembers[login]; ok {
return nil, nil, errors.New(fmt.Sprintf("Error: Team Member '%s' cannot be specified multiple times.", login))
}
configMembers[login] = TeamMember{0, login}
}

return stateMembers, configMembers, nil
}

func memberChanges(stateMembers, configMembers map[string]TeamMember) []MemberChange {
var changes []MemberChange
for _, user := range configMembers {
_, ok := stateMembers[user.Email]
if !ok {
// Member doesn't exist in Grafana's state for the team, should be added.
changes = append(changes, MemberChange{AddMember, user})
continue
}
}
for _, user := range stateMembers {
if _, ok := configMembers[user.Email]; !ok {
// Member exists in Grafana's state for the team, but isn't
// present in the team configuration, should be removed.
changes = append(changes, MemberChange{RemoveMember, user})
}
}
return changes
}

func addMemberIdsToChanges(meta interface{}, changes []MemberChange) ([]MemberChange, error) {
client := meta.(*gapi.Client)
gUserMap := make(map[string]int64)
gUsers, err := client.Users()
if err != nil {
return nil, err
}
for _, u := range gUsers {
gUserMap[u.Email] = u.Id
}
var output []MemberChange

for _, change := range changes {
id, ok := gUserMap[change.Member.Email]
if !ok {
return nil, errors.New(fmt.Sprintf("Error adding user %s. User does not exist in Grafana.", change.Member.Email))
}

change.Member.ID = id
output = append(output, change)
}
return output, nil
}

func applyMemberChanges(meta interface{}, teamId int64, changes []MemberChange) error {
var err error
client := meta.(*gapi.Client)
for _, change := range changes {
u := change.Member
switch change.Type {
case AddMember:
err = client.AddTeamMember(teamId, u.ID)
case RemoveMember:
err = client.RemoveMemberFromTeam(teamId, u.ID)
}
if err != nil {
return err
}
}
return nil
}
Loading