package utilities import ( "context" "fmt" "strconv" "time" "github.com/hashicorp/terraform-plugin-framework/diag" "github.com/hashicorp/terraform-plugin-framework/types" "github.com/hashicorp/terraform-plugin-framework/types/basetypes" "github.com/hashicorp/terraform-plugin-log/tflog" decort "repository.basistech.ru/BASIS/decort-golang-sdk" "repository.basistech.ru/BASIS/decort-golang-sdk/pkg/cloudapi/account" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/service/cloudapi/account/models" "repository.basistech.ru/BASIS/terraform-provider-dynamix/internal/status" ) // AccountResourceCheckPresence checks if account with accountId exists func AccountResourceCheckPresence(ctx context.Context, accountId uint64, c *decort.DecortClient) (*account.RecordAccount, error) { tflog.Info(ctx, fmt.Sprintf("AccountResourceCheckPresence: Get info about resource with ID - %v", accountId)) accountRecord, err := c.CloudAPI().Account().Get(ctx, account.GetRequest{AccountID: accountId}) if err != nil { return nil, fmt.Errorf("AccountResourceCheckPresence: cannot get info about resource with error: %w", err) } tflog.Info(ctx, "AccountResourceCheckPresence: response from CloudAPI().Account().Get", map[string]any{"account_id": accountId, "response": accountRecord}) return accountRecord, err } // AccountReadStatus loads account resource by its id, gets it current status. Performs restore and enable if needed for // Deleted status. // In case of failure returns errors. func AccountReadStatus(ctx context.Context, state *models.ResourceAccountModel, c *decort.DecortClient) diag.Diagnostics { tflog.Info(ctx, "AccountReadStatus: Read status resource with ID", map[string]any{"account_id": state.Id.ValueString()}) diags := diag.Diagnostics{} accountId, err := strconv.ParseUint(state.Id.ValueString(), 10, 64) if err != nil { diags.AddError("AccountReadStatus: Cannot parse resource ID from state", err.Error()) return diags } recordAccount, err := AccountResourceCheckPresence(ctx, accountId, c) if err != nil { diags.AddError("AccountReadStatus: Unable to Read account before status check", err.Error()) return diags } // check resource status switch recordAccount.Status { case status.Disabled: tflog.Info(ctx, "The account is in status Disabled, troubles may occur with update. Please, enable account first.") case status.Deleted: restore := state.Restore.ValueBool() if state.Restore.IsNull() { restore = true } // default true if restore { // attempt to restore account tflog.Info(ctx, "AccountReadStatus: account with status.Deleted is being read, attempt to restore it", map[string]any{ "account_id": accountId, "status": recordAccount.Status}) diags.Append(RestoreAccount(ctx, accountId, c)...) if diags.HasError() { tflog.Error(ctx, "AccountReadStatus: cannot restore account") return diags } tflog.Info(ctx, "AccountReadStatus: account restored successfully", map[string]any{"account_id": accountId}) state.LastUpdated = types.StringValue(time.Now().Format(time.RFC850)) } else { tflog.Info(ctx, "AccountReadStatus: account is i status Deleted but restore is not specified") } case status.Destroyed: diags.AddError( "AccountReadStatus: Account is in status Destroyed", fmt.Sprintf("the resource with account_id %d cannot be read or updated because it has been destroyed", accountId), ) return diags case status.Destroying: diags.AddError( "AccountReadStatus: Account is in progress with status Destroying", fmt.Sprintf("the resource with account_id %d cannot be read or updated because it is currently being destroyed", accountId), ) return diags } return nil } // RestoreAccount performs account Restore request. // Returns error in case of failures. func RestoreAccount(ctx context.Context, accountId uint64, c *decort.DecortClient) diag.Diagnostics { diags := diag.Diagnostics{} restoreReq := account.RestoreRequest{ AccountID: accountId, } tflog.Info(ctx, "RestoreAccount: before calling CloudAPI().Account().Restore", map[string]any{"account_id": accountId, "req": restoreReq}) res, err := c.CloudAPI().Account().Restore(ctx, restoreReq) if err != nil { diags.AddError( "RestoreAccount: cannot restore account", err.Error(), ) return diags } tflog.Info(ctx, "RestoreAccount: response from CloudAPI().Account().Restore", map[string]any{"account_id": accountId, "response": res}) return nil } // EnableDisableAccount performs account Enable/Disable request. // Returns error in case of failures. func EnableDisableAccount(ctx context.Context, accountId uint64, enable bool, c *decort.DecortClient) diag.Diagnostics { tflog.Info(ctx, "Start EnableDisableAccount", map[string]any{"account_id": accountId}) diags := diag.Diagnostics{} if enable { tflog.Info(ctx, "EnableDisableAccount: before calling CloudAPI().Account().Enable", map[string]any{"account_id": accountId}) res, err := c.CloudAPI().Account().Enable(ctx, account.DisableEnableRequest{AccountID: accountId}) if err != nil { diags.AddError( "EnableDisableAccount: cannot enable account", err.Error(), ) return diags } tflog.Info(ctx, "EnableDisableAccount: response from CloudAPI().Account().Enable", map[string]any{"account_id": accountId, "response": res}) return nil } tflog.Info(ctx, "EnableDisableAccount: before calling CloudAPI().Account().Disable", map[string]any{"account_id": accountId}) res, err := c.CloudAPI().Account().Disable(ctx, account.DisableEnableRequest{AccountID: accountId}) if err != nil { diags.AddError( "EnableDisableAccount: cannot disable account", err.Error(), ) return diags } tflog.Info(ctx, "EnableDisableAccount: response from CloudAPI().Account().Disable", map[string]any{"account_id": accountId, "response": res}) return nil } // UpdateAccount updates disk data: account_name, resource_limits, send_access_emails. // Returns error in case of failures. func UpdateAccount(ctx context.Context, accountId uint64, plan, state *models.ResourceAccountModel, c *decort.DecortClient) diag.Diagnostics { tflog.Info(ctx, "Start UpdateAccount", map[string]any{"account_id": accountId}) var diags diag.Diagnostics var updateNeeded bool updateReq := account.UpdateRequest{ AccountID: accountId, } // check if account_name was changed if !plan.AccountName.Equal(state.AccountName) { updateReq.Name = plan.AccountName.ValueString() updateNeeded = true } // check if resource_limits were changed if !plan.ResourceLimits.Equal(state.ResourceLimits) && !plan.ResourceLimits.IsUnknown() { tflog.Info(ctx, "UpdateAccount: new ResourceLimits specified", map[string]any{"account_id": accountId}) var resourceLimitsPlan models.ResourceLimitsInAccountResourceModel diags.Append(plan.ResourceLimits.As(ctx, &resourceLimitsPlan, basetypes.ObjectAsOptions{})...) if diags.HasError() { tflog.Error(ctx, "UpdateAccount: cannot populate ResourceLimits with plan.ResourceLimits object element") return diags } if resourceLimitsPlan.CUM.ValueFloat64() == 0 { updateReq.MaxMemoryCapacity = -1 } else { updateReq.MaxMemoryCapacity = int64(resourceLimitsPlan.CUM.ValueFloat64()) } if resourceLimitsPlan.CUD.ValueFloat64() == 0 { updateReq.MaxVDiskCapacity = -1 } else { updateReq.MaxVDiskCapacity = int64(resourceLimitsPlan.CUD.ValueFloat64()) } if resourceLimitsPlan.CUC.ValueFloat64() == 0 { updateReq.MaxCPUCapacity = -1 } else { updateReq.MaxCPUCapacity = int64(resourceLimitsPlan.CUC.ValueFloat64()) } if resourceLimitsPlan.CUI.ValueFloat64() == 0 { updateReq.MaxNumPublicIP = -1 } else { updateReq.MaxNumPublicIP = int64(resourceLimitsPlan.CUI.ValueFloat64()) } if resourceLimitsPlan.CUNP.ValueFloat64() == 0 { updateReq.MaxNetworkPeerTransfer = -1 } else { updateReq.MaxNetworkPeerTransfer = int64(resourceLimitsPlan.CUNP.ValueFloat64()) } if resourceLimitsPlan.GPUUnits.ValueFloat64() == 0 { updateReq.GPUUnits = -1 } else { updateReq.GPUUnits = int64(resourceLimitsPlan.GPUUnits.ValueFloat64()) } updateNeeded = true } // check if send_access_emails was changed if !plan.SendAccessEmails.Equal(state.SendAccessEmails) && !plan.SendAccessEmails.IsNull() { updateReq.SendAccessEmails = plan.SendAccessEmails.ValueBool() updateNeeded = true } if !updateNeeded { tflog.Info(ctx, "UpdateAccount: no general account update is needed because neither account_name, nor resource_limits, nor send_access_emails were changed.", map[string]any{ "account_id": plan.Id.ValueString(), }) return nil } // perform account update tflog.Info(ctx, "UpdateAccount: before calling CloudAPI().Account().Update", map[string]any{ "account_id": accountId, "req": updateReq, }) res, err := c.CloudAPI().Account().Update(ctx, updateReq) if err != nil { diags.AddError("UpdateAccount: Unable to update account", err.Error()) return diags } tflog.Info(ctx, "UpdateAccount: response from CloudAPI().Account().Update", map[string]any{ "account_id": accountId, "response": res}) return nil } // AddDeleteUsersAccount adds/deletes users to/from account. // In case of failure returns errors. func AddDeleteUsersAccount(ctx context.Context, accountId uint64, plan, state *models.ResourceAccountModel, c *decort.DecortClient) diag.Diagnostics { tflog.Info(ctx, "Start AddDeleteUsersAccount: new users specified", map[string]any{"account_id": accountId}) diags := diag.Diagnostics{} usersPlan := make([]models.UsersModel, 0, len(plan.Users.Elements())) diags.Append(plan.Users.ElementsAs(ctx, &usersPlan, true)...) if diags.HasError() { tflog.Error(ctx, "AddDeleteUsersAccount: cannot populate usersPlan with plan.Users list elements") return diags } usersState := make([]models.UsersModel, 0, len(state.Users.Elements())) diags.Append(state.Users.ElementsAs(ctx, &usersState, true)...) if diags.HasError() { tflog.Error(ctx, "AddDeleteUsersAccount: cannot populate usersState with state.Users list elements") return diags } // define users to be deleted, added and updated var deletedUsers, addedUsers, updatedUsers []models.UsersModel for _, user := range usersState { if !containsUser(usersPlan, user) { deletedUsers = append(deletedUsers, user) } } for _, user := range usersPlan { if !containsUser(usersState, user) { addedUsers = append(addedUsers, user) } else if isChangedUser(usersState, user) { updatedUsers = append(updatedUsers, user) } } // delete users if len(deletedUsers) == 0 { tflog.Info(ctx, "AddDeleteUsersAccount: no users need to be deleted", map[string]any{"account_id": accountId}) } if len(deletedUsers) > 0 { tflog.Info(ctx, "AddDeleteUsersAccount: users need to be deleted", map[string]any{ "accountId": accountId, "deletedUsers": deletedUsers}) for _, user := range deletedUsers { delUserReq := account.DeleteUserRequest{ AccountID: accountId, UserID: user.UserID.ValueString(), } tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().DeleteUser", map[string]any{"account_id": accountId, "req": delUserReq}) res, err := c.CloudAPI().Account().DeleteUser(ctx, delUserReq) tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().DeleteUser", map[string]any{"account_id": accountId, "response": res}) if err != nil { diags.AddError( "AddDeleteUsersAccount: can not delete user from account", err.Error()) } } } // add users if len(addedUsers) == 0 { tflog.Info(ctx, "AddDeleteUsersAccount: no users needs to be added", map[string]any{"account_id": accountId}) } if len(addedUsers) > 0 { tflog.Info(ctx, "AddDeleteUsersAccount: users need to be added", map[string]any{"account_id": accountId}) for _, user := range addedUsers { addUserReq := account.AddUserRequest{ AccountID: accountId, UserID: user.UserID.ValueString(), AccessType: user.AccessType.ValueString(), } tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().AddUser", map[string]any{ "account_id": accountId, "addUserReq": addUserReq}) res, err := c.CloudAPI().Account().AddUser(ctx, addUserReq) if err != nil { diags.AddError("AddDeleteUsersAccount: Unable to add users to account", err.Error()) } tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().AddUser", map[string]any{ "account_id": accountId, "response": res}) } } // update users if len(updatedUsers) == 0 { tflog.Info(ctx, "AddDeleteUsersAccount: no users needs to be updated", map[string]any{"account_id": accountId}) } if len(updatedUsers) > 0 { tflog.Info(ctx, "AddDeleteUsersAccount: users need to be updated", map[string]any{"account_id": accountId}) for _, user := range updatedUsers { updUserReq := account.UpdateUserRequest{ AccountID: accountId, UserID: user.UserID.ValueString(), AccessType: user.AccessType.ValueString(), } tflog.Info(ctx, "AddDeleteUsersAccount: before calling CloudAPI().Account().UpdateUser", map[string]any{ "account_id": accountId, "updatedUsers": updatedUsers}) res, err := c.CloudAPI().Account().UpdateUser(ctx, updUserReq) if err != nil { diags.AddError("AddDeleteUsersAccount: Unable to update users", err.Error()) } tflog.Info(ctx, "AddDeleteUsersAccount: response from CloudAPI().Account().UpdateUser", map[string]any{ "account_id": accountId, "response": res}) } } return diags } func containsUser(users []models.UsersModel, target models.UsersModel) bool { for _, user := range users { if target.UserID == user.UserID { return true } } return false } func isChangedUser(users []models.UsersModel, target models.UsersModel) bool { for _, user := range users { if user.UserID.Equal(target.UserID) && !user.AccessType.Equal(target.AccessType) { return true } } return false }