forked from heroku/terraform-provider-heroku
/
resource_heroku_team_collaborator.go
285 lines (221 loc) · 8.17 KB
/
resource_heroku_team_collaborator.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
package heroku
import (
"context"
"fmt"
"log"
"time"
"github.com/hashicorp/go-multierror"
"github.com/hashicorp/terraform/helper/resource"
"github.com/hashicorp/terraform/helper/schema"
"github.com/heroku/heroku-go/v3"
)
/**
Heroku's collaborator & team collaborator overlap in several of their CRUD endpoints.
However, these two resources have minute differences similar to the square/rectangle analogy.
Given that is likely a heroku provider user will likely be using teams, I'm implementing this
resource first. So if you have a team/org, please use this resource.
*/
/**
herokuTeamCollaborator is a value type used to hold the details of a
team collaborator. We use this for common storage of values needed for the
heroku.TeamCollaborator types
*/
type herokuTeamCollaborator struct {
Email string
}
// type teamCollaborator is used to store all the details of a heroku team collaborator
type teamCollaborator struct {
Id string // Id of the resource
AppName string // the app the collaborator belongs to
TeamCollaborator *herokuTeamCollaborator
Client *heroku.Service
Permissions []string // can be a combo or all of ["view", "deploy", "operate", "manage"]
}
func resourceHerokuTeamCollaborator() *schema.Resource {
return &schema.Resource{
Create: resourceHerokuTeamCollaboratorCreate,
Read: resourceHerokuTeamCollaboratorRead,
Update: resourceHerokuTeamCollaboratorUpdate,
Delete: resourceHerokuTeamCollaboratorDelete,
Importer: &schema.ResourceImporter{
State: resourceHerokuTeamCollaboratorImport,
},
Schema: map[string]*schema.Schema{
"app": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"email": {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
"permissions": {
Type: schema.TypeSet, // We are using TypeSet type here as the order for permissions is not important.
Required: true,
MinItems: 1,
MaxItems: 4,
Elem: &schema.Schema{
Type: schema.TypeString,
},
},
},
}
}
func resourceHerokuTeamCollaboratorCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api
opts := heroku.TeamAppCollaboratorCreateOpts{}
appName := getAppName(d)
opts.User = getEmail(d)
/**
Setting the silent parameter to true by default. It is really an optional parameter that doesn't
belong in the resource's state, especially since it's not part of the collaborator GET endpoint.
After several iterations to keep it as part of schema but ignoring a state diff, nothing worked out well.
*/
vs := true
opts.Silent = &vs
if v, ok := d.GetOk("permissions"); ok {
permsSet := v.(*schema.Set)
perms := make([]*string, permsSet.Len())
for i, perm := range permsSet.List() {
p := perm.(string)
perms[i] = &p
}
log.Printf("[DEBUG] Permissions: %v", perms)
opts.Permissions = perms
}
log.Printf("[DEBUG] Creating Heroku Team Collaborator: [%s]", opts.User)
collaborator, err := client.TeamAppCollaboratorCreate(context.TODO(), appName, opts)
if err != nil {
return err
}
d.SetId(collaborator.ID)
log.Printf("[INFO] New Collaborator ID: %s", d.Id())
return resourceHerokuTeamCollaboratorRead(d, meta)
}
func resourceHerokuTeamCollaboratorRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api
teamCollaborator, err := resourceHerokuTeamCollaboratorRetrieve(d.Id(), d.Get("app").(string), client)
if err != nil {
return err
}
d.Set("app", teamCollaborator.AppName)
d.Set("email", teamCollaborator.TeamCollaborator.Email)
d.Set("permissions", teamCollaborator.Permissions)
return nil
}
func resourceHerokuTeamCollaboratorUpdate(d *schema.ResourceData, meta interface{}) error {
// Enable Partial state mode to track what was successfully committed
d.Partial(true)
client := meta.(*Config).Api
opts := heroku.TeamAppCollaboratorUpdateOpts{}
if d.HasChange("permissions") {
permsSet := d.Get("permissions").(*schema.Set)
perms := make([]string, permsSet.Len())
for i, perm := range permsSet.List() {
perms[i] = perm.(string)
}
log.Printf("[DEBUG] Permissions: %s", perms)
opts.Permissions = perms
}
appName := getAppName(d)
email := getEmail(d)
log.Printf("[DEBUG] Updating Heroku Team Collaborator: [%s]", email)
updatedTeamCollaborator, err := client.TeamAppCollaboratorUpdate(context.TODO(), appName, email, opts)
if err != nil {
return err
}
d.SetPartial("permissions")
d.SetId(updatedTeamCollaborator.ID)
d.Partial(false)
return resourceHerokuTeamCollaboratorRead(d, meta)
}
func resourceHerokuTeamCollaboratorDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*Config).Api
log.Printf("[INFO] Deleting Heroku Team Collaborator: [%s]", d.Id())
_, err := client.TeamAppCollaboratorDelete(context.TODO(), getAppName(d), getEmail(d))
if err != nil {
return fmt.Errorf("error deleting Team Collaborator: %s", err)
}
// So long as the DELETE succeeded, remove the resource from state
d.SetId("")
/**
There's a edge scenario where if you immediately delete a team collaborator and recreate it, the Heroku api
will complain the team collaborator still exists. So to remedy this, we will do a GET for the collaborator
until it 404s before proceeding further.
*/
log.Printf("[INFO] Begin checking if [%s] has been deleted", getEmail(d))
retryError := resource.Retry(10*time.Second, func() *resource.RetryError {
_, err := client.TeamAppCollaboratorInfo(context.TODO(), getAppName(d), d.Id())
// Debug log to check
log.Printf("[INFO] Is error nil when GET#show team collaborator? %t", err == nil)
// If err is nil, then that means the GET was successful and the collaborator still exists on the team app
if err == nil {
// fmt.ErrorF does not output to log when TF_LOG=DEBUG is set to true, hence the need to execute log.PrintF for
// debugging purpose and fmt.ErrorF so the retry func loops
log.Printf("[WARNING] Team collaborator [%s] exists after deletion. Checking again", getEmail(d))
return resource.RetryableError(err)
} else {
// if there is an error in the GET, the collaborator no longer exists.
return nil
}
})
if retryError != nil {
return fmt.Errorf("[ERROR] Team collaborator [%s] still exists on [%s] after checking several times", getEmail(d), getAppName(d))
}
return nil
}
func resourceHerokuTeamCollaboratorRetrieve(id string, appName string, client *heroku.Service) (*teamCollaborator, error) {
teamCollaborator := teamCollaborator{Id: id, AppName: appName, Client: client}
err := teamCollaborator.Update()
if err != nil {
return nil, fmt.Errorf("[ERROR] Error retrieving team collaborator: %s", err)
}
return &teamCollaborator, nil
}
func (tc *teamCollaborator) Update() error {
var errs []error
log.Printf("[INFO] tc.Id is %s", tc.Id)
teamCollaborator, err := tc.Client.TeamAppCollaboratorInfo(context.TODO(), tc.AppName, tc.Id)
if err != nil {
errs = append(errs, err)
} else {
tc.TeamCollaborator = &herokuTeamCollaborator{}
tc.TeamCollaborator.Email = teamCollaborator.User.Email
tc.AppName = teamCollaborator.App.Name
}
// The underlying go client does not return permission info on the collaborator when calling
// 'TeamAppCollaboratorInfo'. Instead that is returned via calling 'CollaboratorInfo'
collaborator, collaboratorErr := tc.Client.CollaboratorInfo(context.TODO(), tc.AppName, tc.Id)
if collaboratorErr != nil {
errs = append(errs, collaboratorErr)
} else {
// build the slice of perms
perms := make([]string, 0, len(collaborator.Permissions))
for _, perm := range collaborator.Permissions {
perms = append(perms, perm.Name)
}
tc.Permissions = perms
}
if len(errs) > 0 {
return &multierror.Error{Errors: errs}
}
return nil
}
func resourceHerokuTeamCollaboratorImport(d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*Config).Api
app, email, err := parseCompositeID(d.Id())
if err != nil {
return nil, err
}
collaborator, err := client.CollaboratorInfo(context.Background(), app, email)
if err != nil {
return nil, err
}
d.SetId(collaborator.ID)
d.Set("app", collaborator.App.Name)
d.Set("email", collaborator.User.Email)
d.Set("permissions", collaborator.Permissions)
return []*schema.ResourceData{d}, nil
}