Skip to content

feat: Add codefresh_account_user_association resource #123

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 8 commits into from
Oct 3, 2023
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
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