Skip to content
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

feat: add resource and data source for MAAS zones #258

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ NAMESPACE=canonical
NAME=maas
PROVIDER_NAME=terraform-provider-${NAME}
BINARY=terraform-provider-${NAME}
VERSION=1.0.1
VERSION=2.4.0
skatsaounis marked this conversation as resolved.
Show resolved Hide resolved

OS?=$$(go env GOOS)
ARCH?=$$(go env GOARCH)
Expand Down
50 changes: 50 additions & 0 deletions maas/data_source_maas_zone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package maas

import (
"context"
"fmt"

"github.com/canonical/gomaasclient/client"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func dataSourceMaasZone() *schema.Resource {
return &schema.Resource{
Description: "Provides details about an existing MAAS zone.",
ReadContext: dataSourceZoneRead,

Schema: map[string]*schema.Schema{
"description": {
Type: schema.TypeString,
Computed: true,
Description: "A brief description of the zone.",
},
"name": {
Type: schema.TypeString,
Required: true,
Description: "The zone's name.",
},
},
}
}

func dataSourceZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*client.Client)

zone, err := getZone(client, d.Get("name").(string))
if err != nil {
return diag.FromErr(err)
}
d.SetId(zone.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about setting as terraform ID the database ID of the zone instead of the name? In addition, line 41 "id": fmt.Sprintf("%v", zone.ID), is not needed

Suggested change
d.SetId(zone.Name)
d.SetId(fmt.Sprintf("%v", zone.ID))

tfstate := map[string]interface{}{
"id": fmt.Sprintf("%v", zone.ID),
"name": zone.Name,
"description": zone.Description,
}
if err := setTerraformState(d, tfstate); err != nil {
return diag.FromErr(err)
}

return nil
}
47 changes: 47 additions & 0 deletions maas/data_source_maas_zone_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package maas_test

import (
"fmt"
"terraform-provider-maas/maas/testutils"
"testing"

"github.com/canonical/gomaasclient/entity"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
)

func TestAccDataSourceMaasZone_basic(t *testing.T) {

var zone entity.Zone
description := "Test description"
name := acctest.RandomWithPrefix("tf-zone-")

checks := []resource.TestCheckFunc{
testAccMaasZoneCheckExists("maas_zone.test", &zone),
resource.TestCheckResourceAttr("data.maas_zone.test", "description", description),
resource.TestCheckResourceAttr("data.maas_zone.test", "name", name),
}

resource.ParallelTest(t, resource.TestCase{
PreCheck: func() { testutils.PreCheck(t, nil) },
Providers: testutils.TestAccProviders,
CheckDestroy: testAccCheckMaasZoneDestroy,
ErrorCheck: func(err error) error { return err },
Steps: []resource.TestStep{
{
Config: testAccDataSourceMaasZone(description, name),
Check: resource.ComposeTestCheckFunc(checks...),
},
},
})
}

func testAccDataSourceMaasZone(description string, name string) string {
return fmt.Sprintf(`
%s

data "maas_zone" "test" {
name = maas_zone.test.name
}
`, testAccMaasZone(name, description))
}
2 changes: 2 additions & 0 deletions maas/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ func Provider() *schema.Provider {
"maas_tag": resourceMaasTag(),
"maas_user": resourceMaasUser(),
"maas_resource_pool": resourceMaasResourcePool(),
"maas_zone": resourceMaasZone(),
},
DataSourcesMap: map[string]*schema.Resource{
"maas_boot_source": dataSourceMaasBootSource(),
Expand All @@ -76,6 +77,7 @@ func Provider() *schema.Provider {
"maas_device": dataSourceMaasDevice(),
"maas_resource_pool": dataSourceMaasResourcePool(),
"maas_rack_controller": dataSourceMaasRackController(),
"maas_zone": dataSourceMaasZone(),
},
ConfigureContextFunc: providerConfigure,
}
Expand Down
142 changes: 142 additions & 0 deletions maas/resource_maas_zone.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package maas

import (
"context"
"fmt"

"github.com/canonical/gomaasclient/client"
"github.com/canonical/gomaasclient/entity"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)

func resourceMaasZone() *schema.Resource {
return &schema.Resource{
Description: "Provides a resource to manage MAAS zones.",
CreateContext: resourceZoneCreate,
ReadContext: resourceZoneRead,
UpdateContext: resourceZoneUpdate,
DeleteContext: resourceZoneDelete,
Importer: &schema.ResourceImporter{
StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
client := meta.(*client.Client)
zone, err := getZone(client, d.Id())
if err != nil {
return nil, err
}
tfState := map[string]interface{}{
skatsaounis marked this conversation as resolved.
Show resolved Hide resolved
"name": zone.Name,
"description": zone.Description,
}
if err := setTerraformState(d, tfState); err != nil {
return nil, err
}
return []*schema.ResourceData{d}, nil
},
},

Schema: map[string]*schema.Schema{
"description": {
Type: schema.TypeString,
Optional: true,
Description: "A brief description of the new zone.",
},
"name": {
Type: schema.TypeString,
Required: true,
Description: "The name of the new zone.",
},
},
}
}

func resourceZoneRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*client.Client)

zone, err := client.Zone.Get(d.Id())
if err != nil {
return diag.FromErr(err)
}

tfstate := map[string]any{
"name": zone.Name,
"description": zone.Description,
}

if err := setTerraformState(d, tfstate); err != nil {
return diag.FromErr(err)
}

return nil
}

func resourceZoneCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*client.Client)

params := getZoneParams(d)
zone, err := findZone(client, params.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trying to find if zone already exists in MAAS here costs an extra API call to MAAS. It is accepted to blindly try create the zone as the intention is that Terraform is managing it. If it already exists, MAAS will throw an error and this will be propagated to gomaasclient, then from the client to terraform provider and finally being presented to user as part of diagnostic errors. Then the user can import the zone to Terraform and continue its management with Terraform.

Let's simplify the function. Resource pool is again a good example as the implementation is more or less identical.

if err != nil {
return diag.FromErr(err)
}
if zone == nil {
zone, err = client.Zones.Create(params)
if err != nil {
return diag.FromErr(err)
}
}
d.SetId(zone.Name)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As said earlier, let's save the database ID as a Terraform resource ID


return resourceZoneUpdate(ctx, d, meta)
skatsaounis marked this conversation as resolved.
Show resolved Hide resolved
}

func resourceZoneUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*client.Client)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is needed since we want to save the database ID. Then id should be given to Zone.Update instead of d.Id().

Suggested change
id, err := strconv.Atoi(d.Id())
if err != nil {
return diag.FromErr(err)
}

params := getZoneParams(d)
if _, err := client.Zone.Update(d.Id(), params); err != nil {
return diag.FromErr(err)
}

return resourceZoneRead(ctx, d, meta)
}

func resourceZoneDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
client := meta.(*client.Client)

if err := client.Zone.Delete(d.Id()); err != nil {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be simplified to

Suggested change
if err := client.Zone.Delete(d.Id()); err != nil {
return client.Zone.Delete(id))

given that id is calculated like the previous suggestion

return diag.FromErr(err)
}

return nil
}

func getZoneParams(d *schema.ResourceData) *entity.ZoneParams {
return &entity.ZoneParams{
Name: d.Get("name").(string),
Description: d.Get("description").(string),
}
}

func findZone(client *client.Client, identifier string) (*entity.Zone, error) {
zones, err := client.Zones.Get()
if err != nil {
return nil, err
}
for _, z := range zones {
if fmt.Sprintf("%v", z.ID) == identifier || z.Name == identifier {
return &z, nil
}
}
return nil, nil
}

func getZone(client *client.Client, identifier string) (*entity.Zone, error) {
zone, err := findZone(client, identifier)
if err != nil {
return nil, err
}
if zone == nil {
return nil, fmt.Errorf("zone (%s) was not found", identifier)
}
return zone, nil
}
118 changes: 118 additions & 0 deletions maas/resource_maas_zone_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package maas_test

import (
"fmt"
"strings"
"terraform-provider-maas/maas/testutils"
"testing"

"github.com/canonical/gomaasclient/client"
"github.com/canonical/gomaasclient/entity"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/assert"
)

func TestAccResourceMaasZone_basic(t *testing.T) {

var zone entity.Zone
name := acctest.RandomWithPrefix("tf-zone-")
description := "Test description"

checks := []resource.TestCheckFunc{
testAccMaasZoneCheckExists("maas_zone.test", &zone),
resource.TestCheckResourceAttr("maas_zone.test", "name", name),
resource.TestCheckResourceAttr("maas_zone.test", "description", description),
}

resource.ParallelTest(t, resource.TestCase{
Providers: testutils.TestAccProviders,
skatsaounis marked this conversation as resolved.
Show resolved Hide resolved
CheckDestroy: testAccCheckMaasZoneDestroy,
ErrorCheck: func(err error) error { return err },
Steps: []resource.TestStep{
{
Config: testAccMaasZone(name, description),
Check: resource.ComposeTestCheckFunc(checks...),
},
// Test import using name
skatsaounis marked this conversation as resolved.
Show resolved Hide resolved
{
ResourceName: "maas_zone.test",
ImportState: true,
ImportStateCheck: func(is []*terraform.InstanceState) error {
var zone *terraform.InstanceState
if len(is) != 1 {
return fmt.Errorf("expected 1 state: %#v", t)
}
zone = is[0]
assert.Equal(t, zone.Attributes["name"], name)
assert.Equal(t, zone.Attributes["description"], description)
return nil
},
},
},
})
}

func testAccMaasZoneCheckExists(rn string, zone *entity.Zone) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[rn]
if !ok {
return fmt.Errorf("resource not found: %s\n %#v", rn, s.RootModule().Resources)
}

if rs.Primary.ID == "" {
return fmt.Errorf("resource id not set")
}

conn := testutils.TestAccProvider.Meta().(*client.Client)
gotZone, err := conn.Zone.Get(rs.Primary.ID)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is also needed if we are to save the database ID as the Terraform resource ID

                id, err := strconv.Atoi(rs.Primary.ID)
		if err != nil {
			return err
		}

if err != nil {
return fmt.Errorf("error getting zone: %s", err)
}

*zone = *gotZone

return nil
}
}

func testAccMaasZone(name string, description string) string {
return fmt.Sprintf(`
resource "maas_zone" "test" {
name = "%s"
description = "%s"
}
`, name, description)
}

func testAccCheckMaasZoneDestroy(s *terraform.State) error {
// retrieve the connection established in Provider configuration
conn := testutils.TestAccProvider.Meta().(*client.Client)

// loop through the resources in state, verifying each maas_zone
// is destroyed
for _, rs := range s.RootModule().Resources {
if rs.Type != "maas_zone" {
continue
}

// Retrieve our maas_zone by referencing it's state ID for API lookup
response, err := conn.Zone.Get(rs.Primary.ID)
if err == nil {
if response != nil && response.Name == rs.Primary.ID {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if response != nil && response.Name == rs.Primary.ID {
if response != nil && response.ID == rs.Primary.ID {

return fmt.Errorf("MAAS Zone (%s) still exists.", rs.Primary.ID)
}

return nil
}

// If the error is equivalent to 404 not found, the maas_zone is destroyed.
// Otherwise return the error
if !strings.Contains(err.Error(), "404 Not Found") {
return err
}
}

return nil
}