Skip to content

Commit 0fd87b3

Browse files
authored
feat: Add codefresh_account_user_association resource (#123)
## What * Add `codefresh_account_user_association` resource ## Why * Allow account user associations to be made (aka account collaborators) ## Notes <!-- Add any notes here --> ## Checklist * [x] _I have read [CONTRIBUTING.md](https://github.com/codefresh-io/terraform-provider-codefresh/blob/master/CONTRIBUTING.md)._ * [x] _I have [allowed changes to my fork to be made](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/allowing-changes-to-a-pull-request-branch-created-from-a-fork)._ * [x] _I have added tests, assuming new tests are warranted_. * [x] _I understand that the `/test` comment will be ignored by the CI trigger [unless it is made by a repo admin or collaborator](https://codefresh.io/docs/docs/pipelines/triggers/git-triggers/#support-for-building-pull-requests-from-forks)._
1 parent e9e9559 commit 0fd87b3

File tree

15 files changed

+635
-31
lines changed

15 files changed

+635
-31
lines changed

client/current_account.go

+25-6
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package client
33
import (
44
"encoding/json"
55
"fmt"
6+
67
"github.com/stretchr/objx"
78
)
89

@@ -11,13 +12,15 @@ type CurrentAccountUser struct {
1112
ID string `json:"id,omitempty"`
1213
UserName string `json:"name,omitempty"`
1314
Email string `json:"email,omitempty"`
15+
Status string `json:"status,omitempty"`
1416
}
1517

1618
// CurrentAccount spec
1719
type CurrentAccount struct {
18-
ID string
19-
Name string
20-
Users []CurrentAccountUser
20+
ID string
21+
Name string
22+
Users []CurrentAccountUser
23+
Admins []CurrentAccountUser
2124
}
2225

2326
// GetCurrentAccount -
@@ -42,15 +45,29 @@ func (client *Client) GetCurrentAccount() (*CurrentAccount, error) {
4245
return nil, fmt.Errorf("GetCurrentAccount - cannot get activeAccountName")
4346
}
4447
currentAccount := &CurrentAccount{
45-
Name: activeAccountName,
46-
Users: make([]CurrentAccountUser, 0),
48+
Name: activeAccountName,
49+
Users: make([]CurrentAccountUser, 0),
50+
Admins: make([]CurrentAccountUser, 0),
4751
}
4852

4953
allAccountsI := currentAccountX.Get("account").InterSlice()
5054
for _, accI := range allAccountsI {
5155
accX := objx.New(accI)
5256
if accX.Get("name").String() == activeAccountName {
5357
currentAccount.ID = accX.Get("id").String()
58+
admins := accX.Get("admins").InterSlice()
59+
for _, adminI := range admins {
60+
admin, err := client.GetUserByID(adminI.(string))
61+
if err != nil {
62+
return nil, err
63+
}
64+
currentAccount.Admins = append(currentAccount.Admins, CurrentAccountUser{
65+
ID: admin.ID,
66+
UserName: admin.UserName,
67+
Email: admin.Email,
68+
Status: admin.Status,
69+
})
70+
}
5471
break
5572
}
5673
}
@@ -69,17 +86,19 @@ func (client *Client) GetCurrentAccount() (*CurrentAccount, error) {
6986

7087
accountUsersI := make([]interface{}, 0)
7188
if e := json.Unmarshal(accountUsersResp, &accountUsersI); e != nil {
72-
return nil, fmt.Errorf("Cannot unmarshal accountUsers responce for accountId=%s: %v", currentAccount.ID, e)
89+
return nil, fmt.Errorf("cannot unmarshal accountUsers responce for accountId=%s: %v", currentAccount.ID, e)
7390
}
7491
for _, userI := range accountUsersI {
7592
userX := objx.New(userI)
7693
userName := userX.Get("userName").String()
7794
email := userX.Get("email").String()
95+
status := userX.Get("status").String()
7896
userID := userX.Get("_id").String()
7997
currentAccount.Users = append(currentAccount.Users, CurrentAccountUser{
8098
ID: userID,
8199
UserName: userName,
82100
Email: email,
101+
Status: status,
83102
})
84103
}
85104

client/user.go

+34-4
Original file line numberDiff line numberDiff line change
@@ -63,16 +63,22 @@ type UserAccounts struct {
6363
Account []Account `json:"account"`
6464
}
6565

66-
func (client *Client) AddNewUserToAccount(accountId, userName, userEmail string) (*User, error) {
66+
// The API accepts two different schemas when updating the user details
67+
func generateUserDetailsBody(userName, userEmail string) string {
68+
userDetails := fmt.Sprintf(`{"userDetails": "%s"}`, userEmail)
69+
if userName != "" {
70+
userDetails = fmt.Sprintf(`{"userName": "%s", "email": "%s"}`, userName, userEmail)
71+
}
72+
return userDetails
73+
}
6774

68-
userDetails := fmt.Sprintf(`{"userName": "%s", "email": "%s"}`, userName, userEmail)
75+
func (client *Client) AddNewUserToAccount(accountId, userName, userEmail string) (*User, error) {
6976

7077
fullPath := fmt.Sprintf("/accounts/%s/adduser", accountId)
71-
7278
opts := RequestOptions{
7379
Path: fullPath,
7480
Method: "POST",
75-
Body: []byte(userDetails),
81+
Body: []byte(generateUserDetailsBody(userName, userEmail)),
7682
}
7783

7884
resp, err := client.RequestAPI(&opts)
@@ -338,3 +344,27 @@ func (client *Client) UpdateUserAccounts(userId string, accounts []Account) erro
338344

339345
return nil
340346
}
347+
348+
func (client *Client) UpdateUserDetails(accountId, userId, userName, userEmail string) (*User, error) {
349+
350+
fullPath := fmt.Sprintf("/accounts/%s/%s/updateuser", accountId, userId)
351+
opts := RequestOptions{
352+
Path: fullPath,
353+
Method: "POST",
354+
Body: []byte(generateUserDetailsBody(userName, userEmail)),
355+
}
356+
357+
resp, err := client.RequestAPI(&opts)
358+
if err != nil {
359+
return nil, err
360+
}
361+
362+
var respUser User
363+
364+
err = DecodeResponseInto(resp, &respUser)
365+
if err != nil {
366+
return nil, err
367+
}
368+
369+
return &respUser, nil
370+
}

codefresh/provider.go

+15-14
Original file line numberDiff line numberDiff line change
@@ -53,20 +53,21 @@ func Provider() *schema.Provider {
5353
"codefresh_pipelines": dataSourcePipelines(),
5454
},
5555
ResourcesMap: map[string]*schema.Resource{
56-
"codefresh_account": resourceAccount(),
57-
"codefresh_account_admins": resourceAccountAdmins(),
58-
"codefresh_api_key": resourceApiKey(),
59-
"codefresh_context": resourceContext(),
60-
"codefresh_registry": resourceRegistry(),
61-
"codefresh_idp_accounts": resourceIDPAccounts(),
62-
"codefresh_permission": resourcePermission(),
63-
"codefresh_pipeline": resourcePipeline(),
64-
"codefresh_pipeline_cron_trigger": resourcePipelineCronTrigger(),
65-
"codefresh_project": resourceProject(),
66-
"codefresh_step_types": resourceStepTypes(),
67-
"codefresh_user": resourceUser(),
68-
"codefresh_team": resourceTeam(),
69-
"codefresh_abac_rules": resourceGitopsAbacRule(),
56+
"codefresh_account": resourceAccount(),
57+
"codefresh_account_user_association": resourceAccountUserAssociation(),
58+
"codefresh_account_admins": resourceAccountAdmins(),
59+
"codefresh_api_key": resourceApiKey(),
60+
"codefresh_context": resourceContext(),
61+
"codefresh_registry": resourceRegistry(),
62+
"codefresh_idp_accounts": resourceIDPAccounts(),
63+
"codefresh_permission": resourcePermission(),
64+
"codefresh_pipeline": resourcePipeline(),
65+
"codefresh_pipeline_cron_trigger": resourcePipelineCronTrigger(),
66+
"codefresh_project": resourceProject(),
67+
"codefresh_step_types": resourceStepTypes(),
68+
"codefresh_user": resourceUser(),
69+
"codefresh_team": resourceTeam(),
70+
"codefresh_abac_rules": resourceGitopsAbacRule(),
7071
},
7172
ConfigureFunc: configureProvider,
7273
}

codefresh/provider_test.go

+2-2
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ func TestProvider(t *testing.T) {
2424
}
2525

2626
func testAccPreCheck(t *testing.T) {
27-
if v := os.Getenv("CODEFRESH_API_KEY"); v == "" {
28-
t.Fatal("CODEFRESH_API_KEY must be set for acceptance tests")
27+
if v := os.Getenv(ENV_CODEFRESH_API_KEY); v == "" {
28+
t.Fatalf("%s must be set for acceptance tests", ENV_CODEFRESH_API_KEY)
2929
}
3030
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
package codefresh
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
8+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
9+
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
10+
)
11+
12+
func resourceAccountUserAssociation() *schema.Resource {
13+
return &schema.Resource{
14+
Description: `
15+
Associates a user with the account which the provider is authenticated against. If the user is not present in the system, an invitation will be sent to the specified email address.
16+
`,
17+
Create: resourceAccountUserAssociationCreate,
18+
Read: resourceAccountUserAssociationRead,
19+
Update: resourceAccountUserAssociationUpdate,
20+
Delete: resourceAccountUserAssociationDelete,
21+
Importer: &schema.ResourceImporter{
22+
State: schema.ImportStatePassthrough,
23+
},
24+
Schema: map[string]*schema.Schema{
25+
"email": {
26+
Description: `
27+
The email of the user to associate with the specified account.
28+
If the user is not present in the system, an invitation will be sent to this email.
29+
This field can only be changed when 'status' is 'pending'.
30+
`,
31+
Type: schema.TypeString,
32+
Required: true,
33+
},
34+
"admin": {
35+
Description: "Whether to make this user an account admin.",
36+
Type: schema.TypeBool,
37+
Optional: true,
38+
Default: false,
39+
},
40+
"username": {
41+
Computed: true,
42+
Type: schema.TypeString,
43+
Description: "The username of the associated user.",
44+
},
45+
"status": {
46+
Computed: true,
47+
Type: schema.TypeString,
48+
Description: "The status of the association.",
49+
},
50+
},
51+
CustomizeDiff: customdiff.All(
52+
// The email field is immutable, except for users with status "pending".
53+
customdiff.ForceNewIf("email", func(_ context.Context, d *schema.ResourceDiff, _ any) bool {
54+
return d.Get("status").(string) != "pending" && d.HasChange("email")
55+
}),
56+
),
57+
}
58+
}
59+
60+
func resourceAccountUserAssociationCreate(d *schema.ResourceData, meta interface{}) error {
61+
client := meta.(*cfClient.Client)
62+
currentAccount, err := client.GetCurrentAccount()
63+
if err != nil {
64+
return err
65+
}
66+
67+
user, err := client.AddNewUserToAccount(currentAccount.ID, "", d.Get("email").(string))
68+
if err != nil {
69+
return err
70+
}
71+
72+
d.SetId(user.ID)
73+
74+
if d.Get("admin").(bool) {
75+
err = client.SetUserAsAccountAdmin(currentAccount.ID, d.Id())
76+
if err != nil {
77+
return err
78+
}
79+
}
80+
81+
d.Set("status", user.Status)
82+
83+
return nil
84+
}
85+
86+
func resourceAccountUserAssociationRead(d *schema.ResourceData, meta interface{}) error {
87+
client := meta.(*cfClient.Client)
88+
currentAccount, err := client.GetCurrentAccount()
89+
if err != nil {
90+
return err
91+
}
92+
93+
userID := d.Id()
94+
if userID == "" {
95+
d.SetId("")
96+
return nil
97+
}
98+
99+
for _, user := range currentAccount.Users {
100+
if user.ID == userID {
101+
d.Set("email", user.Email)
102+
d.Set("username", user.UserName)
103+
d.Set("status", user.Status)
104+
d.Set("admin", false) // avoid missing attributes after import
105+
for _, admin := range currentAccount.Admins {
106+
if admin.ID == userID {
107+
d.Set("admin", true)
108+
}
109+
}
110+
}
111+
}
112+
113+
if d.Id() == "" {
114+
return fmt.Errorf("a user with ID %s was not found", userID)
115+
}
116+
117+
return nil
118+
}
119+
120+
func resourceAccountUserAssociationUpdate(d *schema.ResourceData, meta interface{}) error {
121+
client := meta.(*cfClient.Client)
122+
123+
currentAccount, err := client.GetCurrentAccount()
124+
if err != nil {
125+
return err
126+
}
127+
128+
if d.HasChange("email") {
129+
user, err := client.UpdateUserDetails(currentAccount.ID, d.Id(), d.Get("username").(string), d.Get("email").(string))
130+
if err != nil {
131+
return err
132+
}
133+
if user.Email != d.Get("email").(string) {
134+
return fmt.Errorf("failed to update user email, despite successful API response")
135+
}
136+
}
137+
138+
if d.HasChange("admin") {
139+
if d.Get("admin").(bool) {
140+
err = client.SetUserAsAccountAdmin(currentAccount.ID, d.Id())
141+
if err != nil {
142+
return err
143+
}
144+
} else {
145+
err = client.DeleteUserAsAccountAdmin(currentAccount.ID, d.Id())
146+
if err != nil {
147+
return err
148+
}
149+
}
150+
}
151+
152+
return nil
153+
}
154+
155+
func resourceAccountUserAssociationDelete(d *schema.ResourceData, meta interface{}) error {
156+
client := meta.(*cfClient.Client)
157+
158+
currentAccount, err := client.GetCurrentAccount()
159+
if err != nil {
160+
return err
161+
}
162+
163+
err = client.DeleteUserFromAccount(currentAccount.ID, d.Id())
164+
if err != nil {
165+
return err
166+
}
167+
168+
return nil
169+
}

0 commit comments

Comments
 (0)