Skip to content

Commit d27a4f1

Browse files
Merge pull request #120 from jonathan-dorsey/master
Adding team resource functionality
2 parents de7a0b7 + 22b362c commit d27a4f1

File tree

15 files changed

+654
-28
lines changed

15 files changed

+654
-28
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ module github.com/terraform-providers/terraform-provider-grafana
33
go 1.14
44

55
require (
6-
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56
6+
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea
77
github.com/hashicorp/hcl v1.0.0 // indirect
88
github.com/hashicorp/terraform v0.12.2
99
)

go.sum

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01 h1:OgCNGSnEalfkR
139139
github.com/gophercloud/utils v0.0.0-20190128072930-fbb6ab446f01/go.mod h1:wjDF8z83zTeg5eMLml5EBSlAhbF7G8DobyI1YsMuyzw=
140140
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
141141
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
142-
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56 h1:+XIU6AjxfFIu3yOHC+M1Hwz7dUAs5zc/ugJ9cCfvUNA=
143-
github.com/grafana/grafana-api-golang-client v0.0.0-20200812141400-4d890c757d56/go.mod h1:jFjwT3lvwl4JKqCw3guRJvlQ1/fmhER1h3Zgix3z7jw=
142+
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea h1:DB+ppVl+w8B5LtFmpjyuPf9zT6t5j95RzwuP/ypYZeU=
143+
github.com/grafana/grafana-api-golang-client v0.0.0-20201012135725-c87fc20af1ea/go.mod h1:jFjwT3lvwl4JKqCw3guRJvlQ1/fmhER1h3Zgix3z7jw=
144144
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
145145
github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs=
146146
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk=
@@ -298,12 +298,6 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
298298
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
299299
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d h1:VhgPp6v9qf9Agr/56bj7Y/xa04UccTW04VP0Qed4vnQ=
300300
github.com/nu7hatch/gouuid v0.0.0-20131221200532-179d4d0c4d8d/go.mod h1:YUTz3bUH2ZwIWBy3CJBeOBEugqcmXREj14T+iG/4k4U=
301-
github.com/nytm/go-grafana-api v0.2.0 h1:Aa5h2zMKDGjY1gS0bfesAfwSeRdTZG1GF8it+Nkd6l0=
302-
github.com/nytm/go-grafana-api v0.2.0/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
303-
github.com/nytm/go-grafana-api v0.3.1 h1:UMQYx88ZIpAxW2GunD2JgNve4QJt/T31PQXIyEtxzEs=
304-
github.com/nytm/go-grafana-api v0.3.1/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
305-
github.com/nytm/go-grafana-api v0.5.0 h1:8pIbNPNDguBa4aUNxcYl0GN247W6PXMvsOwiRBmk1sE=
306-
github.com/nytm/go-grafana-api v0.5.0/go.mod h1:YOJL2MOLAmCeqz0cbHU9tZIDj0OxpOiIFKSJXRbAorY=
307301
github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=
308302
github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA=
309303
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=

grafana/provider.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ func Provider() terraform.ResourceProvider {
3232
"grafana_data_source": ResourceDataSource(),
3333
"grafana_folder": ResourceFolder(),
3434
"grafana_organization": ResourceOrganization(),
35+
"grafana_team": ResourceTeam(),
3536
"grafana_user": ResourceUser(),
3637
},
3738

grafana/resource_team.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package grafana
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
"log"
7+
"strconv"
8+
"strings"
9+
10+
gapi "github.com/grafana/grafana-api-golang-client"
11+
"github.com/hashicorp/terraform/helper/schema"
12+
)
13+
14+
type TeamMember struct {
15+
ID int64
16+
Email string
17+
}
18+
19+
type MemberChange struct {
20+
Type ChangeMemberType
21+
Member TeamMember
22+
}
23+
24+
type ChangeMemberType int8
25+
26+
const (
27+
AddMember ChangeMemberType = iota
28+
RemoveMember
29+
)
30+
31+
func ResourceTeam() *schema.Resource {
32+
return &schema.Resource{
33+
Create: CreateTeam,
34+
Read: ReadTeam,
35+
Update: UpdateTeam,
36+
Delete: DeleteTeam,
37+
Exists: ExistsTeam,
38+
Importer: &schema.ResourceImporter{
39+
State: ImportTeam,
40+
},
41+
42+
Schema: map[string]*schema.Schema{
43+
"team_id": {
44+
Type: schema.TypeInt,
45+
Computed: true,
46+
},
47+
"name": {
48+
Type: schema.TypeString,
49+
Required: true,
50+
},
51+
"email": {
52+
Type: schema.TypeString,
53+
Optional: true,
54+
},
55+
"members": {
56+
Type: schema.TypeList,
57+
Optional: true,
58+
Elem: &schema.Schema{
59+
Type: schema.TypeString,
60+
},
61+
},
62+
},
63+
}
64+
}
65+
66+
func CreateTeam(d *schema.ResourceData, meta interface{}) error {
67+
client := meta.(*gapi.Client)
68+
name := d.Get("name").(string)
69+
email := d.Get("email").(string)
70+
teamID, err := client.AddTeam(name, email)
71+
if err != nil {
72+
return err
73+
}
74+
75+
d.SetId(strconv.FormatInt(teamID, 10))
76+
return UpdateMembers(d, meta)
77+
}
78+
79+
func ReadTeam(d *schema.ResourceData, meta interface{}) error {
80+
client := meta.(*gapi.Client)
81+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
82+
resp, err := client.Team(teamID)
83+
if err != nil && strings.HasPrefix(err.Error(), "status: 404") {
84+
log.Printf("[WARN] removing team %s from state because it no longer exists in grafana", d.Id())
85+
d.SetId("")
86+
return nil
87+
}
88+
if err != nil {
89+
return err
90+
}
91+
d.Set("name", resp.Name)
92+
d.Set("email", resp.Email)
93+
if err := ReadMembers(d, meta); err != nil {
94+
return err
95+
}
96+
return nil
97+
}
98+
99+
func UpdateTeam(d *schema.ResourceData, meta interface{}) error {
100+
client := meta.(*gapi.Client)
101+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
102+
if d.HasChange("name") || d.HasChange("email") {
103+
name := d.Get("name").(string)
104+
email := d.Get("email").(string)
105+
err := client.UpdateTeam(teamID, name, email)
106+
if err != nil {
107+
return err
108+
}
109+
}
110+
return UpdateMembers(d, meta)
111+
}
112+
113+
func DeleteTeam(d *schema.ResourceData, meta interface{}) error {
114+
client := meta.(*gapi.Client)
115+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
116+
return client.DeleteTeam(teamID)
117+
}
118+
119+
func ExistsTeam(d *schema.ResourceData, meta interface{}) (bool, error) {
120+
client := meta.(*gapi.Client)
121+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
122+
_, err := client.Team(teamID)
123+
if err != nil && strings.HasPrefix(err.Error(), "status: 404") {
124+
return false, nil
125+
}
126+
if err != nil {
127+
return false, err
128+
}
129+
return true, err
130+
}
131+
132+
func ImportTeam(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
133+
exists, err := ExistsTeam(d, meta)
134+
if err != nil || !exists {
135+
return nil, errors.New(fmt.Sprintf("Error: Unable to import Grafana Team: %s.", err))
136+
}
137+
err = ReadTeam(d, meta)
138+
if err != nil {
139+
return nil, err
140+
}
141+
return []*schema.ResourceData{d}, nil
142+
}
143+
144+
func ReadMembers(d *schema.ResourceData, meta interface{}) error {
145+
client := meta.(*gapi.Client)
146+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
147+
teamMembers, err := client.TeamMembers(teamID)
148+
if err != nil {
149+
return err
150+
}
151+
memberSlice := []string{}
152+
for _, teamMember := range teamMembers {
153+
memberSlice = append(memberSlice, teamMember.Email)
154+
}
155+
d.Set("members", memberSlice)
156+
157+
return nil
158+
}
159+
160+
func UpdateMembers(d *schema.ResourceData, meta interface{}) error {
161+
stateMembers, configMembers, err := collectMembers(d)
162+
if err != nil {
163+
return err
164+
}
165+
//compile the list of differences between current state and config
166+
changes := memberChanges(stateMembers, configMembers)
167+
//retrieves the corresponding user IDs based on the email provided
168+
changes, err = addMemberIdsToChanges(meta, changes)
169+
if err != nil {
170+
return err
171+
}
172+
teamID, _ := strconv.ParseInt(d.Id(), 10, 64)
173+
//now we can make the corresponding updates so current state matches config
174+
return applyMemberChanges(meta, teamID, changes)
175+
}
176+
177+
func collectMembers(d *schema.ResourceData) (map[string]TeamMember, map[string]TeamMember, error) {
178+
stateMembers, configMembers := make(map[string]TeamMember), make(map[string]TeamMember)
179+
180+
// Get the lists of team members read in from Grafana state (old) and configured (new)
181+
state, config := d.GetChange("members")
182+
for _, u := range state.([]interface{}) {
183+
login := u.(string)
184+
// Sanity check that a member isn't specified twice within a team
185+
if _, ok := stateMembers[login]; ok {
186+
return nil, nil, errors.New(fmt.Sprintf("Error: Team Member '%s' cannot be specified multiple times.", login))
187+
}
188+
stateMembers[login] = TeamMember{0, login}
189+
}
190+
for _, u := range config.([]interface{}) {
191+
login := u.(string)
192+
// Sanity check that a member isn't specified twice within a team
193+
if _, ok := configMembers[login]; ok {
194+
return nil, nil, errors.New(fmt.Sprintf("Error: Team Member '%s' cannot be specified multiple times.", login))
195+
}
196+
configMembers[login] = TeamMember{0, login}
197+
}
198+
199+
return stateMembers, configMembers, nil
200+
}
201+
202+
func memberChanges(stateMembers, configMembers map[string]TeamMember) []MemberChange {
203+
var changes []MemberChange
204+
for _, user := range configMembers {
205+
_, ok := stateMembers[user.Email]
206+
if !ok {
207+
// Member doesn't exist in Grafana's state for the team, should be added.
208+
changes = append(changes, MemberChange{AddMember, user})
209+
continue
210+
}
211+
}
212+
for _, user := range stateMembers {
213+
if _, ok := configMembers[user.Email]; !ok {
214+
// Member exists in Grafana's state for the team, but isn't
215+
// present in the team configuration, should be removed.
216+
changes = append(changes, MemberChange{RemoveMember, user})
217+
}
218+
}
219+
return changes
220+
}
221+
222+
func addMemberIdsToChanges(meta interface{}, changes []MemberChange) ([]MemberChange, error) {
223+
client := meta.(*gapi.Client)
224+
gUserMap := make(map[string]int64)
225+
gUsers, err := client.Users()
226+
if err != nil {
227+
return nil, err
228+
}
229+
for _, u := range gUsers {
230+
gUserMap[u.Email] = u.Id
231+
}
232+
var output []MemberChange
233+
234+
for _, change := range changes {
235+
id, ok := gUserMap[change.Member.Email]
236+
if !ok {
237+
return nil, errors.New(fmt.Sprintf("Error adding user %s. User does not exist in Grafana.", change.Member.Email))
238+
}
239+
240+
change.Member.ID = id
241+
output = append(output, change)
242+
}
243+
return output, nil
244+
}
245+
246+
func applyMemberChanges(meta interface{}, teamId int64, changes []MemberChange) error {
247+
var err error
248+
client := meta.(*gapi.Client)
249+
for _, change := range changes {
250+
u := change.Member
251+
switch change.Type {
252+
case AddMember:
253+
err = client.AddTeamMember(teamId, u.ID)
254+
case RemoveMember:
255+
err = client.RemoveMemberFromTeam(teamId, u.ID)
256+
}
257+
if err != nil {
258+
return err
259+
}
260+
}
261+
return nil
262+
}

0 commit comments

Comments
 (0)