From 025be9cad417ab7f47eafafe9e53c4b92fa0b8d3 Mon Sep 17 00:00:00 2001 From: Dimitrios Karagiannis Date: Wed, 12 Feb 2020 14:04:18 +0000 Subject: [PATCH] megaport_mcr: add new resource to manage MCRs Signed-off-by: Dimitrios Karagiannis --- examples/megaport_mcr_basic/main.tf | 12 ++ examples/megaport_mcr_full/main.tf | 19 +++ megaport/common_test.go | 8 + megaport/provider.go | 1 + megaport/resource_megaport_mcr.go | 187 +++++++++++++++++++++ megaport/resource_megaport_mcr_test.go | 206 ++++++++++++++++++++++++ megaport/resource_megaport_port_test.go | 1 + 7 files changed, 434 insertions(+) create mode 100644 examples/megaport_mcr_basic/main.tf create mode 100644 examples/megaport_mcr_full/main.tf create mode 100644 megaport/resource_megaport_mcr.go create mode 100644 megaport/resource_megaport_mcr_test.go diff --git a/examples/megaport_mcr_basic/main.tf b/examples/megaport_mcr_basic/main.tf new file mode 100644 index 0000000..e622fc2 --- /dev/null +++ b/examples/megaport_mcr_basic/main.tf @@ -0,0 +1,12 @@ +data "megaport_location" "foo" { + name_regex = "{{ .location }}" + mcr_available = {{ .mcr_version }} +} + +resource "megaport_mcr" "foo" { + mcr_version = {{ .mcr_version }} + name = "terraform_acctest_{{ .uid }}" + location_id = data.megaport_location.foo.id + rate_limit = {{ .rate_limit }} +} + diff --git a/examples/megaport_mcr_full/main.tf b/examples/megaport_mcr_full/main.tf new file mode 100644 index 0000000..ac56b69 --- /dev/null +++ b/examples/megaport_mcr_full/main.tf @@ -0,0 +1,19 @@ +data "megaport_location" "foo" { + name_regex = "{{ .location }}" + mcr_available = {{ .mcr_version }} +} + +resource "megaport_mcr" "foo" { + mcr_version = {{ .mcr_version }} + name = "terraform_acctest_{{ .uid }}" + location_id = data.megaport_location.foo.id + rate_limit = {{ .rate_limit }} + invoice_reference = "{{ .uid }}" +{{- if .term }} + term = {{ .term }} +{{- end }} +{{- if .asn }} + asn = {{ .asn }} +{{- end }} +} + diff --git a/megaport/common_test.go b/megaport/common_test.go index 0f6ab38..4273c87 100644 --- a/megaport/common_test.go +++ b/megaport/common_test.go @@ -78,6 +78,14 @@ func testAccCheckResourceDestroy(s *terraform.State) error { if v != nil && !isResourceDeleted(v.ProvisioningStatus) { return fmt.Errorf("testAccCheckResourceDestroy: %q (%s) has not been destroyed", n, rs.Primary.ID) } + case "megaport_mcr": + v, err := cfg.Client.GetMcr(rs.Primary.ID) + if err != nil { + return err + } + if v != nil && !isResourceDeleted(v.ProvisioningStatus) { + return fmt.Errorf("testAccCheckResourceDestroy: %q (%s) has not been destroyed", n, rs.Primary.ID) + } case "megaport_aws_vxc": fallthrough case "megaport_gcp_vxc": diff --git a/megaport/provider.go b/megaport/provider.go index daa5171..3e47527 100644 --- a/megaport/provider.go +++ b/megaport/provider.go @@ -40,6 +40,7 @@ func Provider() terraform.ResourceProvider { ResourcesMap: map[string]*schema.Resource{ "megaport_port": resourceMegaportPort(), + "megaport_mcr": resourceMegaportMcr(), "megaport_aws_vxc": resourceMegaportAwsVxc(), "megaport_gcp_vxc": resourceMegaportGcpVxc(), "megaport_private_vxc": resourceMegaportPrivateVxc(), diff --git a/megaport/resource_megaport_mcr.go b/megaport/resource_megaport_mcr.go new file mode 100644 index 0000000..69f6db3 --- /dev/null +++ b/megaport/resource_megaport_mcr.go @@ -0,0 +1,187 @@ +package megaport + +import ( + "log" + "time" + + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/helper/validation" + "github.com/utilitywarehouse/terraform-provider-megaport/megaport/api" +) + +func resourceMegaportMcr() *schema.Resource { + return &schema.Resource{ + Create: resourceMegaportMcrCreate, + Read: resourceMegaportMcrRead, + Update: resourceMegaportMcrUpdate, + Delete: resourceMegaportMcrDelete, + + Importer: &schema.ResourceImporter{ + State: schema.ImportStatePassthrough, + }, + + Schema: map[string]*schema.Schema{ + "mcr_version": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + ValidateFunc: validation.IntInSlice([]int{1, 2}), + }, + "location_id": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "name": { + Type: schema.TypeString, + Required: true, + }, + "rate_limit": { + Type: schema.TypeInt, + Required: true, + ForceNew: true, + }, + "term": { + Type: schema.TypeInt, + Optional: true, + ForceNew: true, + Default: 1, + ValidateFunc: validation.IntInSlice([]int{1, 12, 24, 36}), + }, + "asn": { + Type: schema.TypeInt, + Optional: true, + Computed: true, + ForceNew: true, + }, + "invoice_reference": { + Type: schema.TypeString, + Optional: true, + }, + }, + } +} + +func resourceMegaportMcrRead(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + p, err := cfg.Client.GetMcr(d.Id()) + if err != nil { + log.Printf("resourceMegaportMcrRead: %v", err) + d.SetId("") + return nil + } + if err := d.Set("mcr_version", p.McrVersion()); err != nil { + return err + } + if err := d.Set("location_id", int(p.LocationId)); err != nil { + return err + } + if err := d.Set("name", p.ProductName); err != nil { + return err + } + if err := d.Set("rate_limit", int(p.PortSpeed)); err != nil { + return err + } + if err := d.Set("asn", int(p.Resources.VirtualRouter.McrASN)); err != nil { + return err + } + if err := d.Set("invoice_reference", p.CostCentre); err != nil { + return err + } + return nil +} + +func resourceMegaportMcrCreate(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + var input api.McrCreateInput + switch d.Get("mcr_version").(int) { + case 1: + input = &api.Mcr1CreateInput{ + LocationId: api.Uint64FromInt(d.Get("location_id")), + Name: api.String(d.Get("name")), + RateLimit: api.Uint64FromInt(d.Get("rate_limit")), + Asn: api.Uint64FromInt(d.Get("asn")), + Term: api.Uint64FromInt(d.Get("term")), + InvoiceReference: api.String(d.Get("invoice_reference")), + } + case 2: + input = &api.Mcr2CreateInput{ + LocationId: api.Uint64FromInt(d.Get("location_id")), + Name: api.String(d.Get("name")), + RateLimit: api.Uint64FromInt(d.Get("rate_limit")), + Asn: api.Uint64FromInt(d.Get("asn")), + InvoiceReference: api.String(d.Get("invoice_reference")), + } + } + uid, err := cfg.Client.CreateMcr(input) + if err != nil { + return err + } + d.SetId(*uid) + if err := waitUntilMcrIsConfigured(cfg.Client, *uid, 5*time.Minute); err != nil { + return err + } + return resourceMegaportMcrRead(d, m) +} + +func resourceMegaportMcrUpdate(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + var input api.McrUpdateInput + switch d.Get("mcr_version").(int) { + case 1: + input = &api.Mcr1UpdateInput{ + InvoiceReference: api.String(d.Get("invoice_reference")), + Name: api.String(d.Get("name")), + ProductUid: api.String(d.Id()), + } + case 2: + input = &api.Mcr2UpdateInput{ + InvoiceReference: api.String(d.Get("invoice_reference")), + Name: api.String(d.Get("name")), + ProductUid: api.String(d.Id()), + } + } + if err := cfg.Client.UpdateMcr(input); err != nil { + return err + } + if err := waitUntilMcrIsConfigured(cfg.Client, d.Id(), 5*time.Minute); err != nil { + return err + } + return resourceMegaportMcrRead(d, m) +} + +func resourceMegaportMcrDelete(d *schema.ResourceData, m interface{}) error { + cfg := m.(*Config) + err := cfg.Client.DeleteMcr(d.Id()) + if err != nil && err != api.ErrNotFound { + return err + } + if err == api.ErrNotFound { + log.Printf("resourceMegaportMcrDelete: resource not found, deleting anyway") + } + return nil +} + +func waitUntilMcrIsConfigured(client *api.Client, productUid string, timeout time.Duration) error { + scc := &resource.StateChangeConf{ + Target: []string{api.ProductStatusConfigured, api.ProductStatusLive}, + Refresh: func() (interface{}, string, error) { + v, err := client.GetMcr(productUid) + if err != nil { + log.Printf("[ERROR] Could not retrieve MCR while waiting for setup to finish: %v", err) + return nil, "", err + } + if v == nil { + return nil, "", nil + } + return v, v.ProvisioningStatus, nil + }, + Timeout: timeout, + MinTimeout: 10 * time.Second, + Delay: 5 * time.Second, + } + log.Printf("[INFO] Waiting for MCR (%s) to be configured", productUid) + _, err := scc.WaitForState() + return err +} diff --git a/megaport/resource_megaport_mcr_test.go b/megaport/resource_megaport_mcr_test.go new file mode 100644 index 0000000..87db134 --- /dev/null +++ b/megaport/resource_megaport_mcr_test.go @@ -0,0 +1,206 @@ +package megaport + +import ( + "fmt" + "log" + "math" + "strconv" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/helper/resource" + "github.com/utilitywarehouse/terraform-provider-megaport/megaport/api" +) + +func init() { + resource.AddTestSweepers("megaport_mcr", &resource.Sweeper{ + Name: "megaport_mcr", + Dependencies: []string{ + "megaport_aws_vxc", + "megaport_gcp_vxc", + "megaport_private_vxc", + }, + F: func(region string) error { + c, err := sharedClientForRegion(region) + if err != nil { + return fmt.Errorf("Error getting client: %s", err) + } + client := c.(*api.Client) + mcrs, err := client.ListMcrs() + if err != nil { + return err + } + for _, m := range mcrs { + if strings.HasPrefix(m.ProductName, "terraform_acctest_") && !client.IsResourceDeleted(m.ProvisioningStatus) { + if err := client.DeleteMcr(m.ProductUid); err != nil { + log.Printf("[ERROR] Could not destroy mcr %q (%s) during sweep: %s", m.ProductName, m.ProductUid, err) + } + } + } + return nil + }, + }) +} + +func TestAccMegaportMcr1_basic(t *testing.T) { + var mcr, mcrUpdated, mcrNew api.Product + rName := "t" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + configValues := map[string]interface{}{ + "uid": rName, + "mcr_version": 1, + "location": "Digital Realty LHR20", // mcr1 + "rate_limit": 100, + } + cfg, err := newTestAccConfig("megaport_mcr_basic", configValues, 0) + if err != nil { + t.Fatal(err) + } + cfgUpdate, err := newTestAccConfig("megaport_mcr_full", configValues, 1) + if err != nil { + t.Fatal(err) + } + rAsn := 1 + acctest.RandIntRange(0, math.MaxInt32) + configValuesNew := mergeMaps(configValues, map[string]interface{}{ + "asn": rAsn, + "term": 12, + "rate_limit": 500, + }) + cfgForceNew, err := newTestAccConfig("megaport_mcr_full", configValuesNew, 2) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckResourceDestroy, + Steps: []resource.TestStep{ + { + PreConfig: func() { cfg.log() }, + Config: cfg.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcr), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "100"), + resource.TestCheckResourceAttrSet("megaport_mcr.foo", "asn"), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "term", "1"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", ""), + ), + }, + { + PreConfig: func() { cfgUpdate.log() }, + Config: cfgUpdate.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcrUpdated), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "100"), + resource.TestCheckResourceAttrSet("megaport_mcr.foo", "asn"), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "term", "1"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", rName), + ), + }, + { + PreConfig: func() { cfgForceNew.log() }, + Config: cfgForceNew.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcrNew), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "500"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "asn", strconv.FormatUint(uint64(rAsn), 10)), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "term", "12"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", rName), + ), + }, + }, + }) + + if mcr.ProductUid != mcrUpdated.ProductUid { + t.Errorf("TestAccMegaportMcr_basic: expected the MCR to be updated but the resource ids differ") + } + if mcr.ProductUid == mcrNew.ProductUid { + t.Errorf("TestAccMegaportMcr_basic: expected the MCR to be recreated but the resource ids are identical") + } +} + +func TestAccMegaportMcr2_basic(t *testing.T) { + var mcr, mcrUpdated, mcrNew api.Product + rName := "t" + acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + configValues := map[string]interface{}{ + "uid": rName, + "mcr_version": 2, + "location": "Global Switch London East", + "rate_limit": 1000, + } + cfg, err := newTestAccConfig("megaport_mcr_basic", configValues, 0) + if err != nil { + t.Fatal(err) + } + cfgUpdate, err := newTestAccConfig("megaport_mcr_full", configValues, 1) + if err != nil { + t.Fatal(err) + } + rAsn := 1 + acctest.RandIntRange(0, math.MaxInt32) + configValuesNew := mergeMaps(configValues, map[string]interface{}{ + "asn": rAsn, + "rate_limit": 2500, + }) + cfgForceNew, err := newTestAccConfig("megaport_mcr_full", configValuesNew, 2) + if err != nil { + t.Fatal(err) + } + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + Providers: testAccProviders, + CheckDestroy: testAccCheckResourceDestroy, + Steps: []resource.TestStep{ + { + PreConfig: func() { cfg.log() }, + Config: cfg.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcr), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "1000"), + resource.TestCheckResourceAttrSet("megaport_mcr.foo", "asn"), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", ""), + ), + }, + { + PreConfig: func() { cfgUpdate.log() }, + Config: cfgUpdate.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcrUpdated), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "1000"), + resource.TestCheckResourceAttrSet("megaport_mcr.foo", "asn"), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", rName), + ), + }, + { + PreConfig: func() { cfgForceNew.log() }, + Config: cfgForceNew.Config, + Check: resource.ComposeTestCheckFunc( + testAccCheckResourceExists("megaport_mcr.foo", &mcrNew), + resource.TestCheckResourceAttr("megaport_mcr.foo", "name", "terraform_acctest_"+rName), + resource.TestCheckResourceAttr("megaport_mcr.foo", "rate_limit", "2500"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "asn", strconv.FormatUint(uint64(rAsn), 10)), + resource.TestCheckResourceAttrPair("megaport_mcr.foo", "location_id", "data.megaport_location.foo", "id"), + resource.TestCheckResourceAttr("megaport_mcr.foo", "invoice_reference", rName), + ), + }, + }, + }) + + if mcr.ProductUid != mcrUpdated.ProductUid { + t.Errorf("TestAccMegaportMcr_basic: expected the MCR to be updated but the resource ids differ") + } + if mcr.ProductUid == mcrNew.ProductUid { + t.Errorf("TestAccMegaportMcr_basic: expected the MCR to be recreated but the resource ids are identical") + } +} diff --git a/megaport/resource_megaport_port_test.go b/megaport/resource_megaport_port_test.go index 4fb1961..9e08193 100644 --- a/megaport/resource_megaport_port_test.go +++ b/megaport/resource_megaport_port_test.go @@ -16,6 +16,7 @@ func init() { Name: "megaport_port", Dependencies: []string{ "megaport_aws_vxc", + "megaport_gcp_vxc", "megaport_private_vxc", }, F: func(region string) error {