Skip to content

Commit

Permalink
Merge pull request #244 from NetApp/243-enhancement-security-account-…
Browse files Browse the repository at this point in the history
…add-import-and-update-reapply

243 enhancement security account add import and update reapply
  • Loading branch information
suhasbshekar authored Jul 18, 2024
2 parents ed72bd0 + 83827eb commit 247e929
Show file tree
Hide file tree
Showing 5 changed files with 233 additions and 17 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ FEATURES:

ENHANCEMENTS:
* **netapp-ontap_lun**: added `size_unit` option. ([#227](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/227))
* **netapp-ontap_security_account**: Add support for import and update ([#243](https://github.com/NetApp/terraform-provider-netapp-ontap/issues/243))

## 1.1.2 (2024-06-03)

Expand Down
59 changes: 58 additions & 1 deletion docs/resources/security_account_resource.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,16 @@ description: |-

# Resource Security Account

Create/Delete a ONTAP user account
Create/Modify/Delete a ONTAP user account

### Related ONTAP commands
```commandline
* security login create
* security login delete
* security login modify
* security login password
* security login lock
* security login unlock
```

## Supported Platforms
Expand Down Expand Up @@ -89,4 +93,57 @@ Optional:

- `name` (String) Account role name

## Import
This resource supports import, which allows you to import existing security account into the state of this resource.
Import require a unique ID composed of the security account name and connection profile, separated by a comma.

id = `name`, `cx_profile_name`

### Terraform Import

For example
```shell
terraform import netapp-ontap_security_account.act_import acc_user,cluster4
```

### Terraform Import Block
This requires Terraform 1.5 or higher, and will auto create the configuration for you

First create the block
```terraform
import {
to = netapp-ontap_security_account.act_import
id = "acc_user,cluster4"
}
```
Next run, this will auto create the configuration for you
```shell
terraform plan -generate-config-out=generated.tf
```
This will generate a file called generated.tf, which will contain the configuration for the imported resource
```terraform
# __generated__ by Terraform
# Please review these resources and move them into your main configuration files.
# __generated__ by Terraform from "acc_user,cluster4"
resource "netapp-ontap_security_account" "act_import" {
cx_profile_name = "cluster4"
name = "acc_user"
applications = [
{
application = "http"
authentication_methods = ["password"]
second_authentication_method = "none"
}
]
comment = null
locked = false
owner = {
name = "abccluster-1"
}
password = null # sensitive
role = {
name = "admin"
}
second_authentication_method = null
}
```
28 changes: 28 additions & 0 deletions internal/interfaces/security_account.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package interfaces

import (
"fmt"

"github.com/hashicorp/terraform-plugin-log/tflog"
"github.com/mitchellh/mapstructure"
"github.com/netapp/terraform-provider-netapp-ontap/internal/restclient"
Expand Down Expand Up @@ -31,6 +32,17 @@ type SecurityAccountGetDataModelONTAP struct {
Applications []SecurityAccountApplication `mapstructure:"applications,omitempty"`
}

// SecurityAccountResourceUpdateBodyDataModelONTAP describes the resource update model using go types for mapping.
type SecurityAccountResourceUpdateBodyDataModelONTAP struct {
Applications []map[string]interface{} `mapstructure:"applications,omitempty"`
// Owner SecurityAccountOwner `mapstructure:"owner,omitempty"`
Role SecurityAccountRole `mapstructure:"role,omitempty"`
Password string `mapstructure:"password,omitempty"`
// SecondAuthenticationMethod string `mapstructure:"second_authentication_method,omitempty"`
Comment string `mapstructure:"comment,omitempty"`
Locked bool `mapstructure:"locked,omitempty"`
}

// SecurityAccountApplication describes the application data model using go types for mapping.
type SecurityAccountApplication struct {
Application string `mapstructure:"application,omitempty"`
Expand Down Expand Up @@ -147,3 +159,19 @@ func DeleteSecurityAccount(errorHandler *utils.ErrorHandler, r restclient.RestCl
}
return nil
}

// UpdateSecurityAccount to update a security account
func UpdateSecurityAccount(errorHandler *utils.ErrorHandler, r restclient.RestClient, data SecurityAccountResourceUpdateBodyDataModelONTAP, uuid string, name string) error {
var body map[string]interface{}
if err := mapstructure.Decode(data, &body); err != nil {
return errorHandler.MakeAndReportError("error encoding security account body", fmt.Sprintf("error on encoding security/accounts body: %s, body: %#v", err, data))
}
tflog.Debug(errorHandler.Ctx, fmt.Sprintf("Update security account info: %#v", data))
query := r.NewQuery()
query.Add("return_records", "true")
statusCode, _, err := r.CallUpdateMethod("security/accounts/"+uuid+"/"+name, query, body)
if err != nil {
return errorHandler.MakeAndReportError("error updating security account", fmt.Sprintf("error on PATCH security/accounts: %s, statusCode %d", err, statusCode))
}
return nil
}
122 changes: 117 additions & 5 deletions internal/provider/security/security_account_resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ package security
import (
"context"
"fmt"
"strings"

"github.com/hashicorp/terraform-plugin-framework/attr"
"github.com/hashicorp/terraform-plugin-framework/path"
"github.com/hashicorp/terraform-plugin-framework/resource"
Expand Down Expand Up @@ -207,7 +209,7 @@ func (r *SecurityAccountResource) Read(ctx context.Context, req resource.ReadReq
}
tflog.Debug(ctx, fmt.Sprintf("read a resource: %#v", data))
var restInfo *interfaces.SecurityAccountGetDataModelONTAP
if data.Owner.IsUnknown() {
if data.Owner.IsNull() {
restInfo, err = interfaces.GetSecurityAccountByName(errorHandler, *client, data.Name.ValueString(), "")
if err != nil {
// error reporting done inside GetSecurityAccount
Expand Down Expand Up @@ -420,17 +422,115 @@ func (r *SecurityAccountResource) Create(ctx context.Context, req resource.Creat

// Update updates the resource and sets the updated Terraform state on success.
func (r *SecurityAccountResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) {
var data *SecurityAccountResourceModel
var plan, state *SecurityAccountResourceModel

// Read Terraform plan data into the model
resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...)
resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...)
resp.Diagnostics.Append(req.State.Get(ctx, &state)...)
errorHandler := utils.NewErrorHandler(ctx, &resp.Diagnostics)

if resp.Diagnostics.HasError() {
return
}
client, err := connection.GetRestClient(utils.NewErrorHandler(ctx, &resp.Diagnostics), r.config, plan.CxProfileName)
if err != nil {
// error reporting done inside NewClient
return
}

var request interfaces.SecurityAccountResourceUpdateBodyDataModelONTAP

// applications update
applications := []interfaces.SecurityAccountApplication{}
for _, item := range plan.Applications {
var application interfaces.SecurityAccountApplication
application.Application = item.Application.ValueString()
if item.SecondAuthentiactionMethod.IsNull() {
application.SecondAuthenticationMethod = item.SecondAuthentiactionMethod.ValueString()
}
if item.AuthenticationMethods != nil {
application.AuthenticationMethods = make([]string, len(*item.AuthenticationMethods))
for index, authenticationMethod := range *item.AuthenticationMethods {
application.AuthenticationMethods[index] = authenticationMethod.ValueString()
}
}
applications = append(applications, application)
}
err = mapstructure.Decode(applications, &request.Applications)
if err != nil {
errorHandler.MakeAndReportError("error creating User applications", fmt.Sprintf("error on encoding copies info: %s, copies %#v", err, request.Applications))
return
}
// password update
if !plan.Password.Equal(state.Password) {
request.Password = plan.Password.ValueString()
}

// role update
if !plan.Role.IsUnknown() {
var role RoleResourceModel
diags := plan.Role.As(ctx, &role, basetypes.ObjectAsOptions{})
if diags.HasError() {
resp.Diagnostics.Append(diags...)
return
}
request.Role.Name = role.Name.ValueString()
}

// locked update
if !plan.Locked.IsNull() {
request.Locked = plan.Locked.ValueBool()
}

// comment update
if !plan.Comment.Equal(state.Comment) {
request.Comment = plan.Comment.ValueString()
}

err = interfaces.UpdateSecurityAccount(errorHandler, *client, request, state.OwnerID.ValueString(), state.Name.ValueString())
if err != nil {
return
}

// Read the resource again to get the updated data
restInfo, err := interfaces.GetSecurityAccountByName(errorHandler, *client, plan.Name.ValueString(), state.OwnerID.ValueString())
if err != nil {
// error reporting done inside GetSecurityAccount
return
}
plan.Name = types.StringValue(restInfo.Name)
// There is no ID in the REST response, so we use the name as ID
plan.ID = types.StringValue(restInfo.Name)
elementTypes := map[string]attr.Type{
"name": types.StringType,
}
elements := map[string]attr.Value{
"name": types.StringValue(restInfo.Owner.Name),
}
objectValue, diags := types.ObjectValue(elementTypes, elements)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
}

plan.Owner = objectValue
plan.OwnerID = types.StringValue(restInfo.Owner.UUID)
plan.Locked = types.BoolValue(restInfo.Locked)
if restInfo.Comment != "" {
plan.Comment = types.StringValue(restInfo.Comment)
}
elementTypes = map[string]attr.Type{
"name": types.StringType,
}
elements = map[string]attr.Value{
"name": types.StringValue(restInfo.Role.Name),
}
objectValue, diags = types.ObjectValue(elementTypes, elements)
if diags.HasError() {
resp.Diagnostics.Append(diags...)
}
plan.Role = objectValue
// Save updated data into Terraform state
resp.Diagnostics.Append(resp.State.Set(ctx, &data)...)
resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...)
}

// Delete deletes the resource and removes the Terraform state on success.
Expand Down Expand Up @@ -465,5 +565,17 @@ func (r *SecurityAccountResource) Delete(ctx context.Context, req resource.Delet

// ImportState imports a resource using ID from terraform import command by calling the Read method.
func (r *SecurityAccountResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp)
tflog.Debug(ctx, fmt.Sprintf("import req security account resource: %#v", req))
// Parse the ID
idParts := strings.Split(req.ID, ",")
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
resp.Diagnostics.AddError(
"Unexpected Import Identifier",
fmt.Sprint("Expected ID in the format 'name,cx_profile_name', got: ", req.ID),
)
return
}

resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("name"), idParts[0])...)
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("cx_profile_name"), idParts[1])...)
}
40 changes: 29 additions & 11 deletions internal/provider/security/security_account_resource_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ package security_test

import (
"fmt"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
ntest "github.com/netapp/terraform-provider-netapp-ontap/internal/provider"
"os"
"testing"

"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
ntest "github.com/netapp/terraform-provider-netapp-ontap/internal/provider"
)

func TestAccSecurityAccountResource(t *testing.T) {
Expand All @@ -14,19 +15,36 @@ func TestAccSecurityAccountResource(t *testing.T) {
ProtoV6ProviderFactories: ntest.TestAccProtoV6ProviderFactories,
Steps: []resource.TestStep{
{
Config: testAccSecurityAccountResourceConfig(),
Config: testAccSecurityAccountResourceConfig("carchitest", "password"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("netapp-ontap_security_account.security_account", "name", "carchitest"),
),
},
// Test updating a resource
{
Config: testAccSecurityAccountResourceConfig("carchitest", "password123"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("netapp-ontap_security_account.security_account", "name", "carchitest"),
resource.TestCheckResourceAttr("netapp-ontap_security_account.security_account", "password", "password123"),
),
},
// Test importing a resource
{
ResourceName: "netapp-ontap_security_account.security_account",
ImportState: true,
ImportStateId: fmt.Sprintf("%s,%s", "acc_user", "cluster2"),
Check: resource.ComposeTestCheckFunc(
resource.TestCheckResourceAttr("netapp-ontap_security_account.security_account", "name", "acc_user"),
),
},
},
})
}

func testAccSecurityAccountResourceConfig() string {
host := os.Getenv("TF_ACC_NETAPP_HOST")
func testAccSecurityAccountResourceConfig(name string, accpassword string) string {
host := os.Getenv("TF_ACC_NETAPP_HOST2")
admin := os.Getenv("TF_ACC_NETAPP_USER")
password := os.Getenv("TF_ACC_NETAPP_PASS")
password := os.Getenv("TF_ACC_NETAPP_PASS2")
if host == "" || admin == "" || password == "" {
fmt.Println("TF_ACC_NETAPP_HOST, TF_ACC_NETAPP_USER, and TF_ACC_NETAPP_PASS must be set for acceptance tests")
os.Exit(1)
Expand All @@ -35,7 +53,7 @@ func testAccSecurityAccountResourceConfig() string {
provider "netapp-ontap" {
connection_profiles = [
{
name = "cluster4"
name = "cluster2"
hostname = "%s"
username = "%s"
password = "%s"
Expand All @@ -46,13 +64,13 @@ provider "netapp-ontap" {
resource "netapp-ontap_security_account" "security_account" {
# required to know which system to interface with
cx_profile_name = "cluster4"
name = "carchitest"
cx_profile_name = "cluster2"
name = "%s"
applications = [{
application = "http"
authentication_methods = ["password"]
}]
password = "netapp1!"
password = "%s"
}
`, host, admin, password)
`, host, admin, password, name, accpassword)
}

0 comments on commit 247e929

Please sign in to comment.