diff --git a/CHANGELOG.md b/CHANGELOG.md index 67b30c457..8818e488c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +## 0.7.0 (Unreleased) + +⚠️ Note: This version contains breaking changes to the `hcp_aws_transit_gateway_attachment` and `hcp_aws_network_peering` resources and data sources. Please pin to the previous version and follow [this migration guide](https://github.com/hashicorp/terraform-provider-hcp/pull/128) when you're ready to migrate. ⚠️ + +FEATURES: +* **New resource** `hcp_hvn_route` (#122) + +IMPROVEMENTS: +* resource/hcp_aws_transit_gateway_attachment: released as Generally Available (#121) + +BREAKING CHANGES: +* resource/hcp_aws_network_peering: now requires `peering_id` to be specified and doesn't accept `peer_vpc_cidr_block` as input (#128) +* datasource/hcp_aws_network_peering: no longer returns `peer_vpc_cidr_block` as output (#128) +* resource/hcp_aws_transit_gateway_attachment: doesn't accept `destination_cidrs` as input (#128) +* datasource/hcp_aws_transit_gateway_attachment: no longer returns `destination_cidrs` as output (#128) + ## 0.6.1 (June 03, 2021) IMPROVEMENTS: diff --git a/README.md b/README.md index be627f94c..90b77e259 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,14 @@ resource "hcp_aws_network_peering" "example" { peer_vpc_id = aws_vpc.peer.id peer_account_id = aws_vpc.peer.owner_id peer_vpc_region = "us-west-2" - peer_vpc_cidr_block = aws_vpc.peer.cidr_block +} + +// Create an HVN route that targets your HCP network peering and matches your AWS VPC's CIDR block. +resource "hcp_hvn_route" "example" { + hvn_link = hcp_hvn.hvn.self_link + hvn_route_id = "peer-route-id" + destination_cidr = aws_vpc.peer.cidr_block + target_link = hcp_aws_network_peering.example.self_link } // Accept the VPC peering within your AWS account. diff --git a/contributing/writing-tests.md b/contributing/writing-tests.md index 3c88d768d..24c62627c 100644 --- a/contributing/writing-tests.md +++ b/contributing/writing-tests.md @@ -30,6 +30,18 @@ export HCP_CLIENT_ID=... export HCP_CLIENT_SECRET=... ``` +For some tests AWS credentials are also required in order to create "customer" +resources for testing certain HCP features (network peerings and TGW attachments): + +```sh +export AWS_ACCESS_KEY_ID=... +export AWS_SECRET_ACCESS_KEY=... +export AWS_SESSION_TOKEN=... +``` + +**Note for HCP developers**: this AWS account **MUST NOT** be the same AWS account that is being used by +your HCP organization (dataplane) otherwise tests would fail. + Tests can then be run by specifying a regular expression defining the tests to run: ```sh diff --git a/docs/data-sources/aws_network_peering.md b/docs/data-sources/aws_network_peering.md index 752a8ae23..321c55852 100644 --- a/docs/data-sources/aws_network_peering.md +++ b/docs/data-sources/aws_network_peering.md @@ -38,7 +38,6 @@ data "hcp_aws_network_peering" "test" { - **expires_at** (String) The time after which the network peering will be considered expired if it hasn't transitioned into `ACCEPTED` or `ACTIVE` state. - **organization_id** (String) The ID of the HCP organization where the network peering is located. Always matches the HVN's organization. - **peer_account_id** (String) The account ID of the peer VPC in AWS. -- **peer_vpc_cidr_block** (String) The CIDR range of the peer VPC in AWS. - **peer_vpc_id** (String) The ID of the peer VPC in AWS. - **peer_vpc_region** (String) The region of the peer VPC in AWS. - **project_id** (String) The ID of the HCP project where the network peering is located. Always matches the HVN's project. diff --git a/docs/data-sources/aws_transit_gateway_attachment.md b/docs/data-sources/aws_transit_gateway_attachment.md index 171ab2200..edec66e41 100644 --- a/docs/data-sources/aws_transit_gateway_attachment.md +++ b/docs/data-sources/aws_transit_gateway_attachment.md @@ -36,7 +36,6 @@ data "hcp_aws_transit_gateway_attachment" "test" { ### Read-Only - **created_at** (String) The time that the transit gateway attachment was created. -- **destination_cidrs** (List of String) The list of associated CIDR ranges. Traffic from these CIDRs will be allowed for all resources in the HVN. Traffic to these CIDRs will be routed into this transit gateway attachment. - **expires_at** (String) The time after which the transit gateway attachment will be considered expired if it hasn't transitioned into `ACCEPTED` or `ACTIVE` state. - **organization_id** (String) The ID of the HCP organization where the transit gateway attachment is located. Always matches the HVN's organization. - **project_id** (String) The ID of the HCP project where the transit gateway attachment is located. Always matches the HVN's project. 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/guides/hvn-route-migration-guide.md b/docs/guides/hvn-route-migration-guide.md new file mode 100644 index 000000000..28491ce26 --- /dev/null +++ b/docs/guides/hvn-route-migration-guide.md @@ -0,0 +1,162 @@ +--- +subcategory: "" +page_title: "HVN Route Migration Guide - HCP Provider" +description: |- + An guide to migrating HCP networking resources to use HVN routes. +--- + +# Introducing HVN routes + +The HVN route is a new resource that belongs to an HVN. It contains a CIDR block and targets a networking connection: +either a peering or transit gateway attachment. + +HVN routes provide a general view on how an HVN's traffic is routed across all networking connections and create a flexible way of managing these routing rules. + +## Migrating existing peerings and transit gateway attachments + +There are two ways to migrate existing peerings and transit gateway attachments managed by Terraform: + + 1. Recreate Resources with Updated Schema + * This option is quicker but will result in downtime and possible data loss. Best for test environments. Will allow you to specify human-readable ids for the resources. + * Comment out all `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resources. + * Run `terraform apply` to destroy currently existing connections. + * Uncomment and update all `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resource definitions to match the new schema. + * Add corresponding `hcp_hvn_route` resources for each CIDR targeting corresponding peering connections or transit gateway attachment. + * Run `terraform apply` to recreate connections. + + 2. Re-Import with Updated Syntax: + * This option allows you to avoid downtime or data loss. + * Update any `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resource definitions to match the new schema. All values needed can be found on the details pages of Peerings and TGW attachment in the HCP Portal. + * Add corresponding `hcp_hvn_route` resources for each CIDR targeting corresponding peering connections or transit gateway attachments. + * Run `terraform import hcp_hvn_route. :` for each `hcp_hvn_route`. The `` can be found on the details pages of the corresponding HVN connection in the HCP Portal. + * Run `terraform plan` and make sure that there are no changes detected by the Terraform. + +The examples below walk through the schema upgrade and re-import steps. + +### Peering example + +Given: +```terraform +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_network_peering" "peering" { + hvn_id = hcp_hvn.hvn.hvn_id + peer_vpc_id = "vpc-845f29fc" + peer_account_id = "572816266891" + peer_vpc_region = "us-west-2" + peer_vpc_cidr_block = "172.31.0.0/16" +} +``` + +Rewrite it to the new schema and add corresponding HVN route: +```terraform +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_network_peering" "peering" { + hvn_id = hcp_hvn.hvn.hvn_id + // add `peering_id` that you can find in the HCP Portal + peering_id = "f03324a9-4377-4a54-9c15-958fd07ad77b" + peer_vpc_id = "vpc-845f29fc" + peer_account_id = "572816266891" + peer_vpc_region = "us-west-2" + // remove `peer_vpc_cidr_block` + // peer_vpc_cidr_block = "172.31.0.0/16" +} + +// Add a `hcp_hvn_route` resource for the peering's CIDR +resource "hcp_hvn_route" "peering-route" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the peering details page in the list of routes + hvn_route_id = "a8dda9a8-0f69-4fa0-b38c-55be302fdddb" + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.peering.self_link +} +``` + +Run `import` for the `hcp_hvn_route`: +```shell +$ terraform import hcp_hvn_route.peering-route prod-hvn:a8dda9a8-0f69-4fa0-b38c-55be302fdddb +``` + +Run `terraform plan` to make sure there are no changes detected by the Terraform: +```shell +$ terraform plan +No changes. Infrastructure is up-to-date. +``` + +### Transit gateway attachment example + +Given: +```terraform +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_transit_gateway_attachment" "prod" { + hvn_id = hcp_hvn.hvn.hvn_id + transit_gateway_attachment_id = "prod-tgw-attachment" + transit_gateway_id = "tgw-0ee94b1a1167cf89d" + resource_share_arn = "arn:aws:ram:us-west-2:..." + destination_cidrs = ["10.1.0.0/24", "10.2.0.0/24"] +} +``` + +Rewrite it to the new schema and add corresponding HVN route: +```terraform +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_transit_gateway_attachment" "prod" { + hvn_id = hcp_hvn.hvn.hvn_id + transit_gateway_attachment_id = "prod-tgw-attachment" + transit_gateway_id = "tgw-0ee94b1a1167cf89d" + resource_share_arn = "arn:aws:ram:us-west-2:..." + // remove `destination_cidrs` + // destination_cidrs = ["10.1.0.0/24", "10.2.0.0/24"] +} + +// add a new `hcp_hvn_route` for each CIDR associated with the transit gateway attachment +resource "hcp_hvn_route" "tgw-route-1" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the TGW attachment details page in the list of Routes + hvn_route_id = "35392425-215a-44ec-bbd0-051bb777ce5f" + destination_cidr = "10.1.0.0/24" + target_link = hcp_aws_transit_gateway_attachment.prod.self_link +} + +resource "hcp_hvn_route" "tgw-route-2" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the transit gateway attachment details page in the list of routes + hvn_route_id = "9867959a-d81b-4e52-ae8e-ca56f9dd06fc" + destination_cidr = "10.2.0.0/24" + target_link = hcp_aws_transit_gateway_attachment.prod.self_link +} +``` + +Run `import` for each `hcp_hvn_route` you've added: +```shell +$ terraform import hcp_hvn_route.tgw-route-1 prod-hvn:35392425-215a-44ec-bbd0-051bb777ce5f +... + +$ terraform import hcp_hvn_route.tgw-route-2 prod-hvn:9867959a-d81b-4e52-ae8e-ca56f9dd06fc +... +``` + +Run `terraform plan` to make sure there are no changes detected by the Terraform: +```shell +$ terraform plan +No changes. Infrastructure is up-to-date. +``` \ No newline at end of file diff --git a/docs/guides/peering.md b/docs/guides/peering.md index 9792f0b0d..9f12b4b56 100644 --- a/docs/guides/peering.md +++ b/docs/guides/peering.md @@ -41,12 +41,19 @@ resource "aws_vpc" "peer" { // Create an HCP network peering to peer your HVN with your AWS VPC. resource "hcp_aws_network_peering" "example" { - peering_id = var.peer_id - hvn_id = hcp_hvn.example.hvn_id - peer_vpc_id = aws_vpc.peer.id - peer_account_id = aws_vpc.peer.owner_id - peer_vpc_region = var.region - peer_vpc_cidr_block = aws_vpc.peer.cidr_block + peering_id = var.peer_id + hvn_id = hcp_hvn.example.hvn_id + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = var.region +} + +// Create an HVN route that targets your HCP network peering and matches your AWS VPC's CIDR block +resource "hcp_hvn_route" "example" { + hvn_link = hcp_hvn.hvn.self_link + hvn_route_id = var.route_id + destination_cidr = aws_vpc.peer.cidr_block + target_link = hcp_aws_network_peering.example.self_link } // Accept the VPC peering within your AWS account. diff --git a/docs/index.md b/docs/index.md index 1818b7b59..1d3b5ea03 100644 --- a/docs/index.md +++ b/docs/index.md @@ -9,8 +9,8 @@ description: |- The HCP provider provides resources to manage [HashiCorp Cloud Platform](https://cloud.hashicorp.com/) (HCP) resources. -~> **Upcoming Migration:** The upcoming release of HVN Routes will include breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This PR](https://github.com/hashicorp/terraform-provider-hcp/pull/128) contains a migration guide. -Please pin to the current version to avoid disruption until you are ready to migrate. +~> **Upcoming Migration:** The upcoming release of HVN Routes in v0.7.0 will include breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This guide](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/hvn-route-migration-guide) walks through how to migrate to the new resource syntax. +Please pin to the previous version to avoid disruption until you are ready to migrate. -> **Note:** Please refer to the provider's [Release Notes](https://github.com/hashicorp/terraform-provider-hcp/releases) for critical fixes. @@ -37,7 +37,7 @@ terraform { required_providers { hcp = { source = "hashicorp/hcp" - version = "~> 0.6.0" + version = "~> 0.7.0" } } } @@ -73,12 +73,20 @@ resource "aws_vpc_peering_connection_accepter" "main" { } // Create a network peering between the HVN and the AWS VPC -resource "hcp_aws_network_peering" "example_peering" { - hvn_id = hcp_hvn.example_hvn.hvn_id - peer_vpc_id = aws_vpc.main.id - peer_account_id = aws_vpc.main.owner_id - peer_vpc_region = data.aws_arn.main.region - peer_vpc_cidr_block = aws_vpc.main.cidr_block +resource "hcp_aws_network_peering" "example" { + hvn_id = hcp_hvn.example_hvn.hvn_id + peering_id = "hcp-tf-example-peering" + peer_vpc_id = aws_vpc.main.id + peer_account_id = aws_vpc.main.owner_id + peer_vpc_region = data.aws_arn.main.region +} + +// Create an HVN route that targets your HCP network peering and matches your AWS VPC's CIDR block +resource "hcp_hvn_route" "example" { + hvn_link = hcp_hvn.hvn.self_link + hvn_route_id = "hcp-tf-example-hvn-route" + destination_cidr = aws_vpc.main.cidr_block + target_link = hcp_aws_network_peering.example.self_link } // Create a Consul cluster in the same region and cloud provider as the HVN diff --git a/docs/resources/aws_network_peering.md b/docs/resources/aws_network_peering.md index f0f224103..4056156ec 100644 --- a/docs/resources/aws_network_peering.md +++ b/docs/resources/aws_network_peering.md @@ -32,16 +32,23 @@ data "aws_arn" "peer" { arn = aws_vpc.peer.arn } -resource "hcp_aws_network_peering" "peer" { - 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 = data.aws_arn.peer.region - peer_vpc_cidr_block = aws_vpc.peer.cidr_block +resource "hcp_aws_network_peering" "dev" { + hvn_id = hcp_hvn.main.hvn_id + peering_id = "dev" + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = data.aws_arn.peer.region +} + +resource "hcp_hvn_route" "main-to-dev" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "main-to-dev" + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.dev.self_link } resource "aws_vpc_peering_connection_accepter" "peer" { - vpc_peering_connection_id = hcp_aws_network_peering.peer.provider_peering_id + vpc_peering_connection_id = hcp_aws_network_peering.dev.provider_peering_id auto_accept = true } ``` @@ -53,14 +60,13 @@ resource "aws_vpc_peering_connection_accepter" "peer" { - **hvn_id** (String) The ID of the HashiCorp Virtual Network (HVN). - **peer_account_id** (String) The account ID of the peer VPC in AWS. -- **peer_vpc_cidr_block** (String) The CIDR range of the peer VPC in AWS. - **peer_vpc_id** (String) The ID of the peer VPC in AWS. - **peer_vpc_region** (String) The region of the peer VPC in AWS. +- **peering_id** (String) The ID of the network peering. ### Optional - **id** (String) The ID of this resource. -- **peering_id** (String) The ID of the network peering. - **timeouts** (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) ### Read-Only diff --git a/docs/resources/aws_transit_gateway_attachment.md b/docs/resources/aws_transit_gateway_attachment.md index e529b05c1..5847aeb42 100644 --- a/docs/resources/aws_transit_gateway_attachment.md +++ b/docs/resources/aws_transit_gateway_attachment.md @@ -58,7 +58,13 @@ resource "hcp_aws_transit_gateway_attachment" "example" { transit_gateway_attachment_id = "example-tgw-attachment" transit_gateway_id = aws_ec2_transit_gateway.example.id resource_share_arn = aws_ram_resource_share.example.arn - destination_cidrs = [aws_vpc.example.cidr_block] +} + +resource "hcp_hvn_route" "route" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "hvn-to-tgw-attachment" + destination_cidr = aws_vpc.example.cidr_block + target_link = hcp_aws_transit_gateway_attachment.example.self_link } resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "example" { @@ -73,7 +79,6 @@ resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "example" { ### Required -- **destination_cidrs** (List of String) The list of associated CIDR ranges. Traffic from these CIDRs will be allowed for all resources in the HVN. Traffic to these CIDRs will be routed into this transit gateway attachment. - **hvn_id** (String) The ID of the HashiCorp Virtual Network (HVN). - **resource_share_arn** (String, Sensitive) The Amazon Resource Name (ARN) of the Resource Share that is needed to grant HCP access to the transit gateway in AWS. The Resource Share should be associated with the HCP AWS account principal (see [aws_ram_principal_association](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ram_principal_association)) and the transit gateway resource (see [aws_ram_resource_association](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/resources/ram_resource_association)) - **transit_gateway_attachment_id** (String) The user-settable name of the transit gateway attachment in HCP. diff --git a/docs/resources/hvn_route.md b/docs/resources/hvn_route.md new file mode 100644 index 000000000..49311f565 --- /dev/null +++ b/docs/resources/hvn_route.md @@ -0,0 +1,92 @@ +--- +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) + +~> **Migration Required:** The release of HVN Routes in v0.7.0 includes breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This guide](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/hvn-route-migration-guide) walks through how to migrate to the new resource syntax. +Please pin to the previous version to avoid disruption until you are ready to migrate. + +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) +- **delete** (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/guides/hvn_route_migration_guide/after-peering.tf b/examples/guides/hvn_route_migration_guide/after-peering.tf new file mode 100644 index 000000000..26ff2b0dc --- /dev/null +++ b/examples/guides/hvn_route_migration_guide/after-peering.tf @@ -0,0 +1,25 @@ +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_network_peering" "peering" { + hvn_id = hcp_hvn.hvn.hvn_id + // add `peering_id` that you can find in the HCP Portal + peering_id = "f03324a9-4377-4a54-9c15-958fd07ad77b" + peer_vpc_id = "vpc-845f29fc" + peer_account_id = "572816266891" + peer_vpc_region = "us-west-2" + // remove `peer_vpc_cidr_block` + // peer_vpc_cidr_block = "172.31.0.0/16" +} + +// Add a `hcp_hvn_route` resource for the peering's CIDR +resource "hcp_hvn_route" "peering-route" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the peering details page in the list of routes + hvn_route_id = "a8dda9a8-0f69-4fa0-b38c-55be302fdddb" + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.peering.self_link +} \ No newline at end of file diff --git a/examples/guides/hvn_route_migration_guide/after-tgw.tf b/examples/guides/hvn_route_migration_guide/after-tgw.tf new file mode 100644 index 000000000..85e63710a --- /dev/null +++ b/examples/guides/hvn_route_migration_guide/after-tgw.tf @@ -0,0 +1,31 @@ +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_transit_gateway_attachment" "prod" { + hvn_id = hcp_hvn.hvn.hvn_id + transit_gateway_attachment_id = "prod-tgw-attachment" + transit_gateway_id = "tgw-0ee94b1a1167cf89d" + resource_share_arn = "arn:aws:ram:us-west-2:..." + // remove `destination_cidrs` + // destination_cidrs = ["10.1.0.0/24", "10.2.0.0/24"] +} + +// add a new `hcp_hvn_route` for each CIDR associated with the transit gateway attachment +resource "hcp_hvn_route" "tgw-route-1" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the TGW attachment details page in the list of Routes + hvn_route_id = "35392425-215a-44ec-bbd0-051bb777ce5f" + destination_cidr = "10.1.0.0/24" + target_link = hcp_aws_transit_gateway_attachment.prod.self_link +} + +resource "hcp_hvn_route" "tgw-route-2" { + hvn_link = hcp_hvn.hvn.self_link + // you can find this ID in the HCP Portal in the transit gateway attachment details page in the list of routes + hvn_route_id = "9867959a-d81b-4e52-ae8e-ca56f9dd06fc" + destination_cidr = "10.2.0.0/24" + target_link = hcp_aws_transit_gateway_attachment.prod.self_link +} \ No newline at end of file diff --git a/examples/guides/hvn_route_migration_guide/before-peering.tf b/examples/guides/hvn_route_migration_guide/before-peering.tf new file mode 100644 index 000000000..3533658d7 --- /dev/null +++ b/examples/guides/hvn_route_migration_guide/before-peering.tf @@ -0,0 +1,13 @@ +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_network_peering" "peering" { + hvn_id = hcp_hvn.hvn.hvn_id + peer_vpc_id = "vpc-845f29fc" + peer_account_id = "572816266891" + peer_vpc_region = "us-west-2" + peer_vpc_cidr_block = "172.31.0.0/16" +} \ No newline at end of file diff --git a/examples/guides/hvn_route_migration_guide/before-tgw.tf b/examples/guides/hvn_route_migration_guide/before-tgw.tf new file mode 100644 index 000000000..ce78bfec4 --- /dev/null +++ b/examples/guides/hvn_route_migration_guide/before-tgw.tf @@ -0,0 +1,13 @@ +resource "hcp_hvn" "hvn" { + hvn_id = "prod-hvn" + region = "us-west-2" + cloud_provider = "aws" +} + +resource "hcp_aws_transit_gateway_attachment" "prod" { + hvn_id = hcp_hvn.hvn.hvn_id + transit_gateway_attachment_id = "prod-tgw-attachment" + transit_gateway_id = "tgw-0ee94b1a1167cf89d" + resource_share_arn = "arn:aws:ram:us-west-2:..." + destination_cidrs = ["10.1.0.0/24", "10.2.0.0/24"] +} \ No newline at end of file diff --git a/examples/guides/peering/main.tf b/examples/guides/peering/main.tf index f66e43952..4fffd4b0c 100644 --- a/examples/guides/peering/main.tf +++ b/examples/guides/peering/main.tf @@ -23,12 +23,19 @@ resource "aws_vpc" "peer" { // Create an HCP network peering to peer your HVN with your AWS VPC. resource "hcp_aws_network_peering" "example" { - peering_id = var.peer_id - hvn_id = hcp_hvn.example.hvn_id - peer_vpc_id = aws_vpc.peer.id - peer_account_id = aws_vpc.peer.owner_id - peer_vpc_region = var.region - peer_vpc_cidr_block = aws_vpc.peer.cidr_block + peering_id = var.peer_id + hvn_id = hcp_hvn.example.hvn_id + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = var.region +} + +// Create an HVN route that targets your HCP network peering and matches your AWS VPC's CIDR block +resource "hcp_hvn_route" "example" { + hvn_link = hcp_hvn.hvn.self_link + hvn_route_id = var.route_id + destination_cidr = aws_vpc.peer.cidr_block + target_link = hcp_aws_network_peering.example.self_link } // Accept the VPC peering within your AWS account. diff --git a/examples/guides/peering/variables.tf b/examples/guides/peering/variables.tf index 749ce4af2..0a7decf4b 100644 --- a/examples/guides/peering/variables.tf +++ b/examples/guides/peering/variables.tf @@ -17,3 +17,8 @@ variable "peer_id" { description = "The ID to use for the HCP network peering." type = string } + +variable "route_id" { + description = "The ID to use for the HCP HVN route." + type = string +} diff --git a/examples/guides/quick_start/_config.tf b/examples/guides/quick_start/_config.tf deleted file mode 100644 index 041a8b52b..000000000 --- a/examples/guides/quick_start/_config.tf +++ /dev/null @@ -1 +0,0 @@ -provider "hcp" {} \ No newline at end of file diff --git a/examples/guides/quick_start/main.tf b/examples/guides/quick_start/main.tf deleted file mode 100644 index c22148490..000000000 --- a/examples/guides/quick_start/main.tf +++ /dev/null @@ -1,20 +0,0 @@ -resource "hcp_hvn" "example_hvn" { - hvn_id = "hcp-tf-example-hvn" - cloud_provider = "aws" - region = "us-west-2" - cidr_block = "172.25.16.0/20" -} - -resource "hcp_consul_cluster" "example_consul_cluster" { - hvn_id = hcp_hvn.example_hvn.hvn_id - cluster_id = "hcp-tf-example-consul-cluster" - tier = "development" -} - -resource "hcp_aws_network_peering" "example_peering" { - hvn_id = hcp_hvn.example_hvn.hvn_id - peer_vpc_id = "vpc-2f09a348" - peer_account_id = "1234567890" - peer_vpc_region = "us-west-2" - peer_vpc_cidr_block = "10.0.1.0/24" -} \ No newline at end of file diff --git a/examples/provider/provider.tf b/examples/provider/provider.tf index 71093bf3c..3300a0ec8 100644 --- a/examples/provider/provider.tf +++ b/examples/provider/provider.tf @@ -3,7 +3,7 @@ terraform { required_providers { hcp = { source = "hashicorp/hcp" - version = "~> 0.6.0" + version = "~> 0.7.0" } } } @@ -39,12 +39,20 @@ resource "aws_vpc_peering_connection_accepter" "main" { } // Create a network peering between the HVN and the AWS VPC -resource "hcp_aws_network_peering" "example_peering" { - hvn_id = hcp_hvn.example_hvn.hvn_id - peer_vpc_id = aws_vpc.main.id - peer_account_id = aws_vpc.main.owner_id - peer_vpc_region = data.aws_arn.main.region - peer_vpc_cidr_block = aws_vpc.main.cidr_block +resource "hcp_aws_network_peering" "example" { + hvn_id = hcp_hvn.example_hvn.hvn_id + peering_id = "hcp-tf-example-peering" + peer_vpc_id = aws_vpc.main.id + peer_account_id = aws_vpc.main.owner_id + peer_vpc_region = data.aws_arn.main.region +} + +// Create an HVN route that targets your HCP network peering and matches your AWS VPC's CIDR block +resource "hcp_hvn_route" "example" { + hvn_link = hcp_hvn.hvn.self_link + hvn_route_id = "hcp-tf-example-hvn-route" + destination_cidr = aws_vpc.main.cidr_block + target_link = hcp_aws_network_peering.example.self_link } // Create a Consul cluster in the same region and cloud provider as the HVN diff --git a/examples/resources/hcp_aws_network_peering/resource.tf b/examples/resources/hcp_aws_network_peering/resource.tf index f67f906d9..6ff52308b 100644 --- a/examples/resources/hcp_aws_network_peering/resource.tf +++ b/examples/resources/hcp_aws_network_peering/resource.tf @@ -17,15 +17,22 @@ data "aws_arn" "peer" { arn = aws_vpc.peer.arn } -resource "hcp_aws_network_peering" "peer" { - 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 = data.aws_arn.peer.region - peer_vpc_cidr_block = aws_vpc.peer.cidr_block +resource "hcp_aws_network_peering" "dev" { + hvn_id = hcp_hvn.main.hvn_id + peering_id = "dev" + peer_vpc_id = aws_vpc.peer.id + peer_account_id = aws_vpc.peer.owner_id + peer_vpc_region = data.aws_arn.peer.region +} + +resource "hcp_hvn_route" "main-to-dev" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "main-to-dev" + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.dev.self_link } resource "aws_vpc_peering_connection_accepter" "peer" { - vpc_peering_connection_id = hcp_aws_network_peering.peer.provider_peering_id + vpc_peering_connection_id = hcp_aws_network_peering.dev.provider_peering_id auto_accept = true } diff --git a/examples/resources/hcp_aws_transit_gateway_attachment/resource.tf b/examples/resources/hcp_aws_transit_gateway_attachment/resource.tf index a3226e8a1..64e152846 100644 --- a/examples/resources/hcp_aws_transit_gateway_attachment/resource.tf +++ b/examples/resources/hcp_aws_transit_gateway_attachment/resource.tf @@ -44,7 +44,13 @@ resource "hcp_aws_transit_gateway_attachment" "example" { transit_gateway_attachment_id = "example-tgw-attachment" transit_gateway_id = aws_ec2_transit_gateway.example.id resource_share_arn = aws_ram_resource_share.example.arn - destination_cidrs = [aws_vpc.example.cidr_block] +} + +resource "hcp_hvn_route" "route" { + hvn_link = hcp_hvn.main.self_link + hvn_route_id = "hvn-to-tgw-attachment" + destination_cidr = aws_vpc.example.cidr_block + target_link = hcp_aws_transit_gateway_attachment.example.self_link } resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "example" { 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_aws_network_peering.go b/internal/provider/data_source_aws_network_peering.go index cb91a35c6..9e3f75509 100644 --- a/internal/provider/data_source_aws_network_peering.go +++ b/internal/provider/data_source_aws_network_peering.go @@ -57,11 +57,6 @@ func dataSourceAwsNetworkPeering() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "peer_vpc_cidr_block": { - Description: "The CIDR range of the peer VPC in AWS.", - Type: schema.TypeString, - Computed: true, - }, "provider_peering_id": { Description: "The peering connection ID used by AWS.", Type: schema.TypeString, diff --git a/internal/provider/data_source_aws_transit_gateway_attachment.go b/internal/provider/data_source_aws_transit_gateway_attachment.go index 9605de24c..3414fc060 100644 --- a/internal/provider/data_source_aws_transit_gateway_attachment.go +++ b/internal/provider/data_source_aws_transit_gateway_attachment.go @@ -55,14 +55,6 @@ func dataSourceAwsTransitGatewayAttachment() *schema.Resource { Type: schema.TypeString, Computed: true, }, - "destination_cidrs": { - Description: "The list of associated CIDR ranges. Traffic from these CIDRs will be allowed for all resources in the HVN. Traffic to these CIDRs will be routed into this transit gateway attachment.", - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - }, - Computed: true, - }, "provider_transit_gateway_attachment_id": { Description: "The transit gateway attachment ID used by AWS.", Type: schema.TypeString, 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/provider_test.go b/internal/provider/provider_test.go index 15db7e981..ae3fe28c6 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -54,7 +54,7 @@ func TestProvider(t *testing.T) { // // These verifications and configuration are preferred at this level to prevent // provider developers from experiencing less clear errors for every test. -func testAccPreCheck(t *testing.T) { +func testAccPreCheck(t *testing.T, requireAWSCreds bool) { // Since we are outside the scope of the Terraform configuration we must // call Configure() to properly initialize the provider configuration. testAccProviderConfigure.Do(func() { @@ -66,6 +66,20 @@ func testAccPreCheck(t *testing.T) { t.Fatal("HCP_CLIENT_SECRET must be set for acceptance tests") } + if requireAWSCreds { + if os.Getenv("AWS_ACCESS_KEY_ID") == "" { + t.Fatal("AWS_ACCESS_KEY_ID must be set for acceptance tests") + } + + if os.Getenv("AWS_SECRET_ACCESS_KEY") == "" { + t.Fatal("AWS_SECRET_ACCESS_KEY must be set for acceptance tests") + } + + if os.Getenv("AWS_SESSION_TOKEN") == "" { + t.Fatal("AWS_SESSION_TOKEN must be set for acceptance tests") + } + } + err := testAccProvider.Configure(context.Background(), terraform.NewResourceConfigRaw(nil)) if err != nil { t.Fatal(err) diff --git a/internal/provider/resource_aws_network_peering.go b/internal/provider/resource_aws_network_peering.go index e5b96ac56..2b955510c 100644 --- a/internal/provider/resource_aws_network_peering.go +++ b/internal/provider/resource_aws_network_peering.go @@ -12,7 +12,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" ) @@ -45,6 +45,13 @@ func resourceAwsNetworkPeering() *schema.Resource { ForceNew: true, ValidateDiagFunc: validateSlugID, }, + "peering_id": { + Description: "The ID of the network peering.", + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateDiagFunc: validateSlugID, + }, "peer_account_id": { Description: "The account ID of the peer VPC in AWS.", Type: schema.TypeString, @@ -66,22 +73,6 @@ func resourceAwsNetworkPeering() *schema.Resource { return strings.ToLower(old) == strings.ToLower(new) }, }, - "peer_vpc_cidr_block": { - Description: "The CIDR range of the peer VPC in AWS.", - Type: schema.TypeString, - Required: true, - ForceNew: true, - ValidateFunc: validation.IsCIDR, - }, - // Optional inputs - "peering_id": { - Description: "The ID of the network peering.", - Type: schema.TypeString, - Optional: true, - ForceNew: true, - Computed: true, - ValidateDiagFunc: validateSlugID, - }, // Computed outputs "organization_id": { Description: "The ID of the HCP organization where the network peering is located. Always matches the HVN's organization.", @@ -125,7 +116,6 @@ func resourceAwsNetworkPeeringCreate(ctx context.Context, d *schema.ResourceData peerAccountID := d.Get("peer_account_id").(string) peerVpcID := d.Get("peer_vpc_id").(string) peerVpcRegion := d.Get("peer_vpc_region").(string) - peerVpcCidr := d.Get("peer_vpc_cidr_block").(string) loc := &sharedmodels.HashicorpCloudLocationLocation{ OrganizationID: client.Config.OrganizationID, @@ -174,7 +164,6 @@ func resourceAwsNetworkPeeringCreate(ctx context.Context, d *schema.ResourceData AccountID: peerAccountID, VpcID: peerVpcID, Region: peerVpcRegion, - Cidr: peerVpcCidr, }, }, }, @@ -316,9 +305,6 @@ func setPeeringResourceData(d *schema.ResourceData, peering *networkmodels.Hashi if err := d.Set("peer_vpc_region", peering.Target.AwsTarget.Region); err != nil { return err } - if err := d.Set("peer_vpc_cidr_block", peering.Target.AwsTarget.Cidr); err != nil { - return err - } if err := d.Set("organization_id", peering.Hvn.Location.OrganizationID); err != nil { return err } diff --git a/internal/provider/resource_aws_network_peering_test.go b/internal/provider/resource_aws_network_peering_test.go new file mode 100644 index 000000000..016bc09c8 --- /dev/null +++ b/internal/provider/resource_aws_network_peering_test.go @@ -0,0 +1,201 @@ +package provider + +import ( + "context" + "fmt" + "math/rand" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +var ( + // using unique names for AWS resource to make debugging easier + hvnPeeringUniqueAWSName = fmt.Sprintf("hcp-tf-provider-test-%d", rand.Intn(99999)) + testAccHvnPeeringConfig = fmt.Sprintf(` +provider "aws" { + region = "us-west-2" +} + +resource "hcp_hvn" "test" { + hvn_id = "test-hvn" + cloud_provider = "aws" + region = "us-west-2" +} + +resource "aws_vpc" "vpc" { + cidr_block = "10.220.0.0/16" + tags = { + Name = "%[1]s" + } +} + +resource "hcp_aws_network_peering" "peering" { + peering_id = "test-peering" + hvn_id = hcp_hvn.test.hvn_id + peer_account_id = aws_vpc.vpc.owner_id + peer_vpc_id = aws_vpc.vpc.id + peer_vpc_region = "us-west-2" +} + +resource "hcp_hvn_route" "route" { + hvn_route_id = "peering-route" + hvn_link = hcp_hvn.test.self_link + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.peering.self_link +} + +resource "aws_vpc_peering_connection_accepter" "peering-accepter" { + vpc_peering_connection_id = hcp_aws_network_peering.peering.provider_peering_id + auto_accept = true + tags = { + Name = "%[1]s" + + // we need to have these tags here because peering-accepter will turn into + // an actual peering which HCP will populate with a set of tags (the ones below). + // After succesfull "apply"" test will try to run "plan" operation + // to make sure there are no changes to the state and if we don't specify these + // tags here then it will fail. + hvn_id = hcp_hvn.test.hvn_id + organization_id = hcp_hvn.test.organization_id + project_id = hcp_hvn.test.project_id + peering_id = hcp_aws_network_peering.peering.peering_id + } +} +`, hvnRouteUniqueAWSName) +) + +func TestAccHvnPeering(t *testing.T) { + resourceName := "hcp_aws_network_peering.peering" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, true) }, + ProviderFactories: providerFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": {VersionConstraint: "~> 2.64.0"}, + }, + CheckDestroy: testAccCheckHvnPeeringDestroy, + + Steps: []resource.TestStep{ + // Testing that initial Apply created correct HVN route + { + Config: testConfig(testAccHvnPeeringConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckHvnPeeringExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "peering_id", "test-peering"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttrSet(resourceName, "peer_account_id"), + resource.TestCheckResourceAttrSet(resourceName, "peer_vpc_id"), + resource.TestCheckResourceAttrSet(resourceName, "peer_vpc_region"), + resource.TestCheckResourceAttrSet(resourceName, "provider_peering_id"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "expires_at"), + testLink(resourceName, "self_link", "test-peering", PeeringResourceType, "hcp_hvn.test"), + ), + }, + // Testing that we can import HVN route created in the previous step and that the + // resource terraform state will be exactly the same + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: "test-hvn:test-peering", + ImportStateVerify: true, + }, + // Testing running Terraform Apply for already known resource + { + Config: testConfig(testAccHvnPeeringConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckHvnPeeringExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "peering_id", "test-peering"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttrSet(resourceName, "peer_account_id"), + resource.TestCheckResourceAttrSet(resourceName, "peer_vpc_id"), + resource.TestCheckResourceAttrSet(resourceName, "peer_vpc_region"), + resource.TestCheckResourceAttrSet(resourceName, "provider_peering_id"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "expires_at"), + testLink(resourceName, "self_link", "test-peering", PeeringResourceType, "hcp_hvn.test"), + ), + }, + }, + }) +} + +func testAccCheckHvnPeeringExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + client := testAccProvider.Meta().(*clients.Client) + + peeringLink, err := buildLinkFromURL(id, PeeringResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build peeringLink for %q: %v", id, err) + } + + hvnID, ok := rs.Primary.Attributes["hvn_id"] + if !ok { + return fmt.Errorf("no hvn_id is set") + } + + peeringID := peeringLink.ID + loc := peeringLink.Location + + if _, err := clients.GetPeeringByID(context.Background(), client, peeringID, hvnID, loc); err != nil { + return fmt.Errorf("unable to get TGW attachment %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckHvnPeeringDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clients.Client) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "hcp_aws_network_peering": + id := rs.Primary.ID + + if id == "" { + return fmt.Errorf("no ID is set") + } + + peeringLink, err := buildLinkFromURL(id, PeeringResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build peeringLink for %q: %v", id, err) + } + + hvnID, ok := rs.Primary.Attributes["hvn_id"] + if !ok { + return fmt.Errorf("no hvn_id is set") + } + + peeringID := peeringLink.ID + loc := peeringLink.Location + + _, err = clients.GetPeeringByID(context.Background(), client, peeringID, hvnID, loc) + if err == nil || !clients.IsResponseCodeNotFound(err) { + return fmt.Errorf("didn't get a 404 when reading destroyed HVN %q: %v", id, err) + } + + default: + continue + } + } + return nil +} diff --git a/internal/provider/resource_aws_transit_gateway_attachment.go b/internal/provider/resource_aws_transit_gateway_attachment.go index 4afee43b5..dd57e7e44 100644 --- a/internal/provider/resource_aws_transit_gateway_attachment.go +++ b/internal/provider/resource_aws_transit_gateway_attachment.go @@ -12,7 +12,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" ) @@ -65,17 +65,6 @@ func resourceAwsTransitGatewayAttachment() *schema.Resource { Sensitive: true, ForceNew: true, }, - "destination_cidrs": { - Description: "The list of associated CIDR ranges. Traffic from these CIDRs will be allowed for all resources in the HVN. Traffic to these CIDRs will be routed into this transit gateway attachment.", - Type: schema.TypeList, - Elem: &schema.Schema{ - Type: schema.TypeString, - ValidateFunc: validation.IsCIDR, - }, - Required: true, - MinItems: 1, - ForceNew: true, - }, // Computed outputs "organization_id": { Description: "The ID of the HCP organization where the transit gateway attachment is located. Always matches the HVN's organization.", @@ -123,16 +112,6 @@ func resourceAwsTransitGatewayAttachmentCreate(ctx context.Context, d *schema.Re tgwAttachmentID := d.Get("transit_gateway_attachment_id").(string) tgwID := d.Get("transit_gateway_id").(string) resourceShareARN := d.Get("resource_share_arn").(string) - rawCIDRs := d.Get("destination_cidrs").([]interface{}) - - destinationCIDRs := make([]string, len(rawCIDRs)) - for i, cidr := range rawCIDRs { - strCidr, ok := cidr.(string) - if !ok { - return diag.Errorf("unable to convert cidr: %v to string", cidr) - } - destinationCIDRs[i] = strCidr - } loc := &sharedmodels.HashicorpCloudLocationLocation{ OrganizationID: client.Config.OrganizationID, @@ -169,7 +148,6 @@ func resourceAwsTransitGatewayAttachmentCreate(ctx context.Context, d *schema.Re createTGWAttachmentParams.HvnLocationOrganizationID = loc.OrganizationID createTGWAttachmentParams.HvnLocationProjectID = loc.ProjectID createTGWAttachmentParams.Body = &networkmodels.HashicorpCloudNetwork20200907CreateTGWAttachmentRequest{ - Cidrs: destinationCIDRs, Hvn: &sharedmodels.HashicorpCloudLocationLink{ ID: hvnID, Location: loc, @@ -310,9 +288,6 @@ func setTransitGatewayAttachmentResourceData(d *schema.ResourceData, tgwAtt *net if err := d.Set("transit_gateway_id", tgwAtt.ProviderData.AwsData.TgwID); err != nil { return err } - if err := d.Set("destination_cidrs", tgwAtt.Cidrs); err != nil { - return err - } if err := d.Set("organization_id", tgwAtt.Location.OrganizationID); err != nil { return err } @@ -350,12 +325,13 @@ func setTransitGatewayAttachmentResourceData(d *schema.ResourceData, tgwAtt *net func resourceAwsTransitGatewayAttachmentImport(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}:{transit_gateway_attachment_id}", d.Id()) + idParts := strings.SplitN(d.Id(), ":", 3) + if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" { + return nil, fmt.Errorf("unexpected format of ID (%q), expected {hvn_id}:{transit_gateway_attachment_id}:{resource_share_arn}", d.Id()) } hvnID := idParts[0] tgwAttID := idParts[1] + resourceShareArn := idParts[2] loc := &sharedmodels.HashicorpCloudLocationLocation{ ProjectID: client.Config.ProjectID, } @@ -370,6 +346,9 @@ func resourceAwsTransitGatewayAttachmentImport(ctx context.Context, d *schema.Re if err := d.Set("hvn_id", hvnID); err != nil { return nil, err } + if err := d.Set("resource_share_arn", resourceShareArn); err != nil { + return nil, err + } return []*schema.ResourceData{d}, nil } diff --git a/internal/provider/resource_aws_transit_gateway_attachment_test.go b/internal/provider/resource_aws_transit_gateway_attachment_test.go new file mode 100644 index 000000000..9a6f6bbde --- /dev/null +++ b/internal/provider/resource_aws_transit_gateway_attachment_test.go @@ -0,0 +1,231 @@ +package provider + +import ( + "context" + "fmt" + "math/rand" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +var ( + // using unique names for AWS resource to make debugging easier + tgwAttUniqueAWSName = fmt.Sprintf("hcp-tf-provider-test-%d", rand.Intn(99999)) + testAccTGWAttachmentConfig = fmt.Sprintf(` +provider "aws" { + region = "us-west-2" +} + +resource "hcp_hvn" "test" { + hvn_id = "test-hvn" + cloud_provider = "aws" + region = "us-west-2" + cidr_block = "172.25.16.0/20" +} + +resource "aws_vpc" "example" { + cidr_block = "172.31.0.0/16" + tags = { + Name = "%[1]s" + } +} + +resource "aws_ec2_transit_gateway" "example" { + tags = { + Name = "%[1]s" + } +} + +resource "aws_ram_resource_share" "example" { + name = "%[1]s" + allow_external_principals = true +} + +resource "aws_ram_principal_association" "example" { + resource_share_arn = aws_ram_resource_share.example.arn + principal = hcp_hvn.test.provider_account_id +} + +resource "aws_ram_resource_association" "example" { + resource_share_arn = aws_ram_resource_share.example.arn + resource_arn = aws_ec2_transit_gateway.example.arn +} + +resource "hcp_aws_transit_gateway_attachment" "example" { + depends_on = [ + aws_ram_principal_association.example, + aws_ram_resource_association.example, + ] + + hvn_id = hcp_hvn.test.hvn_id + transit_gateway_attachment_id = "example-tgw-attachment" + transit_gateway_id = aws_ec2_transit_gateway.example.id + resource_share_arn = aws_ram_resource_share.example.arn +} + +resource "hcp_hvn_route" "route" { + hvn_link = hcp_hvn.test.self_link + hvn_route_id = "hvn-to-tgw-attachment" + destination_cidr = aws_vpc.example.cidr_block + target_link = hcp_aws_transit_gateway_attachment.example.self_link +} + +resource "aws_ec2_transit_gateway_vpc_attachment_accepter" "example" { + transit_gateway_attachment_id = hcp_aws_transit_gateway_attachment.example.provider_transit_gateway_attachment_id + + tags = { + Name = "%[1]s" + + // we need to have these tags here because peering-accepter will turn into + // an actual peering which HCP will populate with a set of tags (the ones below). + // After succesfull "apply"" test will try to run "plan" operation + // to make sure there are no changes to the state and if we don't specify these + // tags here then it will fail. + hvn_id = hcp_hvn.test.hvn_id + organization_id = hcp_hvn.test.organization_id + project_id = hcp_hvn.test.project_id + tgw_attachment_id = hcp_aws_transit_gateway_attachment.example.transit_gateway_attachment_id + } +} +`, tgwAttUniqueAWSName) +) + +func TestAccTGWAttachment(t *testing.T) { + resourceName := "hcp_aws_transit_gateway_attachment.example" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, true) }, + ProviderFactories: providerFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": {VersionConstraint: "~> 2.64.0"}, + }, + CheckDestroy: testAccCheckTGWAttachmentDestroy, + + Steps: []resource.TestStep{ + // Testing that initial Apply creates correct TGW attachment + { + Config: testConfig(testAccTGWAttachmentConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckTGWAttachmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "transit_gateway_attachment_id", "example-tgw-attachment"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttrSet(resourceName, "transit_gateway_id"), + resource.TestCheckResourceAttrSet(resourceName, "provider_transit_gateway_attachment_id"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "expires_at"), + testLink(resourceName, "self_link", "example-tgw-attachment", TgwAttachmentResourceType, "hcp_hvn.test"), + ), + }, + // Testing that we can import TGW attachment created in the previous step and that the + // resource terraform state will be exactly the same + { + ResourceName: resourceName, + ImportState: true, + ImportStateIdFunc: func(s *terraform.State) (string, error) { + resourceShare, ok := s.RootModule().Resources["aws_ram_resource_share.example"] + if !ok { + return "", fmt.Errorf("not found: aws_ram_resource_share.example") + } + + return fmt.Sprintf("test-hvn:example-tgw-attachment:%s", resourceShare.Primary.Attributes["arn"]), nil + }, + ImportStateVerify: true, + }, + // Testing running Terraform Apply for already known resource + { + Config: testConfig(testAccTGWAttachmentConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckTGWAttachmentExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "transit_gateway_attachment_id", "example-tgw-attachment"), + resource.TestCheckResourceAttr(resourceName, "hvn_id", "test-hvn"), + resource.TestCheckResourceAttrSet(resourceName, "transit_gateway_id"), + resource.TestCheckResourceAttrSet(resourceName, "provider_transit_gateway_attachment_id"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "project_id"), + resource.TestCheckResourceAttrSet(resourceName, "created_at"), + resource.TestCheckResourceAttrSet(resourceName, "expires_at"), + testLink(resourceName, "self_link", "example-tgw-attachment", TgwAttachmentResourceType, "hcp_hvn.test"), + ), + }, + }, + }) +} + +func testAccCheckTGWAttachmentExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + client := testAccProvider.Meta().(*clients.Client) + + tgwAttLink, err := buildLinkFromURL(id, TgwAttachmentResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build tgwAttLink for %q: %v", id, err) + } + + hvnID, ok := rs.Primary.Attributes["hvn_id"] + if !ok { + return fmt.Errorf("no hvn_id is set") + } + + tgwAttID := tgwAttLink.ID + loc := tgwAttLink.Location + + if _, err := clients.GetTGWAttachmentByID(context.Background(), client, tgwAttID, hvnID, loc); err != nil { + return fmt.Errorf("unable to get TGW attachment %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckTGWAttachmentDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clients.Client) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "hcp_aws_transit_gateway_attachment": + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + tgwAttLink, err := buildLinkFromURL(id, TgwAttachmentResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build tgwAttLink for %q: %v", id, err) + } + + hvnID, ok := rs.Primary.Attributes["hvn_id"] + if !ok { + return fmt.Errorf("no hvn_id is set") + } + + tgwAttID := tgwAttLink.ID + loc := tgwAttLink.Location + + _, err = clients.GetTGWAttachmentByID(context.Background(), client, tgwAttID, hvnID, loc) + if err == nil || !clients.IsResponseCodeNotFound(err) { + return fmt.Errorf("didn't get a 404 when reading destroyed HVN %q: %v", id, err) + } + + default: + continue + } + } + return nil +} diff --git a/internal/provider/resource_consul_cluster_test.go b/internal/provider/resource_consul_cluster_test.go index 49410892e..153e46425 100644 --- a/internal/provider/resource_consul_cluster_test.go +++ b/internal/provider/resource_consul_cluster_test.go @@ -29,7 +29,7 @@ func TestAccConsulCluster(t *testing.T) { resourceName := "hcp_consul_cluster.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t, false) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckConsulClusterDestroy, Steps: []resource.TestStep{ diff --git a/internal/provider/resource_consul_snapshot_test.go b/internal/provider/resource_consul_snapshot_test.go index 97e788a1b..6042d3c5c 100644 --- a/internal/provider/resource_consul_snapshot_test.go +++ b/internal/provider/resource_consul_snapshot_test.go @@ -32,7 +32,7 @@ func TestAccConsulSnapshot(t *testing.T) { resourceName := "hcp_consul_snapshot.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t, false) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckConsulSnapshotDestroy, Steps: []resource.TestStep{ diff --git a/internal/provider/resource_hvn.go b/internal/provider/resource_hvn.go index bcc234099..bbf8420c4 100644 --- a/internal/provider/resource_hvn.go +++ b/internal/provider/resource_hvn.go @@ -132,7 +132,7 @@ func resourceHvnCreate(ctx context.Context, d *schema.ResourceData, meta interfa log.Printf("[INFO] HVN (%s) not found, proceeding with create", hvnID) } else { - return diag.Errorf("unable to create HVN (%s) - an HVN with this ID already exists; see resouce documentation for hcp_hvn for instructions on how to add an already existing HVN to the state", hvnID) + return diag.Errorf("unable to create HVN (%s) - an HVN with this ID already exists; see resource documentation for hcp_hvn for instructions on how to add an already existing HVN to the state", hvnID) } createNetworkParams := network_service.NewCreateParams() diff --git a/internal/provider/resource_hvn_route.go b/internal/provider/resource_hvn_route.go index 09192e82b..125bcb07b 100644 --- a/internal/provider/resource_hvn_route.go +++ b/internal/provider/resource_hvn_route.go @@ -1,14 +1,245 @@ 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 +var hvnRouteDeleteTimeout = time.Minute * 25 + +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, + Delete: &hvnRouteDeleteTimeout, + }, + 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 { + if clients.IsResponseCodeNotFound(err) { + log.Printf("[WARN] HVN route (%s) not found, so no action was taken", routeID) + return 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 +262,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 +284,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 +} diff --git a/internal/provider/resource_hvn_route_test.go b/internal/provider/resource_hvn_route_test.go new file mode 100644 index 000000000..a678584e6 --- /dev/null +++ b/internal/provider/resource_hvn_route_test.go @@ -0,0 +1,191 @@ +package provider + +import ( + "context" + "fmt" + "math/rand" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + "github.com/hashicorp/terraform-provider-hcp/internal/clients" +) + +var ( + // using unique names for AWS resource to make debugging easier + hvnRouteUniqueAWSName = fmt.Sprintf("hcp-tf-provider-test-%d", rand.Intn(99999)) + testAccHvnRouteConfig = fmt.Sprintf(` +provider "aws" { + region = "us-west-2" +} + +resource "hcp_hvn" "test" { + hvn_id = "test-hvn" + cloud_provider = "aws" + region = "us-west-2" +} + +resource "aws_vpc" "vpc" { + cidr_block = "10.220.0.0/16" + tags = { + Name = "%[1]s" + } +} + +resource "hcp_aws_network_peering" "peering" { + peering_id = "hcp-tf-provider-test" + hvn_id = hcp_hvn.test.hvn_id + peer_account_id = aws_vpc.vpc.owner_id + peer_vpc_id = aws_vpc.vpc.id + peer_vpc_region = "us-west-2" +} + +resource "hcp_hvn_route" "route" { + hvn_route_id = "peering-route" + hvn_link = hcp_hvn.test.self_link + destination_cidr = "172.31.0.0/16" + target_link = hcp_aws_network_peering.peering.self_link +} + +resource "aws_vpc_peering_connection_accepter" "peering-accepter" { + vpc_peering_connection_id = hcp_aws_network_peering.peering.provider_peering_id + auto_accept = true + tags = { + Name = "%[1]s" + + // we need to have these tags here because peering-accepter will turn into + // an actual peering which HCP will populate with a set of tags (the ones below). + // After succesfull "apply"" test will try to run "plan" operation + // to make sure there are no changes to the state and if we don't specify these + // tags here then it will fail. + hvn_id = hcp_hvn.test.hvn_id + organization_id = hcp_hvn.test.organization_id + project_id = hcp_hvn.test.project_id + peering_id = hcp_aws_network_peering.peering.peering_id + } +} +`, hvnRouteUniqueAWSName) +) + +func TestAccHvnRoute(t *testing.T) { + resourceName := "hcp_hvn_route.route" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t, true) }, + ProviderFactories: providerFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": {VersionConstraint: "~> 2.64.0"}, + }, + CheckDestroy: testAccCheckHvnRouteDestroy, + + Steps: []resource.TestStep{ + // Testing that initial Apply created correct HVN route + { + Config: testConfig(testAccHvnRouteConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckHvnRouteExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "hvn_route_id", "peering-route"), + resource.TestCheckResourceAttr(resourceName, "destination_cidr", "172.31.0.0/16"), + testLink(resourceName, "self_link", "peering-route", HVNRouteResourceType, "hcp_hvn.test"), + testLink(resourceName, "target_link", "hcp-tf-provider-test", PeeringResourceType, "hcp_hvn.test"), + ), + }, + // Testing that we can import HVN route created in the previous step and that the + // resource terraform state will be exactly the same + { + ResourceName: resourceName, + ImportState: true, + ImportStateId: "test-hvn:peering-route", + ImportStateVerify: true, + }, + // Testing running Terraform Apply for already known resource + { + Config: testConfig(testAccHvnRouteConfig), + Check: resource.ComposeTestCheckFunc( + testAccCheckHvnRouteExists(resourceName), + resource.TestCheckResourceAttr(resourceName, "hvn_route_id", "peering-route"), + resource.TestCheckResourceAttr(resourceName, "destination_cidr", "172.31.0.0/16"), + testLink(resourceName, "self_link", "peering-route", HVNRouteResourceType, "hcp_hvn.test"), + testLink(resourceName, "target_link", "hcp-tf-provider-test", PeeringResourceType, "hcp_hvn.test"), + ), + }, + }, + }) +} + +func testAccCheckHvnRouteExists(name string) resource.TestCheckFunc { + return func(s *terraform.State) error { + rs, ok := s.RootModule().Resources[name] + if !ok { + return fmt.Errorf("not found: %s", name) + } + + id := rs.Primary.ID + if id == "" { + return fmt.Errorf("no ID is set") + } + + client := testAccProvider.Meta().(*clients.Client) + + hvnRouteLink, err := buildLinkFromURL(id, HVNRouteResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build hvnRouteLink for %q: %v", id, err) + } + + hvnUrl, ok := rs.Primary.Attributes["hvn_link"] + if !ok { + return fmt.Errorf("hcp_hvn_route doesn't have hvn_link") + } + hvnLink, err := parseLinkURL(hvnUrl, HvnResourceType) + if err != nil { + return fmt.Errorf("failed to parse hvn_link: %w", err) + } + + hvnRouteID := hvnRouteLink.ID + loc := hvnRouteLink.Location + + if _, err := clients.GetHVNRoute(context.Background(), client, hvnLink.ID, hvnRouteID, loc); err != nil { + return fmt.Errorf("unable to get HVN route %q: %v", id, err) + } + + return nil + } +} + +func testAccCheckHvnRouteDestroy(s *terraform.State) error { + client := testAccProvider.Meta().(*clients.Client) + + for _, rs := range s.RootModule().Resources { + switch rs.Type { + case "hcp_hvn_route": + id := rs.Primary.ID + + hvnRouteLink, err := buildLinkFromURL(id, HVNRouteResourceType, client.Config.OrganizationID) + if err != nil { + return fmt.Errorf("unable to build hvnRouteLink for %q: %v", id, err) + } + + hvnUrl, ok := rs.Primary.Attributes["hvn_link"] + if !ok { + return fmt.Errorf("hcp_hvn_route doesn't have hvn_link") + } + hvnLink, err := parseLinkURL(hvnUrl, HvnResourceType) + if err != nil { + return fmt.Errorf("failed to parse hvn_link: %w", err) + } + + hvnRouteID := hvnRouteLink.ID + loc := hvnRouteLink.Location + + _, err = clients.GetHVNRoute(context.Background(), client, hvnLink.ID, hvnRouteID, loc) + if err == nil || !clients.IsResponseCodeNotFound(err) { + return fmt.Errorf("didn't get a 404 when reading destroyed HVN %q: %v", id, err) + } + + default: + continue + } + } + return nil +} diff --git a/internal/provider/resource_hvn_test.go b/internal/provider/resource_hvn_test.go index 21e887bf5..f4eab79e3 100644 --- a/internal/provider/resource_hvn_test.go +++ b/internal/provider/resource_hvn_test.go @@ -25,7 +25,7 @@ func TestAccHvn(t *testing.T) { resourceName := "hcp_hvn.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t, false) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckHvnDestroy, Steps: []resource.TestStep{ @@ -41,7 +41,7 @@ func TestAccHvn(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "project_id"), resource.TestCheckResourceAttrSet(resourceName, "created_at"), resource.TestCheckResourceAttrSet(resourceName, "provider_account_id"), - testSelfLink(resourceName, "test-hvn", HvnResourceType), + testLink(resourceName, "self_link", "test-hvn", HvnResourceType, resourceName), ), }, { @@ -69,7 +69,7 @@ func TestAccHvn(t *testing.T) { resource.TestCheckResourceAttrSet(resourceName, "project_id"), resource.TestCheckResourceAttrSet(resourceName, "created_at"), resource.TestCheckResourceAttrSet(resourceName, "provider_account_id"), - testSelfLink(resourceName, "test-hvn", HvnResourceType), + testLink(resourceName, "self_link", "test-hvn", HvnResourceType, resourceName), ), }, }, @@ -106,19 +106,24 @@ func testAccCheckHvnExists(name string) resource.TestCheckFunc { } } -func testSelfLink(name string, expectedID, expectedType string) resource.TestCheckFunc { +func testLink(resourceName, fieldName, expectedID, expectedType, projectIDSourceResource string) resource.TestCheckFunc { return func(s *terraform.State) error { - rs, ok := s.RootModule().Resources[name] + rs, ok := s.RootModule().Resources[resourceName] if !ok { - return fmt.Errorf("not found: %s", name) + return fmt.Errorf("not found: %s", resourceName) + } + + selfLink, ok := rs.Primary.Attributes[fieldName] + if !ok { + return fmt.Errorf("%s isn't set", fieldName) } - selfLink, ok := rs.Primary.Attributes["self_link"] + projectIDSource, ok := s.RootModule().Resources[projectIDSourceResource] if !ok { - return fmt.Errorf("self_link isn't set") + return fmt.Errorf("not found: %s", projectIDSourceResource) } - projectID, ok := rs.Primary.Attributes["project_id"] + projectID, ok := projectIDSource.Primary.Attributes["project_id"] if !ok { return fmt.Errorf("project_id isn't set") } diff --git a/internal/provider/resource_vault_cluster_admin_token_test.go b/internal/provider/resource_vault_cluster_admin_token_test.go index 125344289..8dd7db596 100644 --- a/internal/provider/resource_vault_cluster_admin_token_test.go +++ b/internal/provider/resource_vault_cluster_admin_token_test.go @@ -30,7 +30,7 @@ func TestAccVaultClusterAdminToken(t *testing.T) { resourceName := "hcp_vault_cluster_admin_token.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t, false) }, ProviderFactories: providerFactories, Steps: []resource.TestStep{ { diff --git a/internal/provider/resource_vault_cluster_test.go b/internal/provider/resource_vault_cluster_test.go index 30d81f1c5..873990962 100644 --- a/internal/provider/resource_vault_cluster_test.go +++ b/internal/provider/resource_vault_cluster_test.go @@ -29,7 +29,7 @@ func TestAccVaultCluster(t *testing.T) { resourceName := "hcp_vault_cluster.test" resource.Test(t, resource.TestCase{ - PreCheck: func() { testAccPreCheck(t) }, + PreCheck: func() { testAccPreCheck(t, false) }, ProviderFactories: providerFactories, CheckDestroy: testAccCheckVaultClusterDestroy, Steps: []resource.TestStep{ diff --git a/templates/guides/hvn-route-migration-guide.md.tmpl b/templates/guides/hvn-route-migration-guide.md.tmpl new file mode 100644 index 000000000..4147c3ab1 --- /dev/null +++ b/templates/guides/hvn-route-migration-guide.md.tmpl @@ -0,0 +1,76 @@ +--- +subcategory: "" +page_title: "HVN Route Migration Guide - HCP Provider" +description: |- + An guide to migrating HCP networking resources to use HVN routes. +--- + +# Introducing HVN routes + +The HVN route is a new resource that belongs to an HVN. It contains a CIDR block and targets a networking connection: +either a peering or transit gateway attachment. + +HVN routes provide a general view on how an HVN's traffic is routed across all networking connections and create a flexible way of managing these routing rules. + +## Migrating existing peerings and transit gateway attachments + +There are two ways to migrate existing peerings and transit gateway attachments managed by Terraform: + + 1. Recreate Resources with Updated Schema + * This option is quicker but will result in downtime and possible data loss. Best for test environments. Will allow you to specify human-readable ids for the resources. + * Comment out all `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resources. + * Run `terraform apply` to destroy currently existing connections. + * Uncomment and update all `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resource definitions to match the new schema. + * Add corresponding `hcp_hvn_route` resources for each CIDR targeting corresponding peering connections or transit gateway attachment. + * Run `terraform apply` to recreate connections. + + 2. Re-Import with Updated Syntax: + * This option allows you to avoid downtime or data loss. + * Update any `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment` resource definitions to match the new schema. All values needed can be found on the details pages of Peerings and TGW attachment in the HCP Portal. + * Add corresponding `hcp_hvn_route` resources for each CIDR targeting corresponding peering connections or transit gateway attachments. + * Run `terraform import hcp_hvn_route. :` for each `hcp_hvn_route`. The `` can be found on the details pages of the corresponding HVN connection in the HCP Portal. + * Run `terraform plan` and make sure that there are no changes detected by the Terraform. + +The examples below walk through the schema upgrade and re-import steps. + +### Peering example + +Given: +{{ tffile "examples/guides/hvn_route_migration_guide/before-peering.tf" }} + +Rewrite it to the new schema and add corresponding HVN route: +{{ tffile "examples/guides/hvn_route_migration_guide/after-peering.tf" }} + +Run `import` for the `hcp_hvn_route`: +```shell +$ terraform import hcp_hvn_route.peering-route prod-hvn:a8dda9a8-0f69-4fa0-b38c-55be302fdddb +``` + +Run `terraform plan` to make sure there are no changes detected by the Terraform: +```shell +$ terraform plan +No changes. Infrastructure is up-to-date. +``` + +### Transit gateway attachment example + +Given: +{{ tffile "examples/guides/hvn_route_migration_guide/before-tgw.tf" }} + +Rewrite it to the new schema and add corresponding HVN route: +{{ tffile "examples/guides/hvn_route_migration_guide/after-tgw.tf" }} + +Run `import` for each `hcp_hvn_route` you've added: +```shell +$ terraform import hcp_hvn_route.tgw-route-1 prod-hvn:35392425-215a-44ec-bbd0-051bb777ce5f +... + +$ terraform import hcp_hvn_route.tgw-route-2 prod-hvn:9867959a-d81b-4e52-ae8e-ca56f9dd06fc +... +``` + +Run `terraform plan` to make sure there are no changes detected by the Terraform: +```shell +$ terraform plan +No changes. Infrastructure is up-to-date. +``` \ No newline at end of file diff --git a/templates/index.md.tmpl b/templates/index.md.tmpl index 83804f8aa..6cff2d798 100644 --- a/templates/index.md.tmpl +++ b/templates/index.md.tmpl @@ -9,8 +9,8 @@ description: |- The HCP provider provides resources to manage [HashiCorp Cloud Platform](https://cloud.hashicorp.com/) (HCP) resources. -~> **Upcoming Migration:** The upcoming release of HVN Routes will include breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This PR](https://github.com/hashicorp/terraform-provider-hcp/pull/128) contains a migration guide. -Please pin to the current version to avoid disruption until you are ready to migrate. +~> **Upcoming Migration:** The upcoming release of HVN Routes in v0.7.0 will include breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This guide](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/hvn-route-migration-guide) walks through how to migrate to the new resource syntax. +Please pin to the previous version to avoid disruption until you are ready to migrate. -> **Note:** Please refer to the provider's [Release Notes](https://github.com/hashicorp/terraform-provider-hcp/releases) for critical fixes. diff --git a/templates/resources/hvn_route.md.tmpl b/templates/resources/hvn_route.md.tmpl new file mode 100644 index 000000000..9f19b5879 --- /dev/null +++ b/templates/resources/hvn_route.md.tmpl @@ -0,0 +1,25 @@ +--- +page_title: "{{.Type}} {{.Name}} - {{.ProviderName}}" +subcategory: "" +description: |- +{{ .Description | plainmarkdown | trimspace | prefixlines " " }} +--- + +# {{.Type}} ({{.Name}}) + +~> **Migration Required:** The release of HVN Routes in v0.7.0 includes breaking changes that affect `hcp_aws_network_peering` and `hcp_aws_transit_gateway_attachment`. [This guide](https://registry.terraform.io/providers/hashicorp/hcp/latest/docs/guides/hvn-route-migration-guide) walks through how to migrate to the new resource syntax. +Please pin to the previous version to avoid disruption until you are ready to migrate. + +{{ .Description | trimspace }} + +## Example Usage + +{{ tffile "examples/resources/hcp_hvn_route/resource.tf" }} + +{{ .SchemaMarkdown | trimspace }} + +## Import + +Import is supported using the following syntax: + +{{ codefile "shell" "examples/resources/hcp_hvn_route/import.sh" }}