diff --git a/docs/data-sources/hvn_route.md b/docs/data-sources/hvn_route.md index 774fe3f3b..4342f9f93 100644 --- a/docs/data-sources/hvn_route.md +++ b/docs/data-sources/hvn_route.md @@ -15,8 +15,8 @@ The HVN route data source provides information about an existing HVN route. ```terraform data "hcp_hvn_route" "example" { - hvn = var.hvn - destination_cidr = var.destination_cidr + hvn_link = var.hvn_link + destination_cidr = var.hvn_route_id } ``` @@ -25,8 +25,8 @@ data "hcp_hvn_route" "example" { ### Required -- **destination_cidr** (String) The destination CIDR of the HVN route -- **hvn** (String) The `self_link` of the HashiCorp Virtual Network (HVN). +- **hvn_link** (String) The `self_link` of the HashiCorp Virtual Network (HVN). +- **hvn_route_id** (String) The ID of the HVN route. ### Optional @@ -36,6 +36,7 @@ data "hcp_hvn_route" "example" { ### Read-Only - **created_at** (String) The time that the HVN route was created. +- **destination_cidr** (String) The destination CIDR of the HVN route. - **self_link** (String) A unique URL identifying the HVN route. - **state** (String) The state of the HVN route. - **target_link** (String) A unique URL identifying the target of the HVN route. diff --git a/docs/resources/hvn_route.md b/docs/resources/hvn_route.md new file mode 100644 index 000000000..127fc8317 --- /dev/null +++ b/docs/resources/hvn_route.md @@ -0,0 +1,89 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "hcp_hvn_route Resource - terraform-provider-hcp" +subcategory: "" +description: |- + The HVN route resource allows you to manage an HVN route. +--- + +# hcp_hvn_route (Resource) + +The HVN route resource allows you to manage an HVN route. + +## Example Usage + +```terraform +provider "aws" { + region = "us-west-2" +} + +resource "hcp_hvn" "main" { + hvn_id = "main-hvn" + cloud_provider = "aws" + region = "us-west-2" + cidr_block = "172.25.16.0/20" +} + +// Creating a peering and a route for it. +resource "aws_vpc" "peer" { + cidr_block = "192.168.0.0/20" +} + +resource "hcp_aws_network_peering" "example" { + peering_id = "peer-example" + hvn_id = hcp_hvn.main.hvn_id + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = "us-west-2" +} + +resource "aws_vpc_peering_connection_accepter" "peer" { + vpc_peering_connection_id = hcp_aws_network_peering.example.provider_peering_id + auto_accept = true +} + +resource "hcp_hvn_route" "example-peering-route" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "peering-route" + destination_cidr = aws_vpc.peer.cidr_block + target_link = hcp_aws_network_peering.example.self_link +} +``` + + +## Schema + +### Required + +- **destination_cidr** (String) The destination CIDR of the HVN route. +- **hvn_link** (String) The `self_link` of the HashiCorp Virtual Network (HVN). +- **hvn_route_id** (String) The ID of the HVN route. +- **target_link** (String) A unique URL identifying the target of the HVN route. Examples of the target: [`aws_network_peering`](aws_network_peering.md), [`aws_transit_gateway_attachment`](aws_transit_gateway_attachment.md) + +### Optional + +- **id** (String) The ID of this resource. +- **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- **created_at** (String) The time that the HVN route was created. +- **self_link** (String) A unique URL identifying the HVN route. +- **state** (String) The state of the HVN route. + + +### Nested Schema for `timeouts` + +Optional: + +- **create** (String) +- **default** (String) + +## Import + +Import is supported using the following syntax: + +```shell +# The import ID is {hvn_id}:{hvn_route_id} +terraform import hcp_hvn_route.example main-hvn:example-hvn-route +``` diff --git a/examples/data-sources/hcp_hvn_route/data-source.tf b/examples/data-sources/hcp_hvn_route/data-source.tf index f2b7b5b98..35c2595d2 100644 --- a/examples/data-sources/hcp_hvn_route/data-source.tf +++ b/examples/data-sources/hcp_hvn_route/data-source.tf @@ -1,4 +1,4 @@ data "hcp_hvn_route" "example" { - hvn = var.hvn - destination_cidr = var.destination_cidr + hvn_link = var.hvn_link + destination_cidr = var.hvn_route_id } diff --git a/examples/data-sources/hcp_hvn_route/variables.tf b/examples/data-sources/hcp_hvn_route/variables.tf index 8e6fd7c40..b605f7682 100644 --- a/examples/data-sources/hcp_hvn_route/variables.tf +++ b/examples/data-sources/hcp_hvn_route/variables.tf @@ -1,9 +1,9 @@ -variable "hvn" { +variable "hvn_link" { description = "The `self_link` of the HashiCorp Virtual Network (HVN)." type = string } -variable "destination_cidr" { - description = "The destination CIDR of the HVN route." +variable "hvn_route_id" { + description = "The ID of the HVN route ID." type = string } diff --git a/examples/resources/hcp_hvn_route/import.sh b/examples/resources/hcp_hvn_route/import.sh new file mode 100644 index 000000000..4ae65424d --- /dev/null +++ b/examples/resources/hcp_hvn_route/import.sh @@ -0,0 +1,2 @@ +# The import ID is {hvn_id}:{hvn_route_id} +terraform import hcp_hvn_route.example main-hvn:example-hvn-route diff --git a/examples/resources/hcp_hvn_route/resource.tf b/examples/resources/hcp_hvn_route/resource.tf new file mode 100644 index 000000000..ac20b6523 --- /dev/null +++ b/examples/resources/hcp_hvn_route/resource.tf @@ -0,0 +1,35 @@ +provider "aws" { + region = "us-west-2" +} + +resource "hcp_hvn" "main" { + hvn_id = "main-hvn" + cloud_provider = "aws" + region = "us-west-2" + cidr_block = "172.25.16.0/20" +} + +// Creating a peering and a route for it. +resource "aws_vpc" "peer" { + cidr_block = "192.168.0.0/20" +} + +resource "hcp_aws_network_peering" "example" { + peering_id = "peer-example" + hvn_id = hcp_hvn.main.hvn_id + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = "us-west-2" +} + +resource "aws_vpc_peering_connection_accepter" "peer" { + vpc_peering_connection_id = hcp_aws_network_peering.example.provider_peering_id + auto_accept = true +} + +resource "hcp_hvn_route" "example-peering-route" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "peering-route" + destination_cidr = aws_vpc.peer.cidr_block + target_link = hcp_aws_network_peering.example.self_link +} \ No newline at end of file diff --git a/go.mod b/go.mod index ee7c88546..32530e222 100644 --- a/go.mod +++ b/go.mod @@ -10,7 +10,7 @@ require ( github.com/google/uuid v1.2.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 github.com/hashicorp/hcl/v2 v2.8.2 // indirect - github.com/hashicorp/hcp-sdk-go v0.7.0 + github.com/hashicorp/hcp-sdk-go v0.8.0 github.com/hashicorp/terraform-exec v0.13.3 // indirect github.com/hashicorp/terraform-plugin-docs v0.4.0 github.com/hashicorp/terraform-plugin-sdk/v2 v2.5.0 diff --git a/go.sum b/go.sum index 9ddf2fd76..69d38f2c2 100644 --- a/go.sum +++ b/go.sum @@ -325,8 +325,8 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8= github.com/hashicorp/hcl/v2 v2.8.2 h1:wmFle3D1vu0okesm8BTLVDyJ6/OL9DCLUwn0b2OptiY= github.com/hashicorp/hcl/v2 v2.8.2/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY= -github.com/hashicorp/hcp-sdk-go v0.7.0 h1:OtbcR/rMBlfK5BLowHIPe0HJtb0rEs8FyRAzS+xH9vI= -github.com/hashicorp/hcp-sdk-go v0.7.0/go.mod h1:M+kmFj0s4KWNA5GVOgLhNtCTu3ypTR+QjWYIMgedA5Q= +github.com/hashicorp/hcp-sdk-go v0.8.0 h1:P7mMk2h87BYJ6dk851pD3WvnuXa17hxvutA5slxCWGU= +github.com/hashicorp/hcp-sdk-go v0.8.0/go.mod h1:M+kmFj0s4KWNA5GVOgLhNtCTu3ypTR+QjWYIMgedA5Q= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= github.com/hashicorp/terraform-exec v0.12.0/go.mod h1:SGhto91bVRlgXQWcJ5znSz+29UZIa8kpBbkGwQ+g9E8= diff --git a/internal/clients/hvn_route.go b/internal/clients/hvn_route.go index 80ee082be..71a53eb10 100644 --- a/internal/clients/hvn_route.go +++ b/internal/clients/hvn_route.go @@ -2,16 +2,68 @@ package clients import ( "context" + "fmt" + "log" + "time" "github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/client/network_service" networkmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/models" sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" ) +// CreateHVNRoute creates a new HVN route +func CreateHVNRoute(ctx context.Context, client *Client, + id string, + hvn *sharedmodels.HashicorpCloudLocationLink, + destination string, + target *sharedmodels.HashicorpCloudLocationLink, + location *sharedmodels.HashicorpCloudLocationLocation) (*networkmodels.HashicorpCloudNetwork20200907CreateHVNRouteResponse, error) { + + hvnRouteParams := network_service.NewCreateHVNRouteParams() + hvnRouteParams.Context = ctx + hvnRouteParams.HvnLocationOrganizationID = location.OrganizationID + hvnRouteParams.HvnLocationProjectID = location.ProjectID + hvnRouteParams.HvnID = hvn.ID + hvnRouteParams.Body = &networkmodels.HashicorpCloudNetwork20200907CreateHVNRouteRequest{ + Destination: destination, + Hvn: hvn, + ID: id, + Target: &networkmodels.HashicorpCloudNetwork20200907HVNRouteTarget{ + HvnConnection: target, + }, + } + log.Printf("[INFO] Creating HVN route for HVN (%s) with destination CIDR %s", hvn.ID, destination) + hvnRouteResp, err := client.Network.CreateHVNRoute(hvnRouteParams, nil) + if err != nil { + return nil, fmt.Errorf("unable to create HVN route for HVN (%s) with destination CIDR %s: %v", hvn.ID, destination, err) + } + + return hvnRouteResp.Payload, nil +} + +// GetHVNRoute returns specific HVN route by its ID +func GetHVNRoute(ctx context.Context, client *Client, hvnID, routeID string, loc *sharedmodels.HashicorpCloudLocationLocation) (*networkmodels.HashicorpCloudNetwork20200907HVNRoute, error) { + getHVNRouteParams := network_service.NewGetHVNRouteParams() + getHVNRouteParams.Context = ctx + getHVNRouteParams.HvnID = hvnID + getHVNRouteParams.ID = routeID + getHVNRouteParams.HvnLocationOrganizationID = loc.OrganizationID + getHVNRouteParams.HvnLocationProjectID = loc.ProjectID + + getHVNRouteResponse, err := client.Network.GetHVNRoute(getHVNRouteParams, nil) + if err != nil { + return nil, err + } + + return getHVNRouteResponse.Payload.Route, nil +} + // ListHVNRoutes lists the routes for an HVN. func ListHVNRoutes(ctx context.Context, client *Client, hvnID string, destination string, targetID string, targetType string, loc *sharedmodels.HashicorpCloudLocationLocation) ([]*networkmodels.HashicorpCloudNetwork20200907HVNRoute, error) { + listHVNRoutesParams := network_service.NewListHVNRoutesParams() listHVNRoutesParams.Context = ctx listHVNRoutesParams.HvnID = hvnID @@ -28,3 +80,75 @@ func ListHVNRoutes(ctx context.Context, client *Client, hvnID string, return listHVNRoutesResponse.Payload.Routes, nil } + +// DeleteSnapshotByID deletes an HVN route by its ID +func DeleteHVNRouteByID(ctx context.Context, client *Client, hvnID string, + hvnRouteID string, loc *sharedmodels.HashicorpCloudLocationLocation) (*networkmodels.HashicorpCloudNetwork20200907DeleteHVNRouteResponse, error) { + + deleteHVNRouteParams := network_service.NewDeleteHVNRouteParams() + + deleteHVNRouteParams.Context = ctx + deleteHVNRouteParams.ID = hvnRouteID + deleteHVNRouteParams.HvnID = hvnID + deleteHVNRouteParams.HvnLocationOrganizationID = loc.OrganizationID + deleteHVNRouteParams.HvnLocationProjectID = loc.ProjectID + + deleteHVNRouteResponse, err := client.Network.DeleteHVNRoute(deleteHVNRouteParams, nil) + if err != nil { + return nil, err + } + + return deleteHVNRouteResponse.Payload, nil +} + +const ( + // HvnRouteStateCreating is the CREATING state of an HVN route + HvnRouteStateCreating = string(networkmodels.HashicorpCloudNetwork20200907HVNRouteStateCREATING) + + // HvnRouteStateActive is the ACTIVE state of an HVN route + HvnRouteStateActive = string(networkmodels.HashicorpCloudNetwork20200907HVNRouteStateACTIVE) + + // HvnRouteStatePending is the PENDING state of an HVN route + HvnRouteStatePending = string(networkmodels.HashicorpCloudNetwork20200907HVNRouteStatePENDING) +) + +// hvnRouteRefreshState refreshes the state of the HVN route +func hvnRouteRefreshState(ctx context.Context, client *Client, hvnID, routeID string, loc *sharedmodels.HashicorpCloudLocationLocation) resource.StateRefreshFunc { + return func() (interface{}, string, error) { + route, err := GetHVNRoute(ctx, client, hvnID, routeID, loc) + if err != nil { + return nil, "", err + } + + return route, string(route.State), nil + } +} + +// WaitForHVNRouteToBeActive will poll the GET HVN route endpoint until +// the state is ACTIVE, ctx is canceled, or an error occurs. +func WaitForHVNRouteToBeActive(ctx context.Context, client *Client, + hvnID string, + routeID string, + loc *sharedmodels.HashicorpCloudLocationLocation, + timeout time.Duration) (*networkmodels.HashicorpCloudNetwork20200907HVNRoute, error) { + + stateChangeConf := resource.StateChangeConf{ + Pending: []string{ + HvnRouteStateCreating, + HvnRouteStatePending, + }, + Target: []string{ + HvnRouteStateActive, + }, + Refresh: hvnRouteRefreshState(ctx, client, hvnID, routeID, loc), + Timeout: timeout, + PollInterval: 5 * time.Second, + } + + result, err := stateChangeConf.WaitForStateContext(ctx) + if err != nil { + return nil, fmt.Errorf("error waiting for the HVN route (%s) to become 'ACTIVE': %+v", routeID, err) + } + + return result.(*networkmodels.HashicorpCloudNetwork20200907HVNRoute), nil +} diff --git a/internal/provider/data_source_hvn_route.go b/internal/provider/data_source_hvn_route.go index 04380ec29..db6a47d74 100644 --- a/internal/provider/data_source_hvn_route.go +++ b/internal/provider/data_source_hvn_route.go @@ -7,7 +7,7 @@ import ( sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" "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" + "github.com/hashicorp/terraform-provider-hcp/internal/clients" ) @@ -20,16 +20,15 @@ func dataSourceHVNRoute() *schema.Resource { }, Schema: map[string]*schema.Schema{ // Required inputs - "hvn": { + "hvn_link": { Description: "The `self_link` of the HashiCorp Virtual Network (HVN).", Type: schema.TypeString, Required: true, }, - "destination_cidr": { - Description: "The destination CIDR of the HVN route", - Type: schema.TypeString, - Required: true, - ValidateFunc: validation.IsCIDR, + "hvn_route_id": { + Description: "The ID of the HVN route.", + Type: schema.TypeString, + Required: true, }, // Computed outputs "self_link": { @@ -37,6 +36,11 @@ func dataSourceHVNRoute() *schema.Resource { Type: schema.TypeString, Computed: true, }, + "destination_cidr": { + Description: "The destination CIDR of the HVN route.", + Type: schema.TypeString, + Computed: true, + }, "target_link": { Description: "A unique URL identifying the target of the HVN route.", Type: schema.TypeString, @@ -59,9 +63,7 @@ func dataSourceHVNRoute() *schema.Resource { func dataSourceHVNRouteRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { client := meta.(*clients.Client) - hvn := d.Get("hvn").(string) - var hvnLink *sharedmodels.HashicorpCloudLocationLink - + hvn := d.Get("hvn_link").(string) hvnLink, err := parseLinkURL(hvn, HvnResourceType) if err != nil { return diag.FromErr(err) @@ -71,31 +73,23 @@ func dataSourceHVNRouteRead(ctx context.Context, d *schema.ResourceData, meta in OrganizationID: client.Config.OrganizationID, ProjectID: client.Config.ProjectID, } - destination := d.Get("destination_cidr").(string) - log.Printf("[INFO] Reading HVN route for HVN (%s) with destination_cidr=%s ", hvn, destination) - route, err := clients.ListHVNRoutes(ctx, client, hvnLink.ID, destination, "", "", loc) + routeID := d.Get("hvn_route_id").(string) + routeLink := newLink(loc, HVNRouteResourceType, routeID) + routeUrl, err := linkURL(routeLink) if err != nil { - return diag.Errorf("unable to retrieve HVN route for HVN (%s) with destination_cidr=%s: %v", - hvn, destination, err) - } - - // ListHVNRoutes call should return 1 and only 1 HVN route. - if len(route) > 1 { - return diag.Errorf("Unexpected number of HVN routes returned for destination_cidr=%s: %d", destination, len(route)) - } - if len(route) == 0 { - return diag.Errorf("No HVN route found for destionation_cidr=%s", destination) + return diag.FromErr(err) } + d.SetId(routeUrl) - link := newLink(loc, HVNRouteResourceType, route[0].ID) - url, err := linkURL(link) + log.Printf("[INFO] Reading HVN route (%s)", routeID) + route, err := clients.GetHVNRoute(ctx, client, hvnLink.ID, routeID, loc) if err != nil { - return diag.FromErr(err) + return diag.Errorf("unable to retrieve HVN route (%s): %v", routeID, err) } - d.SetId(url) - if err := setHVNRouteResourceData(d, route[0], loc); err != nil { + // HVN route found, update resource data. + if err := setHVNRouteResourceData(d, route, loc); err != nil { return diag.FromErr(err) } diff --git a/internal/provider/link.go b/internal/provider/link.go index df1c68769..17ea3fe6b 100644 --- a/internal/provider/link.go +++ b/internal/provider/link.go @@ -102,18 +102,27 @@ func linkURL(l *sharedmodels.HashicorpCloudLocationLink) (string, error) { // parseLinkURL parses a link URL into a link. If the URL is malformed, an // error is returned. // +// If `expectedType` is provided it will be matched against the resource from +// the URL and if they don't match the function returns an error. If `expectedType` +// is an empty string then the resource type just will be inferred from the URL +// as is. +// // The resulting link location does not include an organization, which is // typically required for requests. If organization is needed, use // `buildLinkFromURL()`. -func parseLinkURL(urn string, resourceType string) (*sharedmodels.HashicorpCloudLocationLink, error) { - pattern := fmt.Sprintf("^/project/[^/]+/%s/[^/]+$", resourceType) +func parseLinkURL(urn string, expectedType string) (*sharedmodels.HashicorpCloudLocationLink, error) { + pattern := "^/project/[^/]+/[^/]+/[^/]+$" match, _ := regexp.MatchString(pattern, urn) if !match { - return nil, fmt.Errorf("url %q is not in the correct format: /project/{project_id}/%s/{id}", urn, resourceType) + return nil, fmt.Errorf("url %q is not in the correct format: /project/{project_id}/{resource_type}/{id}", urn) } components := strings.Split(urn, "/") + if expectedType != "" && expectedType != components[3] { + return nil, fmt.Errorf("url %q is not in the correct format: /project/{project_id}/%s/{id}", urn, expectedType) + } + return &sharedmodels.HashicorpCloudLocationLink{ Type: components[3], ID: components[4], diff --git a/internal/provider/link_test.go b/internal/provider/link_test.go index 134240b7f..120ef4449 100644 --- a/internal/provider/link_test.go +++ b/internal/provider/link_test.go @@ -92,6 +92,20 @@ func Test_parseLinkURL(t *testing.T) { require.Equal(t, id, l.ID) }) + t.Run("extracting type from the URL", func(t *testing.T) { + urn := fmt.Sprintf("/project/%s/%s/%s", + projID, + svcType, + id) + + l, err := parseLinkURL(urn, "") + require.NoError(t, err) + + require.Equal(t, projID, l.Location.ProjectID) + require.Equal(t, svcType, l.Type) + require.Equal(t, id, l.ID) + }) + t.Run("missing project ID", func(t *testing.T) { urn := fmt.Sprintf("/project/%s/%s/%s", "", diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d2d231b0c..ddc4c40f6 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -35,6 +35,7 @@ func New() func() *schema.Provider { "hcp_consul_cluster_root_token": resourceConsulClusterRootToken(), "hcp_consul_snapshot": resourceConsulSnapshot(), "hcp_hvn": resourceHvn(), + "hcp_hvn_route": resourceHvnRoute(), "hcp_vault_cluster": resourceVaultCluster(), "hcp_vault_cluster_admin_token": resourceVaultClusterAdminToken(), }, diff --git a/internal/provider/resource_hvn_route.go b/internal/provider/resource_hvn_route.go index 09192e82b..a4ec8028e 100644 --- a/internal/provider/resource_hvn_route.go +++ b/internal/provider/resource_hvn_route.go @@ -1,14 +1,238 @@ package provider import ( + "context" + "fmt" + "log" + "strings" "time" networkmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-network/preview/2020-09-07/models" sharedmodels "github.com/hashicorp/hcp-sdk-go/clients/cloud-shared/v1/models" + "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" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" ) var hvnRouteDefaultTimeout = time.Minute * 1 +var hvnRouteCreateTimeout = time.Minute * 35 + +func resourceHvnRoute() *schema.Resource { + return &schema.Resource{ + Description: "The HVN route resource allows you to manage an HVN route.", + + CreateContext: resourceHvnRouteCreate, + ReadContext: resourceHvnRouteRead, + DeleteContext: resourceHvnRouteDelete, + Timeouts: &schema.ResourceTimeout{ + Default: &hvnRouteDefaultTimeout, + Create: &hvnRouteCreateTimeout, + }, + Importer: &schema.ResourceImporter{ + StateContext: resourceHVNRouteImport, + }, + + Schema: map[string]*schema.Schema{ + // Required inputs + "hvn_link": { + Description: "The `self_link` of the HashiCorp Virtual Network (HVN).", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + "hvn_route_id": { + Description: "The ID of the HVN route.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSlugID, + }, + "destination_cidr": { + Description: "The destination CIDR of the HVN route.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsCIDR, + }, + "target_link": { + Description: "A unique URL identifying the target of the HVN route. Examples of the target: [`aws_network_peering`](aws_network_peering.md), [`aws_transit_gateway_attachment`](aws_transit_gateway_attachment.md)", + Type: schema.TypeString, + Required: true, + ForceNew: true, + }, + // Computed outputs + "self_link": { + Description: "A unique URL identifying the HVN route.", + Type: schema.TypeString, + Computed: true, + }, + "state": { + Description: "The state of the HVN route.", + Type: schema.TypeString, + Computed: true, + }, + "created_at": { + Description: "The time that the HVN route was created.", + Type: schema.TypeString, + Computed: true, + }, + }, + } +} + +func resourceHvnRouteCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + destination := d.Get("destination_cidr").(string) + hvnRouteID := d.Get("hvn_route_id").(string) + + hvn := d.Get("hvn_link").(string) + var hvnLink *sharedmodels.HashicorpCloudLocationLink + hvnLink, err := buildLinkFromURL(hvn, HvnResourceType, loc.OrganizationID) + if err != nil { + return diag.FromErr(err) + } + + target := d.Get("target_link").(string) + targetLink, err := parseLinkURL(target, "") + if err != nil { + return diag.Errorf("unable to parse target_link for HVN route (%s): %v", hvnRouteID, err) + } + targetLink.Location.OrganizationID = loc.OrganizationID + + // Check for an existing HVN. + retrievedHvn, err := clients.GetHvnByID(ctx, client, loc, hvnLink.ID) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + return diag.Errorf("unable to find the HVN (%s) for the HVN route", hvnLink.ID) + } + + return diag.Errorf("unable to check for presence of an existing HVN (%s): %v", hvnLink.ID, err) + } + + log.Printf("[INFO] HVN (%s) found, proceeding with HVN route create", hvnLink.ID) + + targetLink.Location.Region = retrievedHvn.Location.Region + + // Create HVN route + hvnRouteResp, err := clients.CreateHVNRoute(ctx, client, hvnRouteID, hvnLink, destination, targetLink, loc) + if err != nil { + return diag.FromErr(err) + } + hvnRoute := hvnRouteResp.Route + + // Set the globally unique id of this HVN route in the state now since it has + // been created, and from this point forward should be deletable. + link := newLink(hvnRoute.Hvn.Location, HVNRouteResourceType, hvnRoute.ID) + url, err := linkURL(link) + if err != nil { + return diag.FromErr(err) + } + d.SetId(url) + + // Wait for HVN route to be created. + if err := clients.WaitForOperation(ctx, client, "create HVN route", loc, hvnRouteResp.Operation.ID); err != nil { + return diag.Errorf("unable to create HVN route (%s): %v", hvnRouteID, err) + } + + log.Printf("[INFO] Created HVN route (%s)", hvnRouteID) + + hvnRoute, err = clients.WaitForHVNRouteToBeActive(ctx, client, hvnLink.ID, hvnRouteID, loc, d.Timeout(schema.TimeoutCreate)) + if err != nil { + return diag.FromErr(err) + } + + if err := setHVNRouteResourceData(d, hvnRoute, loc); err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceHvnRouteRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + hvn := d.Get("hvn_link").(string) + var hvnLink *sharedmodels.HashicorpCloudLocationLink + + hvnLink, err := parseLinkURL(hvn, HvnResourceType) + if err != nil { + return diag.FromErr(err) + } + + idLink, err := parseLinkURL(d.Id(), HVNRouteResourceType) + if err != nil { + return diag.FromErr(err) + } + + loc := &sharedmodels.HashicorpCloudLocationLocation{ + OrganizationID: client.Config.OrganizationID, + ProjectID: client.Config.ProjectID, + } + + log.Printf("[INFO] Reading HVN route (%s)", idLink.ID) + route, err := clients.GetHVNRoute(ctx, client, hvnLink.ID, idLink.ID, loc) + if err != nil { + if clients.IsResponseCodeNotFound(err) { + log.Printf("[WARN] HVN route (%s) not found, removing from state", idLink.ID) + d.SetId("") + return nil + } + + return diag.Errorf("unable to retrieve HVN route (%s): %v", idLink.ID, err) + } + + // The HVN route failed to provision properly so we want to let the user know and remove it from state. + if route.State == networkmodels.HashicorpCloudNetwork20200907HVNRouteStateFAILED { + log.Printf("[WARN] HVN route (%s) failed to provision, removing from state", idLink.ID) + d.SetId("") + return nil + } + + // HVN route found, update resource data. + if err := setHVNRouteResourceData(d, route, loc); err != nil { + return diag.FromErr(err) + } + return nil +} + +func resourceHvnRouteDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics { + client := meta.(*clients.Client) + + link, err := buildLinkFromURL(d.Id(), HVNRouteResourceType, client.Config.OrganizationID) + if err != nil { + return diag.FromErr(err) + } + + routeID := link.ID + loc := link.Location + + hvn := d.Get("hvn_link").(string) + hvnLink, err := buildLinkFromURL(hvn, HvnResourceType, loc.OrganizationID) + if err != nil { + return diag.FromErr(err) + } + + log.Printf("[INFO] Deleting HVN route (%s)", routeID) + resp, err := clients.DeleteHVNRouteByID(ctx, client, hvnLink.ID, routeID, loc) + if err != nil { + return diag.Errorf("unable to delete HVN route (%s): %v", routeID, err) + } + + if err := clients.WaitForOperation(ctx, client, "delete HVN route", loc, resp.Operation.ID); err != nil { + return diag.Errorf("unable to delete HVN route (%s): %v", routeID, err) + } + + log.Printf("[INFO] HVN route (%s) deleted, removing from state", routeID) + + return nil +} func setHVNRouteResourceData(d *schema.ResourceData, route *networkmodels.HashicorpCloudNetwork20200907HVNRoute, loc *sharedmodels.HashicorpCloudLocationLocation) error { @@ -31,6 +255,10 @@ func setHVNRouteResourceData(d *schema.ResourceData, route *networkmodels.Hashic return err } + if err := d.Set("hvn_route_id", route.ID); err != nil { + return err + } + if err := d.Set("target_link", targetLink); err != nil { return err } @@ -49,3 +277,38 @@ func setHVNRouteResourceData(d *schema.ResourceData, route *networkmodels.Hashic return nil } + +// resourceHVNRouteImport implements the logic necessary to import an +// un-tracked (by Terraform) HVN route resource into Terraform state. +func resourceHVNRouteImport(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { + client := meta.(*clients.Client) + + idParts := strings.SplitN(d.Id(), ":", 2) + if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" { + return nil, fmt.Errorf("unexpected format of ID (%q), expected {hvn_id}:{hvn_route_id}", d.Id()) + } + hvnID := idParts[0] + routeID := idParts[1] + loc := &sharedmodels.HashicorpCloudLocationLocation{ + ProjectID: client.Config.ProjectID, + } + + routeLink := newLink(loc, HVNRouteResourceType, routeID) + routeUrl, err := linkURL(routeLink) + if err != nil { + return nil, err + } + d.SetId(routeUrl) + + hvnLink := newLink(loc, HvnResourceType, hvnID) + hvnUrl, err := linkURL(hvnLink) + if err != nil { + return nil, err + } + + if err := d.Set("hvn_link", hvnUrl); err != nil { + return nil, err + } + + return []*schema.ResourceData{d}, nil +}