From 1a7ce9755847ff4a20d8363a3f40e6619fba0cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20V=C3=A4lim=C3=A4ki?= Date: Thu, 28 Sep 2023 04:39:00 +0300 Subject: [PATCH] feat(router): static routes support --- CHANGELOG.md | 1 + go.mod | 2 +- go.sum | 4 +- internal/service/router/router.go | 107 +++++++++++++++++++++--- upcloud/resource_upcloud_router_test.go | 55 ++++++++++-- 5 files changed, 146 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ca158caa..0171b701 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ See updating [Changelog example here](https://keepachangelog.com/en/1.0.0/) ### Added - gateway: add read-only `addresses` field - kubernetes: `control_plane_ip_filter` field to `upcloud_kubernetes_cluster` resource +- router: `static_routes` block to `upcloud_router` resource ### Changed - kubernetes: remove node group maximum value validation. The maximum number of nodes (in the cluster) is determined by the cluster plan and the validation is done on the API side. diff --git a/go.mod b/go.mod index de6943e2..d64e517c 100644 --- a/go.mod +++ b/go.mod @@ -3,7 +3,7 @@ module github.com/UpCloudLtd/terraform-provider-upcloud go 1.20 require ( - github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.0 + github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.1-0.20230927070743-7dd13ddf6ef5 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/go-retryablehttp v0.6.8 github.com/hashicorp/go-uuid v1.0.3 diff --git a/go.sum b/go.sum index 72919acc..48bc3f7c 100644 --- a/go.sum +++ b/go.sum @@ -4,8 +4,8 @@ github.com/Microsoft/go-winio v0.4.16 h1:FtSW/jqD+l4ba5iPBj9CODVtgfYAD8w2wS923g/ github.com/Microsoft/go-winio v0.4.16/go.mod h1:XB6nPKklQyQ7GC9LdcBEcBl8PF76WugXOPRXwdLnMv0= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7 h1:YoJbenK9C67SkzkDfmQuVln04ygHj3vjZfd9FL+GmQQ= github.com/ProtonMail/go-crypto v0.0.0-20210428141323-04723f9f07d7/go.mod h1:z4/9nQmJSSwwds7ejkxaJwO37dru3geImFUdJlaLzQo= -github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.0 h1:Fc9a083OBzl8i4pDV2KXCAfxo4gCjJFHgRuPvRnroBY= -github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.0/go.mod h1:I8rWmBBl+OhiY3AGzKbrobiE5TsLCLNYkCQxE4eJcTg= +github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.1-0.20230927070743-7dd13ddf6ef5 h1:gwwrCax+n27YwYXHmki5DAcDBNFuB5wFJEWkGhPZ8Gs= +github.com/UpCloudLtd/upcloud-go-api/v6 v6.6.1-0.20230927070743-7dd13ddf6ef5/go.mod h1:I8rWmBBl+OhiY3AGzKbrobiE5TsLCLNYkCQxE4eJcTg= github.com/acomagu/bufpipe v1.0.3 h1:fxAGrHZTgQ9w5QqVItgzwj235/uYZYgbXitB+dLupOk= github.com/acomagu/bufpipe v1.0.3/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= diff --git a/internal/service/router/router.go b/internal/service/router/router.go index fe6fe655..943bc443 100644 --- a/internal/service/router/router.go +++ b/internal/service/router/router.go @@ -5,10 +5,13 @@ import ( "fmt" "github.com/UpCloudLtd/terraform-provider-upcloud/internal/utils" + + "github.com/UpCloudLtd/upcloud-go-api/v6/upcloud" "github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/request" "github.com/UpCloudLtd/upcloud-go-api/v6/upcloud/service" "github.com/hashicorp/terraform-plugin-sdk/v2/diag" "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" ) func ResourceRouter() *schema.Resource { @@ -42,20 +45,59 @@ func ResourceRouter() *schema.Resource { Type: schema.TypeString, }, }, + "static_route": { + Description: "A collection of static routes for this router", + Optional: true, + Type: schema.TypeSet, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "name": { + Description: "Name or description of the route.", + Type: schema.TypeString, + Optional: true, + Computed: true, + }, + "nexthop": { + Description: "Next hop address. NOTE: For static route to be active the next hop has to be an address of a reachable running Cloud Server in one of the Private Networks attached to the router.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.Any(validation.IsIPv4Address, validation.IsIPv6Address), + }, + "route": { + Description: "Destination prefix of the route.", + Type: schema.TypeString, + Required: true, + ValidateFunc: validation.Any(validation.IsCIDR), + }, + }, + }, + }, }, } } -func resourceRouterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceRouterCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { client := meta.(*service.Service) - var diags diag.Diagnostics - - opts := &request.CreateRouterRequest{ + req := &request.CreateRouterRequest{ Name: d.Get("name").(string), } - router, err := client.CreateRouter(ctx, opts) + if v, ok := d.GetOk("static_route"); ok { + for _, staticRoute := range v.(*schema.Set).List() { + staticRouteData := staticRoute.(map[string]interface{}) + + r := upcloud.StaticRoute{ + Name: staticRouteData["name"].(string), + Nexthop: staticRouteData["nexthop"].(string), + Route: staticRouteData["route"].(string), + } + + req.StaticRoutes = append(req.StaticRoutes, r) + } + } + + router, err := client.CreateRouter(ctx, req) if err != nil { return diag.FromErr(err) } @@ -70,16 +112,27 @@ func resourceRouterCreate(ctx context.Context, d *schema.ResourceData, meta inte return diag.FromErr(err) } + var staticRoutes []map[string]interface{} + for _, staticRoute := range router.StaticRoutes { + staticRoutes = append(staticRoutes, map[string]interface{}{ + "name": staticRoute.Name, + "nexthop": staticRoute.Nexthop, + "route": staticRoute.Route, + }) + } + + if err := d.Set("static_route", staticRoutes); err != nil { + return diag.FromErr(err) + } + d.SetId(router.UUID) return diags } -func resourceRouterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceRouterRead(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { client := meta.(*service.Service) - var diags diag.Diagnostics - opts := &request.GetRouterDetailsRequest{ UUID: d.Id(), } @@ -105,21 +158,50 @@ func resourceRouterRead(ctx context.Context, d *schema.ResourceData, meta interf return diag.FromErr(err) } + var staticRoutes []map[string]interface{} + for _, staticRoute := range router.StaticRoutes { + staticRoutes = append(staticRoutes, map[string]interface{}{ + "name": staticRoute.Name, + "nexthop": staticRoute.Nexthop, + "route": staticRoute.Route, + }) + } + + if err := d.Set("static_route", staticRoutes); err != nil { + return diag.FromErr(err) + } + return diags } func resourceRouterUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*service.Service) - opts := &request.ModifyRouterRequest{ + req := &request.ModifyRouterRequest{ UUID: d.Id(), } if v, ok := d.GetOk("name"); ok { - opts.Name = v.(string) + req.Name = v.(string) } - _, err := client.ModifyRouter(ctx, opts) + var staticRoutes []upcloud.StaticRoute + + if v, ok := d.GetOk("static_route"); ok { + for _, staticRoute := range v.(*schema.Set).List() { + staticRouteData := staticRoute.(map[string]interface{}) + + staticRoutes = append(staticRoutes, upcloud.StaticRoute{ + Name: staticRouteData["name"].(string), + Nexthop: staticRouteData["nexthop"].(string), + Route: staticRouteData["route"].(string), + }) + } + } + + req.StaticRoutes = &staticRoutes + + _, err := client.ModifyRouter(ctx, req) if err != nil { return diag.FromErr(err) } @@ -127,9 +209,8 @@ func resourceRouterUpdate(ctx context.Context, d *schema.ResourceData, meta inte return resourceRouterRead(ctx, d, meta) } -func resourceRouterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { +func resourceRouterDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) (diags diag.Diagnostics) { client := meta.(*service.Service) - var diags diag.Diagnostics router, err := client.GetRouterDetails(ctx, &request.GetRouterDetailsRequest{ UUID: d.Id(), diff --git a/upcloud/resource_upcloud_router_test.go b/upcloud/resource_upcloud_router_test.go index 8b63fa44..b8e714cd 100644 --- a/upcloud/resource_upcloud_router_test.go +++ b/upcloud/resource_upcloud_router_test.go @@ -20,16 +20,22 @@ func TestAccUpCloudRouter(t *testing.T) { var router upcloud.Router name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) + staticRoutes := []upcloud.StaticRoute{{Name: "my-example-route", Nexthop: "10.0.0.100", Route: "0.0.0.0/0"}} resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviderFactories(&providers), CheckDestroy: testAccCheckRouterDestroy, Steps: []resource.TestStep{ { - Config: testAccRouterConfig(name), + Config: testAccRouterConfig(name, staticRoutes), Check: resource.ComposeTestCheckFunc( testAccCheckRouterExists("upcloud_router.my_example_router", &router), testAccCheckUpCloudRouterAttributes(&router, name), + resource.TestCheckTypeSetElemNestedAttrs("upcloud_router.my_example_router", "static_route.*", map[string]string{ + "name": "my-example-route", + "nexthop": "10.0.0.100", + "route": "0.0.0.0/0", + }), ), }, }, @@ -43,23 +49,36 @@ func TestAccUpCloudRouter_update(t *testing.T) { name := fmt.Sprintf("tf-test-%s", acctest.RandString(10)) updateName := fmt.Sprintf("tf-test-update-%s", acctest.RandString(10)) + staticRoutes := []upcloud.StaticRoute{{Nexthop: "10.0.0.100", Route: "0.0.0.0/0"}} + updateStaticRoutes := []upcloud.StaticRoute{{Name: "my-example-route-2", Nexthop: "10.0.0.101", Route: "0.0.0.0/0"}} + resource.Test(t, resource.TestCase{ PreCheck: func() { testAccPreCheck(t) }, ProviderFactories: testAccProviderFactories(&providers), CheckDestroy: testAccCheckRouterDestroy, Steps: []resource.TestStep{ { - Config: testAccRouterConfig(name), + Config: testAccRouterConfig(name, staticRoutes), Check: resource.ComposeTestCheckFunc( testAccCheckRouterExists("upcloud_router.my_example_router", &router), testAccCheckUpCloudRouterAttributes(&router, name), + resource.TestCheckTypeSetElemNestedAttrs("upcloud_router.my_example_router", "static_route.*", map[string]string{ + "name": "static-route-0", + "nexthop": "10.0.0.100", + "route": "0.0.0.0/0", + }), ), }, { - Config: testAccRouterConfig(updateName), + Config: testAccRouterConfig(updateName, updateStaticRoutes), Check: resource.ComposeTestCheckFunc( testAccCheckRouterExists("upcloud_router.my_example_router", &router), testAccCheckUpCloudRouterAttributes(&router, updateName), + resource.TestCheckTypeSetElemNestedAttrs("upcloud_router.my_example_router", "static_route.*", map[string]string{ + "name": "my-example-route-2", + "nexthop": "10.0.0.101", + "route": "0.0.0.0/0", + }), ), }, }, @@ -77,7 +96,7 @@ func TestAccUpCloudRouter_import(t *testing.T) { CheckDestroy: testAccCheckRouterDestroy, Steps: []resource.TestStep{ { - Config: testAccRouterConfig(name), + Config: testAccRouterConfig(name, nil), Check: resource.ComposeTestCheckFunc( testAccCheckRouterExists("upcloud_router.my_example_router", &router), ), @@ -373,11 +392,33 @@ func testAccCheckRouterNetworkDestroy(s *terraform.State) error { return nil } -func testAccRouterConfig(name string) string { - return fmt.Sprintf(` +func testAccRouterConfig(name string, staticRoutes []upcloud.StaticRoute) string { + s := fmt.Sprintf(` resource "upcloud_router" "my_example_router" { name = "%s" -}`, name) +`, name) + + if len(staticRoutes) > 0 { + for _, staticRoute := range staticRoutes { + s = s + fmt.Sprintf(` + static_route { + nexthop = "%s" + route = "%s" +`, staticRoute.Nexthop, staticRoute.Route) + + if len(staticRoute.Name) > 0 { + s = s + fmt.Sprintf(` + name = "%s" +`, staticRoute.Name) + } + } + s = s + ` + }` + } + s = s + ` +} +` + return s } func testAccNetworkRouterAttached(network *upcloud.Network, router *upcloud.Router) resource.TestCheckFunc {