Skip to content

Commit

Permalink
feat: Add codefresh_account_user_association resource (#123)
Browse files Browse the repository at this point in the history
## 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)._
  • Loading branch information
korenyoni authored Oct 3, 2023
1 parent e9e9559 commit 0fd87b3
Show file tree
Hide file tree
Showing 15 changed files with 635 additions and 31 deletions.
31 changes: 25 additions & 6 deletions client/current_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package client
import (
"encoding/json"
"fmt"

"github.com/stretchr/objx"
)

Expand All @@ -11,13 +12,15 @@ type CurrentAccountUser struct {
ID string `json:"id,omitempty"`
UserName string `json:"name,omitempty"`
Email string `json:"email,omitempty"`
Status string `json:"status,omitempty"`
}

// CurrentAccount spec
type CurrentAccount struct {
ID string
Name string
Users []CurrentAccountUser
ID string
Name string
Users []CurrentAccountUser
Admins []CurrentAccountUser
}

// GetCurrentAccount -
Expand All @@ -42,15 +45,29 @@ func (client *Client) GetCurrentAccount() (*CurrentAccount, error) {
return nil, fmt.Errorf("GetCurrentAccount - cannot get activeAccountName")
}
currentAccount := &CurrentAccount{
Name: activeAccountName,
Users: make([]CurrentAccountUser, 0),
Name: activeAccountName,
Users: make([]CurrentAccountUser, 0),
Admins: make([]CurrentAccountUser, 0),
}

allAccountsI := currentAccountX.Get("account").InterSlice()
for _, accI := range allAccountsI {
accX := objx.New(accI)
if accX.Get("name").String() == activeAccountName {
currentAccount.ID = accX.Get("id").String()
admins := accX.Get("admins").InterSlice()
for _, adminI := range admins {
admin, err := client.GetUserByID(adminI.(string))
if err != nil {
return nil, err
}
currentAccount.Admins = append(currentAccount.Admins, CurrentAccountUser{
ID: admin.ID,
UserName: admin.UserName,
Email: admin.Email,
Status: admin.Status,
})
}
break
}
}
Expand All @@ -69,17 +86,19 @@ func (client *Client) GetCurrentAccount() (*CurrentAccount, error) {

accountUsersI := make([]interface{}, 0)
if e := json.Unmarshal(accountUsersResp, &accountUsersI); e != nil {
return nil, fmt.Errorf("Cannot unmarshal accountUsers responce for accountId=%s: %v", currentAccount.ID, e)
return nil, fmt.Errorf("cannot unmarshal accountUsers responce for accountId=%s: %v", currentAccount.ID, e)
}
for _, userI := range accountUsersI {
userX := objx.New(userI)
userName := userX.Get("userName").String()
email := userX.Get("email").String()
status := userX.Get("status").String()
userID := userX.Get("_id").String()
currentAccount.Users = append(currentAccount.Users, CurrentAccountUser{
ID: userID,
UserName: userName,
Email: email,
Status: status,
})
}

Expand Down
38 changes: 34 additions & 4 deletions client/user.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,22 @@ type UserAccounts struct {
Account []Account `json:"account"`
}

func (client *Client) AddNewUserToAccount(accountId, userName, userEmail string) (*User, error) {
// The API accepts two different schemas when updating the user details
func generateUserDetailsBody(userName, userEmail string) string {
userDetails := fmt.Sprintf(`{"userDetails": "%s"}`, userEmail)
if userName != "" {
userDetails = fmt.Sprintf(`{"userName": "%s", "email": "%s"}`, userName, userEmail)
}
return userDetails
}

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

fullPath := fmt.Sprintf("/accounts/%s/adduser", accountId)

opts := RequestOptions{
Path: fullPath,
Method: "POST",
Body: []byte(userDetails),
Body: []byte(generateUserDetailsBody(userName, userEmail)),
}

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

return nil
}

func (client *Client) UpdateUserDetails(accountId, userId, userName, userEmail string) (*User, error) {

fullPath := fmt.Sprintf("/accounts/%s/%s/updateuser", accountId, userId)
opts := RequestOptions{
Path: fullPath,
Method: "POST",
Body: []byte(generateUserDetailsBody(userName, userEmail)),
}

resp, err := client.RequestAPI(&opts)
if err != nil {
return nil, err
}

var respUser User

err = DecodeResponseInto(resp, &respUser)
if err != nil {
return nil, err
}

return &respUser, nil
}
29 changes: 15 additions & 14 deletions codefresh/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,20 +53,21 @@ func Provider() *schema.Provider {
"codefresh_pipelines": dataSourcePipelines(),
},
ResourcesMap: map[string]*schema.Resource{
"codefresh_account": resourceAccount(),
"codefresh_account_admins": resourceAccountAdmins(),
"codefresh_api_key": resourceApiKey(),
"codefresh_context": resourceContext(),
"codefresh_registry": resourceRegistry(),
"codefresh_idp_accounts": resourceIDPAccounts(),
"codefresh_permission": resourcePermission(),
"codefresh_pipeline": resourcePipeline(),
"codefresh_pipeline_cron_trigger": resourcePipelineCronTrigger(),
"codefresh_project": resourceProject(),
"codefresh_step_types": resourceStepTypes(),
"codefresh_user": resourceUser(),
"codefresh_team": resourceTeam(),
"codefresh_abac_rules": resourceGitopsAbacRule(),
"codefresh_account": resourceAccount(),
"codefresh_account_user_association": resourceAccountUserAssociation(),
"codefresh_account_admins": resourceAccountAdmins(),
"codefresh_api_key": resourceApiKey(),
"codefresh_context": resourceContext(),
"codefresh_registry": resourceRegistry(),
"codefresh_idp_accounts": resourceIDPAccounts(),
"codefresh_permission": resourcePermission(),
"codefresh_pipeline": resourcePipeline(),
"codefresh_pipeline_cron_trigger": resourcePipelineCronTrigger(),
"codefresh_project": resourceProject(),
"codefresh_step_types": resourceStepTypes(),
"codefresh_user": resourceUser(),
"codefresh_team": resourceTeam(),
"codefresh_abac_rules": resourceGitopsAbacRule(),
},
ConfigureFunc: configureProvider,
}
Expand Down
4 changes: 2 additions & 2 deletions codefresh/provider_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ func TestProvider(t *testing.T) {
}

func testAccPreCheck(t *testing.T) {
if v := os.Getenv("CODEFRESH_API_KEY"); v == "" {
t.Fatal("CODEFRESH_API_KEY must be set for acceptance tests")
if v := os.Getenv(ENV_CODEFRESH_API_KEY); v == "" {
t.Fatalf("%s must be set for acceptance tests", ENV_CODEFRESH_API_KEY)
}
}
169 changes: 169 additions & 0 deletions codefresh/resource_account_user_association.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package codefresh

import (
"context"
"fmt"

cfClient "github.com/codefresh-io/terraform-provider-codefresh/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/customdiff"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceAccountUserAssociation() *schema.Resource {
return &schema.Resource{
Description: `
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.
`,
Create: resourceAccountUserAssociationCreate,
Read: resourceAccountUserAssociationRead,
Update: resourceAccountUserAssociationUpdate,
Delete: resourceAccountUserAssociationDelete,
Importer: &schema.ResourceImporter{
State: schema.ImportStatePassthrough,
},
Schema: map[string]*schema.Schema{
"email": {
Description: `
The email of the user to associate with the specified account.
If the user is not present in the system, an invitation will be sent to this email.
This field can only be changed when 'status' is 'pending'.
`,
Type: schema.TypeString,
Required: true,
},
"admin": {
Description: "Whether to make this user an account admin.",
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"username": {
Computed: true,
Type: schema.TypeString,
Description: "The username of the associated user.",
},
"status": {
Computed: true,
Type: schema.TypeString,
Description: "The status of the association.",
},
},
CustomizeDiff: customdiff.All(
// The email field is immutable, except for users with status "pending".
customdiff.ForceNewIf("email", func(_ context.Context, d *schema.ResourceDiff, _ any) bool {
return d.Get("status").(string) != "pending" && d.HasChange("email")
}),
),
}
}

func resourceAccountUserAssociationCreate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cfClient.Client)
currentAccount, err := client.GetCurrentAccount()
if err != nil {
return err
}

user, err := client.AddNewUserToAccount(currentAccount.ID, "", d.Get("email").(string))
if err != nil {
return err
}

d.SetId(user.ID)

if d.Get("admin").(bool) {
err = client.SetUserAsAccountAdmin(currentAccount.ID, d.Id())
if err != nil {
return err
}
}

d.Set("status", user.Status)

return nil
}

func resourceAccountUserAssociationRead(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cfClient.Client)
currentAccount, err := client.GetCurrentAccount()
if err != nil {
return err
}

userID := d.Id()
if userID == "" {
d.SetId("")
return nil
}

for _, user := range currentAccount.Users {
if user.ID == userID {
d.Set("email", user.Email)
d.Set("username", user.UserName)
d.Set("status", user.Status)
d.Set("admin", false) // avoid missing attributes after import
for _, admin := range currentAccount.Admins {
if admin.ID == userID {
d.Set("admin", true)
}
}
}
}

if d.Id() == "" {
return fmt.Errorf("a user with ID %s was not found", userID)
}

return nil
}

func resourceAccountUserAssociationUpdate(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cfClient.Client)

currentAccount, err := client.GetCurrentAccount()
if err != nil {
return err
}

if d.HasChange("email") {
user, err := client.UpdateUserDetails(currentAccount.ID, d.Id(), d.Get("username").(string), d.Get("email").(string))
if err != nil {
return err
}
if user.Email != d.Get("email").(string) {
return fmt.Errorf("failed to update user email, despite successful API response")
}
}

if d.HasChange("admin") {
if d.Get("admin").(bool) {
err = client.SetUserAsAccountAdmin(currentAccount.ID, d.Id())
if err != nil {
return err
}
} else {
err = client.DeleteUserAsAccountAdmin(currentAccount.ID, d.Id())
if err != nil {
return err
}
}
}

return nil
}

func resourceAccountUserAssociationDelete(d *schema.ResourceData, meta interface{}) error {
client := meta.(*cfClient.Client)

currentAccount, err := client.GetCurrentAccount()
if err != nil {
return err
}

err = client.DeleteUserFromAccount(currentAccount.ID, d.Id())
if err != nil {
return err
}

return nil
}
Loading

0 comments on commit 0fd87b3

Please sign in to comment.