From 4898b8caaebc9aa3fe2c82965cef782f0cc74ce8 Mon Sep 17 00:00:00 2001 From: Myroslav Vivcharyk Date: Wed, 8 Jan 2025 15:53:49 +0100 Subject: [PATCH] feat(organization_vpc): added organization_vpc and peering connections resources --- Makefile | 1 + .../aws_org_vpc_peering_connection.md | 36 ++ .../azure_org_vpc_peering_connection.md | 37 ++ .../gcp_org_vpc_peering_connection.md | 34 ++ docs/data-sources/organization_vpc.md | 35 ++ .../aws_org_vpc_peering_connection.md | 75 +++ .../azure_org_vpc_peering_connection.md | 78 +++ .../gcp_org_vpc_peering_connection.md | 72 +++ docs/resources/organization_vpc.md | 66 +++ .../import.sh | 1 + .../resource.tf | 13 + .../import.sh | 1 + .../resource.tf | 15 + .../import.sh | 1 + .../resource.tf | 12 + .../aiven_organization_vpc/import.sh | 1 + .../aiven_organization_vpc/resource.tf | 5 + go.mod | 1 + internal/acctest/acctest.go | 46 ++ internal/acctest/template.go | 13 + internal/common/client.go | 11 + internal/schemautil/schemautil.go | 9 + internal/sdkprovider/provider/provider.go | 32 +- .../project/organization_project_test.go | 156 +++--- .../vpc/aws_org_vpc_peering_connection.go | 211 ++++++++ ..._org_vpc_peering_connection_data_source.go | 35 ++ .../aws_org_vpc_peering_connection_test.go | 320 ++++++++++++ .../service/vpc/aws_vpc_peering_connection.go | 2 +- .../vpc/azure_org_vpc_peering_connection.go | 223 +++++++++ ..._org_vpc_peering_connection_data_source.go | 36 ++ .../azure_org_vpc_peering_connection_test.go | 139 ++++++ .../vpc/azure_vpc_peering_connection.go | 2 +- .../vpc/gcp_org_vpc_peering_connection.go | 197 ++++++++ ..._org_vpc_peering_connection_data_source.go | 34 ++ .../gcp_org_vpc_peering_connection_test.go | 233 +++++++++ .../service/vpc/gcp_vpc_peering_connection.go | 2 +- .../service/vpc/org_vpc_peering_connection.go | 149 ++++++ .../vpc/org_vpc_peering_connection_test.go | 454 ++++++++++++++++++ .../service/vpc/organization_vpc.go | 203 ++++++++ .../vpc/organization_vpc_data_source.go | 31 ++ .../service/vpc/organization_vpc_test.go | 178 +++++++ internal/sdkprovider/service/vpc/sweep.go | 119 +++++ .../service/vpc/vpc_peering_connection.go | 57 +-- .../vpc/vpc_peering_connection_state.go | 111 +++++ 44 files changed, 3341 insertions(+), 146 deletions(-) create mode 100644 docs/data-sources/aws_org_vpc_peering_connection.md create mode 100644 docs/data-sources/azure_org_vpc_peering_connection.md create mode 100644 docs/data-sources/gcp_org_vpc_peering_connection.md create mode 100644 docs/data-sources/organization_vpc.md create mode 100644 docs/resources/aws_org_vpc_peering_connection.md create mode 100644 docs/resources/azure_org_vpc_peering_connection.md create mode 100644 docs/resources/gcp_org_vpc_peering_connection.md create mode 100644 docs/resources/organization_vpc.md create mode 100644 examples/resources/aiven_aws_org_vpc_peering_connection/import.sh create mode 100644 examples/resources/aiven_aws_org_vpc_peering_connection/resource.tf create mode 100644 examples/resources/aiven_azure_org_vpc_peering_connection/import.sh create mode 100644 examples/resources/aiven_azure_org_vpc_peering_connection/resource.tf create mode 100644 examples/resources/aiven_gcp_org_vpc_peering_connection/import.sh create mode 100644 examples/resources/aiven_gcp_org_vpc_peering_connection/resource.tf create mode 100644 examples/resources/aiven_organization_vpc/import.sh create mode 100644 examples/resources/aiven_organization_vpc/resource.tf create mode 100644 internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection.go create mode 100644 internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_data_source.go create mode 100644 internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_test.go create mode 100644 internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection.go create mode 100644 internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_data_source.go create mode 100644 internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_test.go create mode 100644 internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection.go create mode 100644 internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_data_source.go create mode 100644 internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_test.go create mode 100644 internal/sdkprovider/service/vpc/org_vpc_peering_connection.go create mode 100644 internal/sdkprovider/service/vpc/org_vpc_peering_connection_test.go create mode 100644 internal/sdkprovider/service/vpc/organization_vpc.go create mode 100644 internal/sdkprovider/service/vpc/organization_vpc_data_source.go create mode 100644 internal/sdkprovider/service/vpc/organization_vpc_test.go create mode 100644 internal/sdkprovider/service/vpc/vpc_peering_connection_state.go diff --git a/Makefile b/Makefile index 402681aeb..11eb04c26 100644 --- a/Makefile +++ b/Makefile @@ -69,6 +69,7 @@ build: # } # } #} + build-dev: $(BUILD_DEV_DIR) $(GO) build -gcflags='all=-N -l' -o $(BUILD_DEV_BIN) diff --git a/docs/data-sources/aws_org_vpc_peering_connection.md b/docs/data-sources/aws_org_vpc_peering_connection.md new file mode 100644 index 000000000..aae5b935c --- /dev/null +++ b/docs/data-sources/aws_org_vpc_peering_connection.md @@ -0,0 +1,36 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_aws_org_vpc_peering_connection Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Gets information about an AWS VPC peering connection. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_aws_org_vpc_peering_connection (Data Source) + +Gets information about an AWS VPC peering connection. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `aws_account_id` (String) AWS account ID. Changing this property forces recreation of the resource. +- `aws_vpc_id` (String) AWS VPC ID. Changing this property forces recreation of the resource. +- `aws_vpc_region` (String) The AWS region of the peered VPC. For example, `eu-central-1`. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. + +### Read-Only + +- `aws_vpc_peering_connection_id` (String) The ID of the AWS VPC peering connection. +- `id` (String) The ID of this resource. +- `peering_connection_id` (String) The ID of the peering connection. +- `state` (String) State of the peering connection. The possible values are `ACTIVE`, `APPROVED`, `APPROVED_PEER_REQUESTED`, `DELETED`, `DELETED_BY_PEER`, `DELETING`, `ERROR`, `INVALID_SPECIFICATION`, `PENDING_PEER` and `REJECTED_BY_PEER`. diff --git a/docs/data-sources/azure_org_vpc_peering_connection.md b/docs/data-sources/azure_org_vpc_peering_connection.md new file mode 100644 index 000000000..5a0473992 --- /dev/null +++ b/docs/data-sources/azure_org_vpc_peering_connection.md @@ -0,0 +1,37 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_azure_org_vpc_peering_connection Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Gets information about about an Azure VPC peering connection. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_azure_org_vpc_peering_connection (Data Source) + +Gets information about about an Azure VPC peering connection. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `azure_subscription_id` (String) The ID of the Azure subscription in UUID4 format. Changing this property forces recreation of the resource. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. +- `peer_resource_group` (String) The name of the Azure resource group associated with the VNet. Changing this property forces recreation of the resource. +- `vnet_name` (String) The name of the Azure VNet. Changing this property forces recreation of the resource. + +### Read-Only + +- `id` (String) The ID of this resource. +- `peer_azure_app_id` (String) The ID of the Azure app that is allowed to create a peering to the Azure Virtual Network (VNet) in UUID4 format. Changing this property forces recreation of the resource. +- `peer_azure_tenant_id` (String) The Azure tenant ID in UUID4 format. Changing this property forces recreation of the resource. +- `peering_connection_id` (String) The ID of the cloud provider for the peering connection. +- `state` (String) State of the peering connection diff --git a/docs/data-sources/gcp_org_vpc_peering_connection.md b/docs/data-sources/gcp_org_vpc_peering_connection.md new file mode 100644 index 000000000..ebfed93d7 --- /dev/null +++ b/docs/data-sources/gcp_org_vpc_peering_connection.md @@ -0,0 +1,34 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_gcp_org_vpc_peering_connection Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + The GCP VPC Peering Connection data source provides information about the existing Aiven VPC Peering Connection. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_gcp_org_vpc_peering_connection (Data Source) + +The GCP VPC Peering Connection data source provides information about the existing Aiven VPC Peering Connection. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `gcp_project_id` (String) Google Cloud project ID. Changing this property forces recreation of the resource. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. +- `peer_vpc` (String) Google Cloud VPC network name. Changing this property forces recreation of the resource. + +### Read-Only + +- `id` (String) The ID of this resource. +- `self_link` (String) Computed Google Cloud network peering link. +- `state` (String) State of the peering connection. diff --git a/docs/data-sources/organization_vpc.md b/docs/data-sources/organization_vpc.md new file mode 100644 index 000000000..9ac732431 --- /dev/null +++ b/docs/data-sources/organization_vpc.md @@ -0,0 +1,35 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_organization_vpc Data Source - terraform-provider-aiven" +subcategory: "" +description: |- + Gets information about an existing VPC in an Aiven organization. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_organization_vpc (Data Source) + +Gets information about an existing VPC in an Aiven organization. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + + + + +## Schema + +### Required + +- `organization_id` (String) The ID of the organization. +- `organization_vpc_id` (String) The ID of the Aiven Organization VPC. + +### Read-Only + +- `cloud_name` (String) The cloud provider and region where the service is hosted in the format `CLOUD_PROVIDER-REGION_NAME`. For example, `google-europe-west1` or `aws-us-east-2`. Changing this property forces recreation of the resource. +- `create_time` (String) Time of creation of the VPC. +- `id` (String) The ID of this resource. +- `network_cidr` (String) Network address range used by the VPC. For example, `192.168.0.0/24`. +- `state` (String) State of the VPC. The possible values are `ACTIVE`, `APPROVED`, `DELETED` and `DELETING`. +- `update_time` (String) Time of the last update of the VPC. diff --git a/docs/resources/aws_org_vpc_peering_connection.md b/docs/resources/aws_org_vpc_peering_connection.md new file mode 100644 index 000000000..7c611d61b --- /dev/null +++ b/docs/resources/aws_org_vpc_peering_connection.md @@ -0,0 +1,75 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_aws_org_vpc_peering_connection Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages an AWS VPC peering connection with an Aiven Organization VPC. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_aws_org_vpc_peering_connection (Resource) + +Creates and manages an AWS VPC peering connection with an Aiven Organization VPC. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + +## Example Usage + +```terraform +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "aws-eu-central-1" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_aws_org_vpc_peering_connection" "example_peering" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + aws_account_id = var.aws_id + aws_vpc_id = "vpc-1a2b3c4d5e6f7g8h9" + aws_vpc_region = "aws-us-east-2" +} +``` + + +## Schema + +### Required + +- `aws_account_id` (String) AWS account ID. Changing this property forces recreation of the resource. +- `aws_vpc_id` (String) AWS VPC ID. Changing this property forces recreation of the resource. +- `aws_vpc_region` (String) The AWS region of the peered VPC. For example, `eu-central-1`. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `aws_vpc_peering_connection_id` (String) The ID of the AWS VPC peering connection. +- `id` (String) The ID of this resource. +- `peering_connection_id` (String) The ID of the peering connection. +- `state` (String) State of the peering connection. The possible values are `ACTIVE`, `APPROVED`, `APPROVED_PEER_REQUESTED`, `DELETED`, `DELETED_BY_PEER`, `DELETING`, `ERROR`, `INVALID_SPECIFICATION`, `PENDING_PEER` and `REJECTED_BY_PEER`. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import aiven_aws_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/AWS_ACCOUNT_ID/AWS_VPC_ID/AWS_REGION +``` diff --git a/docs/resources/azure_org_vpc_peering_connection.md b/docs/resources/azure_org_vpc_peering_connection.md new file mode 100644 index 000000000..de63fe889 --- /dev/null +++ b/docs/resources/azure_org_vpc_peering_connection.md @@ -0,0 +1,78 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_azure_org_vpc_peering_connection Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages an Azure VPC peering connection with an Aiven VPC. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_azure_org_vpc_peering_connection (Resource) + +Creates and manages an Azure VPC peering connection with an Aiven VPC. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + +## Example Usage + +```terraform +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "azure-germany-westcentral" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_azure_org_vpc_peering_connection" "example_peering" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + vnet_name = "my-vnet" + peer_resource_group = "my-resource-group" + peer_azure_app_id = "87654321-4321-4321-4321-210987654321" + peer_azure_tenant_id = "11111111-2222-3333-4444-555555555555" +} +``` + + +## Schema + +### Required + +- `azure_subscription_id` (String) The ID of the Azure subscription in UUID4 format. Changing this property forces recreation of the resource. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. +- `peer_azure_app_id` (String) The ID of the Azure app that is allowed to create a peering to the Azure Virtual Network (VNet) in UUID4 format. Changing this property forces recreation of the resource. +- `peer_azure_tenant_id` (String) The Azure tenant ID in UUID4 format. Changing this property forces recreation of the resource. +- `peer_resource_group` (String) The name of the Azure resource group associated with the VNet. Changing this property forces recreation of the resource. +- `vnet_name` (String) The name of the Azure VNet. Changing this property forces recreation of the resource. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. +- `peering_connection_id` (String) The ID of the cloud provider for the peering connection. +- `state` (String) State of the peering connection + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import aiven_azure_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/AZURE_SUBSCRIPTION_ID/VNET_NAME/RESOURCE_GROUP +``` diff --git a/docs/resources/gcp_org_vpc_peering_connection.md b/docs/resources/gcp_org_vpc_peering_connection.md new file mode 100644 index 000000000..65e04bbbb --- /dev/null +++ b/docs/resources/gcp_org_vpc_peering_connection.md @@ -0,0 +1,72 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_gcp_org_vpc_peering_connection Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages a Google Cloud VPC peering connection. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_gcp_org_vpc_peering_connection (Resource) + +Creates and manages a Google Cloud VPC peering connection. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + +## Example Usage + +```terraform +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "google-europe-west10" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_gcp_org_vpc_peering_connection" "example" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + gcp_project_id = "my-gcp-project-123" # Your GCP project ID + peer_vpc = "my-vpc-network" # Your GCP VPC network name +} +``` + + +## Schema + +### Required + +- `gcp_project_id` (String) Google Cloud project ID. Changing this property forces recreation of the resource. +- `organization_id` (String) Identifier of the organization. +- `organization_vpc_id` (String) Identifier of the organization VPC. +- `peer_vpc` (String) Google Cloud VPC network name. Changing this property forces recreation of the resource. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `id` (String) The ID of this resource. +- `self_link` (String) Computed Google Cloud network peering link. +- `state` (String) State of the peering connection. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import aiven_gcp_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/GCP_PROJECT_ID/VPC_NAME +``` diff --git a/docs/resources/organization_vpc.md b/docs/resources/organization_vpc.md new file mode 100644 index 000000000..7f98c49ea --- /dev/null +++ b/docs/resources/organization_vpc.md @@ -0,0 +1,66 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "aiven_organization_vpc Resource - terraform-provider-aiven" +subcategory: "" +description: |- + Creates and manages a VPC for an Aiven organization. + This resource is in the beta stage and may change without notice. Set + the PROVIDER_AIVEN_ENABLE_BETA environment variable to use the resource. +--- + +# aiven_organization_vpc (Resource) + +Creates and manages a VPC for an Aiven organization. + +**This resource is in the beta stage and may change without notice.** Set +the `PROVIDER_AIVEN_ENABLE_BETA` environment variable to use the resource. + +## Example Usage + +```terraform +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "aws-eu-central-1" + network_cidr = "10.0.0.0/24" +} +``` + + +## Schema + +### Required + +- `cloud_name` (String) The cloud provider and region where the service is hosted in the format `CLOUD_PROVIDER-REGION_NAME`. For example, `google-europe-west1` or `aws-us-east-2`. Changing this property forces recreation of the resource. +- `network_cidr` (String) Network address range used by the VPC. For example, `192.168.0.0/24`. +- `organization_id` (String) The ID of the organization. + +### Optional + +- `timeouts` (Block, Optional) (see [below for nested schema](#nestedblock--timeouts)) + +### Read-Only + +- `create_time` (String) Time of creation of the VPC. +- `id` (String) The ID of this resource. +- `organization_vpc_id` (String) The ID of the Aiven Organization VPC. +- `state` (String) State of the VPC. The possible values are `ACTIVE`, `APPROVED`, `DELETED` and `DELETING`. +- `update_time` (String) Time of the last update of the VPC. + + +### Nested Schema for `timeouts` + +Optional: + +- `create` (String) +- `default` (String) +- `delete` (String) +- `read` (String) +- `update` (String) + +## Import + +Import is supported using the following syntax: + +```shell +terraform import aiven_organization_vpc.example ORGANIZATION_ID/ORGANIZATION_VPC_ID +``` diff --git a/examples/resources/aiven_aws_org_vpc_peering_connection/import.sh b/examples/resources/aiven_aws_org_vpc_peering_connection/import.sh new file mode 100644 index 000000000..b3064b075 --- /dev/null +++ b/examples/resources/aiven_aws_org_vpc_peering_connection/import.sh @@ -0,0 +1 @@ +terraform import aiven_aws_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/AWS_ACCOUNT_ID/AWS_VPC_ID/AWS_REGION \ No newline at end of file diff --git a/examples/resources/aiven_aws_org_vpc_peering_connection/resource.tf b/examples/resources/aiven_aws_org_vpc_peering_connection/resource.tf new file mode 100644 index 000000000..620c34648 --- /dev/null +++ b/examples/resources/aiven_aws_org_vpc_peering_connection/resource.tf @@ -0,0 +1,13 @@ +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "aws-eu-central-1" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_aws_org_vpc_peering_connection" "example_peering" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + aws_account_id = var.aws_id + aws_vpc_id = "vpc-1a2b3c4d5e6f7g8h9" + aws_vpc_region = "aws-us-east-2" +} \ No newline at end of file diff --git a/examples/resources/aiven_azure_org_vpc_peering_connection/import.sh b/examples/resources/aiven_azure_org_vpc_peering_connection/import.sh new file mode 100644 index 000000000..8b942e75c --- /dev/null +++ b/examples/resources/aiven_azure_org_vpc_peering_connection/import.sh @@ -0,0 +1 @@ +terraform import aiven_azure_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/AZURE_SUBSCRIPTION_ID/VNET_NAME/RESOURCE_GROUP \ No newline at end of file diff --git a/examples/resources/aiven_azure_org_vpc_peering_connection/resource.tf b/examples/resources/aiven_azure_org_vpc_peering_connection/resource.tf new file mode 100644 index 000000000..1cd073c4d --- /dev/null +++ b/examples/resources/aiven_azure_org_vpc_peering_connection/resource.tf @@ -0,0 +1,15 @@ +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "azure-germany-westcentral" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_azure_org_vpc_peering_connection" "example_peering" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + azure_subscription_id = "12345678-1234-1234-1234-123456789012" + vnet_name = "my-vnet" + peer_resource_group = "my-resource-group" + peer_azure_app_id = "87654321-4321-4321-4321-210987654321" + peer_azure_tenant_id = "11111111-2222-3333-4444-555555555555" +} \ No newline at end of file diff --git a/examples/resources/aiven_gcp_org_vpc_peering_connection/import.sh b/examples/resources/aiven_gcp_org_vpc_peering_connection/import.sh new file mode 100644 index 000000000..5e322b74b --- /dev/null +++ b/examples/resources/aiven_gcp_org_vpc_peering_connection/import.sh @@ -0,0 +1 @@ +terraform import aiven_gcp_org_vpc_peering_connection.example ORGANIZATION_ID/ORGANIZATION_VPC_ID/GCP_PROJECT_ID/VPC_NAME \ No newline at end of file diff --git a/examples/resources/aiven_gcp_org_vpc_peering_connection/resource.tf b/examples/resources/aiven_gcp_org_vpc_peering_connection/resource.tf new file mode 100644 index 000000000..97f7948b4 --- /dev/null +++ b/examples/resources/aiven_gcp_org_vpc_peering_connection/resource.tf @@ -0,0 +1,12 @@ +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "google-europe-west10" + network_cidr = "10.0.0.0/24" +} + +resource "aiven_gcp_org_vpc_peering_connection" "example" { + organization_id = aiven_organization_vpc.example_vpc.organization_id + organization_vpc_id = aiven_organization_vpc.example_vpc.organization_vpc_id + gcp_project_id = "my-gcp-project-123" # Your GCP project ID + peer_vpc = "my-vpc-network" # Your GCP VPC network name +} \ No newline at end of file diff --git a/examples/resources/aiven_organization_vpc/import.sh b/examples/resources/aiven_organization_vpc/import.sh new file mode 100644 index 000000000..a0b2f2f80 --- /dev/null +++ b/examples/resources/aiven_organization_vpc/import.sh @@ -0,0 +1 @@ +terraform import aiven_organization_vpc.example ORGANIZATION_ID/ORGANIZATION_VPC_ID \ No newline at end of file diff --git a/examples/resources/aiven_organization_vpc/resource.tf b/examples/resources/aiven_organization_vpc/resource.tf new file mode 100644 index 000000000..50bea6014 --- /dev/null +++ b/examples/resources/aiven_organization_vpc/resource.tf @@ -0,0 +1,5 @@ +resource "aiven_organization_vpc" "example_vpc" { + organization_id = data.aiven_organization.example.id + cloud_name = "aws-eu-central-1" + network_cidr = "10.0.0.0/24" +} \ No newline at end of file diff --git a/go.mod b/go.mod index b1c696e67..e16c45f2f 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,7 @@ require ( github.com/docker/go-units v0.5.0 github.com/ettle/strcase v0.2.0 github.com/google/go-cmp v0.6.0 + github.com/google/uuid v1.6.0 github.com/gruntwork-io/terratest v0.48.2 github.com/hamba/avro/v2 v2.28.0 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 diff --git a/internal/acctest/acctest.go b/internal/acctest/acctest.go index da84036a8..75a1aa7d5 100644 --- a/internal/acctest/acctest.go +++ b/internal/acctest/acctest.go @@ -230,3 +230,49 @@ func ResourceFromState(state *terraform.State, name string) (*terraform.Resource return rs, nil } + +// EnvVarCheckMode determines how missing environment variables are handled +type EnvVarCheckMode int + +const ( + // MustBeSet fails the test if any required env vars are missing + MustBeSet EnvVarCheckMode = iota + // SkipIfMissing skips the test if any required env vars are missing + SkipIfMissing +) + +func CheckEnvVars(t *testing.T, mode EnvVarCheckMode, vars ...string) map[string]string { + t.Helper() + + values := make(map[string]string) + missingVars := make([]string, 0) + + for _, v := range vars { + val, ok := os.LookupEnv(v) + if !ok { + missingVars = append(missingVars, v) + continue + } + values[v] = val + } + + if len(missingVars) > 0 { + msg := fmt.Sprintf("required environment variables not set: %s", strings.Join(missingVars, ", ")) + switch mode { + case MustBeSet: + t.Fatal(msg) + case SkipIfMissing: + t.Skip(msg) + } + } + + return values +} + +func MustHaveEnvVars(t *testing.T, vars ...string) map[string]string { + return CheckEnvVars(t, MustBeSet, vars...) +} + +func SkipIfEnvVarsNotSet(t *testing.T, vars ...string) map[string]string { + return CheckEnvVars(t, SkipIfMissing, vars...) +} diff --git a/internal/acctest/template.go b/internal/acctest/template.go index 1cca67355..1d48aba58 100644 --- a/internal/acctest/template.go +++ b/internal/acctest/template.go @@ -221,3 +221,16 @@ func (b *CompositionBuilder) MustRender(t testing.TB) string { } return result } + +type TemplateValue struct { + Value string + IsLiteral bool +} + +func Literal(v string) TemplateValue { + return TemplateValue{Value: v, IsLiteral: true} +} + +func Reference(v string) TemplateValue { + return TemplateValue{Value: v, IsLiteral: false} +} diff --git a/internal/common/client.go b/internal/common/client.go index 241a67e31..b47c87f66 100644 --- a/internal/common/client.go +++ b/internal/common/client.go @@ -71,6 +71,17 @@ func WithGenClient(handler CrudHandler) func(context.Context, *schema.ResourceDa } } +// WithGenClientDiag wraps the CRUD handlers and runs with avngen.Client, but returns diag.Diagnostics instead of error +func WithGenClientDiag(f func(context.Context, *schema.ResourceData, avngen.Client) diag.Diagnostics) func( + context.Context, + *schema.ResourceData, + any, +) diag.Diagnostics { + return func(ctx context.Context, d *schema.ResourceData, _ any) diag.Diagnostics { + return f(ctx, d, genClientCache) + } +} + type ClientOpt func(o *clientOpts) type clientOpts struct { token string diff --git a/internal/schemautil/schemautil.go b/internal/schemautil/schemautil.go index 3766e6116..0daca34b7 100644 --- a/internal/schemautil/schemautil.go +++ b/internal/schemautil/schemautil.go @@ -246,6 +246,15 @@ func SplitResourceID4(resourceID string) (string, string, string, string, error) return parts[0], parts[1], parts[2], parts[3], nil } +func SplitResourceID5(resourceID string) (string, string, string, string, string, error) { + parts, err := SplitResourceID(resourceID, 5) + if err != nil { + return "", "", "", "", "", err + } + + return parts[0], parts[1], parts[2], parts[3], parts[4], nil +} + func FlattenToString[T any](a []T) []string { r := make([]string, len(a)) for i, v := range a { diff --git a/internal/sdkprovider/provider/provider.go b/internal/sdkprovider/provider/provider.go index a5541b5eb..60fe7d42d 100644 --- a/internal/sdkprovider/provider/provider.go +++ b/internal/sdkprovider/provider/provider.go @@ -109,14 +109,18 @@ func Provider(version string) (*schema.Provider, error) { "aiven_organization_project": project.DatasourceOrganizationProject(), // vpc - "aiven_aws_privatelink": vpc.DatasourceAWSPrivatelink(), - "aiven_aws_vpc_peering_connection": vpc.DatasourceAWSVPCPeeringConnection(), - "aiven_azure_privatelink": vpc.DatasourceAzurePrivatelink(), - "aiven_azure_vpc_peering_connection": vpc.DatasourceAzureVPCPeeringConnection(), - "aiven_gcp_privatelink": vpc.DatasourceGCPPrivatelink(), - "aiven_gcp_vpc_peering_connection": vpc.DatasourceGCPVPCPeeringConnection(), - "aiven_project_vpc": vpc.DatasourceProjectVPC(), - "aiven_transit_gateway_vpc_attachment": vpc.DatasourceTransitGatewayVPCAttachment(), + "aiven_aws_privatelink": vpc.DatasourceAWSPrivatelink(), + "aiven_aws_vpc_peering_connection": vpc.DatasourceAWSVPCPeeringConnection(), + "aiven_aws_org_vpc_peering_connection": vpc.DatasourceAWSOrgVPCPeeringConnection(), + "aiven_azure_privatelink": vpc.DatasourceAzurePrivatelink(), + "aiven_azure_vpc_peering_connection": vpc.DatasourceAzureVPCPeeringConnection(), + "aiven_azure_org_vpc_peering_connection": vpc.DatasourceAzureOrgVPCPeeringConnection(), + "aiven_gcp_privatelink": vpc.DatasourceGCPPrivatelink(), + "aiven_gcp_vpc_peering_connection": vpc.DatasourceGCPVPCPeeringConnection(), + "aiven_gcp_org_vpc_peering_connection": vpc.DatasourceGCPOrgVPCPeeringConnection(), + "aiven_project_vpc": vpc.DatasourceProjectVPC(), + "aiven_transit_gateway_vpc_attachment": vpc.DatasourceTransitGatewayVPCAttachment(), + "aiven_organization_vpc": vpc.DataSourceOrganizationVPC(), // service integrations "aiven_service_integration": serviceintegration.DatasourceServiceIntegration(), @@ -227,14 +231,18 @@ func Provider(version string) (*schema.Provider, error) { // vpc "aiven_aws_privatelink": vpc.ResourceAWSPrivatelink(), "aiven_aws_vpc_peering_connection": vpc.ResourceAWSVPCPeeringConnection(), + "aiven_aws_org_vpc_peering_connection": vpc.ResourceAWSOrgVPCPeeringConnection(), "aiven_azure_privatelink": vpc.ResourceAzurePrivatelink(), "aiven_azure_privatelink_connection_approval": vpc.ResourceAzurePrivatelinkConnectionApproval(), "aiven_azure_vpc_peering_connection": vpc.ResourceAzureVPCPeeringConnection(), + "aiven_azure_org_vpc_peering_connection": vpc.ResourceAzureOrgVPCPeeringConnection(), "aiven_gcp_privatelink": vpc.ResourceGCPPrivatelink(), "aiven_gcp_privatelink_connection_approval": vpc.ResourceGCPPrivatelinkConnectionApproval(), "aiven_gcp_vpc_peering_connection": vpc.ResourceGCPVPCPeeringConnection(), + "aiven_gcp_org_vpc_peering_connection": vpc.ResourceGCPOrgVPCPeeringConnection(), "aiven_project_vpc": vpc.ResourceProjectVPC(), "aiven_transit_gateway_vpc_attachment": vpc.ResourceTransitGatewayVPCAttachment(), + "aiven_organization_vpc": vpc.ResourceOrganizationVPC(), // service integrations "aiven_service_integration": serviceintegration.ResourceServiceIntegration(), @@ -304,6 +312,10 @@ func Provider(version string) (*schema.Provider, error) { "aiven_flink_jar_application_version", "aiven_flink_jar_application_deployment", "aiven_organization_project", + "aiven_organization_vpc", + "aiven_aws_org_vpc_peering_connection", + "aiven_gcp_org_vpc_peering_connection", + "aiven_azure_org_vpc_peering_connection", } betaDataSources := []string{ @@ -312,6 +324,10 @@ func Provider(version string) (*schema.Provider, error) { "aiven_alloydbomni_database", "aiven_organization_user_list", "aiven_organization_project", + "aiven_organization_vpc", + "aiven_aws_org_vpc_peering_connection", + "aiven_gcp_org_vpc_peering_connection", + "aiven_azure_org_vpc_peering_connection", } missing := append( diff --git a/internal/sdkprovider/service/project/organization_project_test.go b/internal/sdkprovider/service/project/organization_project_test.go index f8505aabf..8197a7d42 100644 --- a/internal/sdkprovider/service/project/organization_project_test.go +++ b/internal/sdkprovider/service/project/organization_project_test.go @@ -33,47 +33,47 @@ func TestAccAivenOrganizationProject(t *testing.T) { // test creating project with all possible fields { Config: fmt.Sprintf(` - resource "aiven_organization" "foo" { - name = "test-acc-org-%[1]s" - } - - resource "aiven_billing_group" "foo" { - name = "test-acc-bg-%[1]s" - } - - resource "aiven_organizational_unit" "foo" { - name = "test-acc-unit-%[1]s" - parent_id = aiven_organization.foo.id - } - - resource "aiven_organization_project" "foo" { - project_id = "%[2]s" - - organization_id = aiven_organization.foo.id - billing_group_id = aiven_billing_group.foo.id - parent_id = aiven_organizational_unit.foo.id - technical_emails = ["john.doe+1@gmail.com", "john.doe+2@gmail.com"] - - tag { - key = "key1" - value = "value1" - } - - tag { - key = "key2" - value = "value2" - } - - tag { - key = "key3" - value = "value3" - } - } - - data "aiven_organization_project" "ds_test" { - project_id = aiven_organization_project.foo.project_id - organization_id = aiven_organization_project.foo.organization_id - } +resource "aiven_organization" "foo" { + name = "test-acc-org-%[1]s" +} + +resource "aiven_billing_group" "foo" { + name = "test-acc-bg-%[1]s" +} + +resource "aiven_organizational_unit" "foo" { + name = "test-acc-unit-%[1]s" + parent_id = aiven_organization.foo.id +} + +resource "aiven_organization_project" "foo" { + project_id = "%[2]s" + + organization_id = aiven_organization.foo.id + billing_group_id = aiven_billing_group.foo.id + parent_id = aiven_organizational_unit.foo.id + technical_emails = ["john.doe+1@gmail.com", "john.doe+2@gmail.com"] + + tag { + key = "key1" + value = "value1" + } + + tag { + key = "key2" + value = "value2" + } + + tag { + key = "key3" + value = "value3" + } +} + +data "aiven_organization_project" "ds_test" { + project_id = aiven_organization_project.foo.project_id + organization_id = aiven_organization_project.foo.organization_id +} `, rName, projectID), Check: resource.ComposeTestCheckFunc( resource.TestCheckResourceAttr(resourceName, "project_id", projectID), @@ -117,44 +117,42 @@ func TestAccAivenOrganizationProject(t *testing.T) { }, // test resource update { - Config: fmt.Sprintf( - ` - resource "aiven_organization" "foo" { - name = "test-acc-org-%[1]s" - } - - resource "aiven_billing_group" "foo" { - name = "test-acc-bg-%[1]s" - } - - resource "aiven_organizational_unit" "foo" { - name = "test-acc-unit-%[1]s" - parent_id = aiven_organization.foo.id - } - - resource "aiven_organization_project" "foo" { - project_id = "%[2]s" #updating project_id without changing other billing_group_id would fail in this scenario - - organization_id = aiven_organization.foo.id # should not change - billing_group_id = aiven_billing_group.foo.id - parent_id = aiven_organizational_unit.foo.id - technical_emails = ["john.doe+3@gmail.com", "john.doe+2@gmail.com", "john.doe+4@gmail.com"] #update emails - - tag { #update tags - key = "key1" - value = "value1" - } - tag { - key = "key2" - value = "value2-new" - } - tag { - key = "key4" - value = "value4" - } - } - `, - rName, + Config: fmt.Sprintf(` +resource "aiven_organization" "foo" { + name = "test-acc-org-%[1]s" +} + +resource "aiven_billing_group" "foo" { + name = "test-acc-bg-%[1]s" +} + +resource "aiven_organizational_unit" "foo" { + name = "test-acc-unit-%[1]s" + parent_id = aiven_organization.foo.id +} + +resource "aiven_organization_project" "foo" { + project_id = "%[2]s" #updating project_id without changing other billing_group_id would fail in this scenario + + organization_id = aiven_organization.foo.id # should not change + billing_group_id = aiven_billing_group.foo.id + parent_id = aiven_organizational_unit.foo.id + technical_emails = ["john.doe+3@gmail.com", "john.doe+2@gmail.com", "john.doe+4@gmail.com"] #update emails + + tag { #update tags + key = "key1" + value = "value1" + } + tag { + key = "key2" + value = "value2-new" + } + tag { + key = "key4" + value = "value4" + } +} + `, rName, projectID, ), ConfigPlanChecks: resource.ConfigPlanChecks{ diff --git a/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection.go new file mode 100644 index 000000000..55815743f --- /dev/null +++ b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection.go @@ -0,0 +1,211 @@ +package vpc + +import ( + "context" + "fmt" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +var aivenAWSOrgVPCPeeringConnectionSchema = map[string]*schema.Schema{ + "organization_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization.", + }, + "organization_vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization VPC.", + }, + "aws_account_id": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("AWS account ID.").ForceNew().Build(), + }, + "aws_vpc_id": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("AWS VPC ID.").ForceNew().Build(), + }, + "aws_vpc_region": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("The AWS region of the peered VPC. For example, `eu-central-1`.").Build(), + }, + "peering_connection_id": { + Computed: true, + Type: schema.TypeString, + Description: userconfig.Desc("The ID of the peering connection.").Build(), + }, + "aws_vpc_peering_connection_id": { + Computed: true, + Type: schema.TypeString, + Description: "The ID of the AWS VPC peering connection.", + }, + "state": { + Computed: true, + Type: schema.TypeString, + Description: userconfig.Desc("State of the peering connection.").PossibleValuesString(organizationvpc.VpcPeeringConnectionStateTypeChoices()...).Build(), + }, +} + +func ResourceAWSOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages an AWS VPC peering connection with an Aiven Organization VPC.", + CreateContext: common.WithGenClientDiag(resourceAWSOrgVPCPeeringConnectionCreate), + ReadContext: common.WithGenClientDiag(resourceAWSOrgVPCPeeringConnectionRead), + DeleteContext: common.WithGenClientDiag(resourceAWSOrgVPCPeeringConnectionDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenAWSOrgVPCPeeringConnectionSchema, + } +} + +func resourceAWSOrgVPCPeeringConnectionCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + vpcID = d.Get("organization_vpc_id").(string) + awsAccountID = d.Get("aws_account_id").(string) + awsVPCId = d.Get("aws_vpc_id").(string) + awsRegion = d.Get("aws_vpc_region").(string) + + req = organizationvpc.OrganizationVpcPeeringConnectionCreateIn{ + PeerRegion: util.ToPtr(awsRegion), + PeerVpc: awsVPCId, + PeerCloudAccount: awsAccountID, + } + ) + + pCon, err := createPeeringConnection(ctx, orgID, vpcID, client, d, req) + if err != nil { + return diag.Errorf("Error creating VPC peering connection: %s", err) + } + + diags := getDiagnosticsFromState(newOrganizationVPCPeeringState(pCon)) + + d.SetId(schemautil.BuildResourceID(orgID, vpcID, awsAccountID, awsVPCId, awsRegion)) + + // in case of an error delete VPC peering connection + if diags.HasError() { + deleteDiags := resourceAzureOrgVPCPeeringConnectionDelete(ctx, d, client) + d.SetId("") // Clear the ID after delete + + return append(diags, deleteDiags...) + } + + return append(diags, resourceAWSOrgVPCPeeringConnectionRead(ctx, d, client)...) +} + +func resourceAWSOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, awsAccountID, awsVpcID, awsRegion, err := schemautil.SplitResourceID5(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d)) + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + pc := lookupAWSPeeringConnection(vpc, awsAccountID, awsVpcID, awsRegion) + if pc == nil { + d.SetId("") // Clear the ID as the resource is not found + + return diag.FromErr(fmt.Errorf("VPC peering connection not found")) + } + + if err = d.Set("organization_id", vpc.OrganizationId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("organization_vpc_id", vpc.OrganizationVpcId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peering_connection_id", *pc.PeeringConnectionId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("aws_account_id", pc.PeerCloudAccount); err != nil { + return diag.FromErr(err) + } + if err = d.Set("aws_vpc_id", pc.PeerVpc); err != nil { + return diag.FromErr(err) + } + if err = d.Set("aws_vpc_region", *pc.PeerRegion); err != nil { + return diag.FromErr(err) + } + if err = d.Set("aws_vpc_peering_connection_id", pc.StateInfo.AwsVpcPeeringConnectionId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("state", string(pc.State)); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceAWSOrgVPCPeeringConnectionDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, awsAccountID, awsVpcID, awsRegion, err := schemautil.SplitResourceID5(d.Id()) + if err != nil { + return diag.FromErr(err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return nil // consider already deleted + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + if err = deletePeeringConnection( + ctx, + orgID, + vpcID, + client, + d, + lookupAWSPeeringConnection(vpc, awsAccountID, awsVpcID, awsRegion), + ); err != nil { + return diag.Errorf("Error deleting Azure Aiven VPC Peering Connection: %s", err) + } + + return nil +} + +func lookupAWSPeeringConnection( + vpc *organizationvpc.OrganizationVpcGetOut, + awsAccountID, awsVpcID, awsRegion string, +) *organizationvpc.OrganizationVpcGetPeeringConnectionOut { + for _, pCon := range vpc.PeeringConnections { + if pCon.PeerCloudAccount == awsAccountID && + pCon.PeerVpc == awsVpcID && + pCon.PeerRegion != nil && + *pCon.PeerRegion == awsRegion && + pCon.PeeringConnectionId != nil { + + return &pCon + } + } + + return nil +} diff --git a/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_data_source.go b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_data_source.go new file mode 100644 index 000000000..5eb38d2e8 --- /dev/null +++ b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_data_source.go @@ -0,0 +1,35 @@ +package vpc + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceAWSOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + ReadContext: common.WithGenClientDiag(dataSourceAWSOrgVPCPeeringConnectionRead), + Description: "Gets information about an AWS VPC peering connection.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenAWSOrgVPCPeeringConnectionSchema, + "organization_id", "organization_vpc_id", "aws_account_id", "aws_vpc_id", "aws_vpc_region"), + } +} + +func dataSourceAWSOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + orgVpcID = d.Get("organization_vpc_id").(string) + awsAccountID = d.Get("aws_account_id").(string) + awsVpcID = d.Get("aws_vpc_id").(string) + awsRegion = d.Get("aws_vpc_region").(string) + ) + + d.SetId(schemautil.BuildResourceID(orgID, orgVpcID, awsAccountID, awsVpcID, awsRegion)) + + return resourceAWSOrgVPCPeeringConnectionRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_test.go b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_test.go new file mode 100644 index 000000000..6cca0577f --- /dev/null +++ b/internal/sdkprovider/service/vpc/aws_org_vpc_peering_connection_test.go @@ -0,0 +1,320 @@ +package vpc_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +const ( + awsOrgVPCPeeringResource = "aiven_aws_org_vpc_peering_connection" +) + +// TestAccAivenAWSOrgVPCPeeringConnection tests the AWS VPC peering connection resource functionality. +// Since creating a real AWS VPC peering connection in CI is not feasible now, this test: +// 1. Sets up a test environment with a fake AWS account ID and VPC ID +// 2. Attempts to create an Aiven VPC and a peering connection +// 3. Validates that the creation fails with the expected error due to invalid AWS credentials +func TestAccAivenAWSOrgVPCPeeringConnection(t *testing.T) { + var ( + orgName = acc.SkipIfEnvVarsNotSet(t, "AIVEN_ORGANIZATION_NAME")["AIVEN_ORGANIZATION_NAME"] + registry = preSetAwsOrgVPCPeeringTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("organization_data", map[string]any{ + "organization_name": orgName}) + } + + awsAccountID = "123456789012" // Fake AWS account ID + awsVpcID = "vpc-1a1a111a111a11a11" // Fake AWS VPC ID + awsRegion = "eu-west-2" + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAWSOrgVPCPeeringResourceDestroy, + Steps: []resource.TestStep{ + { + Config: newComposition(). + Add(organizationVPCResource, map[string]any{ + "resource_name": "test_org_vpc", + "cloud_name": fmt.Sprintf("aws-%s", awsRegion), + "network_cidr": "10.0.0.0/24", + }). + Add(awsOrgVPCPeeringResource, map[string]any{ + "resource_name": "test_org_vpc_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.test_org_vpc.organization_vpc_id"), + "aws_account_id": acc.Literal(awsAccountID), + "aws_vpc_id": acc.Literal(awsVpcID), + "aws_vpc_region": awsRegion, + }).MustRender(t), + ExpectError: regexp.MustCompile(`VPC peering connection cannot be created`), // Expected error due to invalid AWS account ID + }, + }, + }) +} + +// TestAccAivenAWSOrgVPCPeeringConnectionFull tests the complete AWS VPC peering connection workflow +// with real AWS resources. This test: +// 1. Creates an AWS VPC and route table +// 2. Creates an Aiven Organization VPC +// 3. Establishes VPC peering between AWS and Aiven VPCs +// 4. Accepts the peering connection on AWS side +// 5. Sets up routing for the peered VPCs +// +// Note: The test will be skipped in CI environments since it requires real AWS credentials +// and resources. This test is meant for local development and verification for now. +// Prerequisites: +// - Valid AWS credentials with permissions to create/delete VPC resources +// - Proper AWS profile configuration (can be set via AWS_PROFILE env var) +// - Required permissions: VPC creation/deletion, VPC peering, route table management +func TestAccAivenAWSOrgVPCPeeringConnectionFull(t *testing.T) { + var envVars = acc.SkipIfEnvVarsNotSet( + t, + "AIVEN_ORGANIZATION_NAME", + "AWS_ACCESS_KEY_ID", + "AWS_SECRET_ACCESS_KEY", + "AWS_SESSION_TOKEN", + ) + + var ( + orgName = envVars["AIVEN_ORGANIZATION_NAME"] + awsRegion = "eu-central-1" + registry = preSetAwsOrgVPCPeeringTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("aws_provider", map[string]any{ + "aws_region": awsRegion}). + Add("organization_data", map[string]any{ + "organization_name": orgName}) + } + + resourceName = fmt.Sprintf("%s.%s", awsOrgVPCPeeringResource, "test_peering") + + randName = acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + serviceName = fmt.Sprintf("test-acc-%s", randName) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: testAccCheckAWSOrgVPCPeeringResourceDestroy, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "aws": { + Source: "hashicorp/aws", + VersionConstraint: "=5.84.0", + }, + }, + Steps: []resource.TestStep{ + { + Config: newComposition(). + Add("aws_vpc", map[string]any{ + "resource_name": "example", + "cidr_block": "172.16.0.0/16", + "vpc_name": fmt.Sprintf("%s-vpc", serviceName), + }). + Add("aws_route_table", map[string]any{ + "resource_name": "example", + "vpc_id": "aws_vpc.example.id", + "route_table_name": fmt.Sprintf("%s-route-table", serviceName), + }). + Add(organizationVPCResource, map[string]any{ + "resource_name": "example", + "cloud_name": fmt.Sprintf("aws-%s", awsRegion), + "network_cidr": "10.0.0.0/24", + }). + Add(awsOrgVPCPeeringResource, map[string]any{ + "resource_name": "test_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.example.organization_vpc_id"), + "aws_account_id": acc.Reference("aws_vpc.example.owner_id"), + "aws_vpc_id": acc.Reference("aws_vpc.example.id"), + "aws_vpc_region": awsRegion, + }). + Add("peering_datasource", map[string]any{ + "resource_name": "test_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.example.organization_vpc_id"), + "aws_account_id": acc.Reference("aws_vpc.example.owner_id"), + "aws_vpc_id": acc.Reference("aws_vpc.example.id"), + "aws_vpc_region": awsRegion, + }). + Add("aws_vpc_peering_accepter", map[string]any{ + "resource_name": "example", + "peering_connection_id": "aiven_aws_org_vpc_peering_connection.test_peering.aws_vpc_peering_connection_id", + "peering_name": fmt.Sprintf("%s-peering-accepter", serviceName), + }). + Add("aws_route", map[string]any{ + "resource_name": "aiven_vpc_route", + "route_table_id": "aws_route_table.example.id", + "destination_cidr": "aiven_organization_vpc.example.network_cidr", + "peering_connection_id": "aws_vpc_peering_connection_accepter.example.vpc_peering_connection_id", + }).MustRender(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "organization_id", "data.aiven_organization.foo", "id"), + resource.TestCheckResourceAttrPair(resourceName, "organization_vpc_id", "aiven_organization_vpc.example", "organization_vpc_id"), + resource.TestCheckResourceAttrPair(resourceName, "aws_account_id", "aws_vpc.example", "owner_id"), + resource.TestCheckResourceAttrPair(resourceName, "aws_vpc_id", "aws_vpc.example", "id"), + resource.TestCheckResourceAttrPair(resourceName, "organization_id", "data.aiven_organization.foo", "id"), + resource.TestCheckResourceAttr(resourceName, "aws_vpc_region", awsRegion), + resource.TestCheckResourceAttrSet(resourceName, "peering_connection_id"), + resource.TestCheckResourceAttrSet(resourceName, "aws_vpc_peering_connection_id"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + + resource.TestCheckResourceAttrPair(fmt.Sprintf("data.%s.%s", awsOrgVPCPeeringResource, "test_peering"), "organization_id", "data.aiven_organization.foo", "id"), + resource.TestCheckResourceAttrPair(fmt.Sprintf("data.%s.%s", awsOrgVPCPeeringResource, "test_peering"), "organization_vpc_id", "aiven_organization_vpc.example", "organization_vpc_id"), + ), + }, + { + // importing the resource + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckAWSOrgVPCPeeringResourceDestroy(s *terraform.State) error { + ctx := context.Background() + + c, err := acc.GetTestGenAivenClient() + if err != nil { + return fmt.Errorf("error initializing Aiven client: %w", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != awsOrgVPCPeeringResource { + continue + } + + orgID, orgVpcID, awsAccountID, awsVpcID, awsRegion, err := schemautil.SplitResourceID5(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error splitting resource with ID: %q - %w", rs.Primary.ID, err) + } + + orgVPC, err := c.OrganizationVpcGet(ctx, orgID, orgVpcID) + if common.IsCritical(err) { + return fmt.Errorf("error fetching VPC (%q): %w", orgVpcID, err) + } + + if orgVPC == nil { + return nil // Peering connection was deleted with the VPC + } + + var pc *organizationvpc.OrganizationVpcGetPeeringConnectionOut + for _, pCon := range orgVPC.PeeringConnections { + if pCon.PeerCloudAccount == awsAccountID && + pCon.PeerVpc == awsVpcID && + pCon.PeerRegion != nil && + *pCon.PeerRegion == awsRegion && + pCon.PeeringConnectionId != nil { + pc = &pCon + + break + } + } + + if pc != nil { + return fmt.Errorf("peering connection %q still exists", *pc.PeeringConnectionId) + } + } + + return nil +} + +func preSetAwsOrgVPCPeeringTemplates(t *testing.T) *acc.TemplateRegistry { + t.Helper() + + registry := acc.NewTemplateRegistry(awsOrgVPCPeeringResource) + + registry.MustAddTemplate(t, "organization_data", ` +data "aiven_organization" "foo" { + name = "{{ .organization_name }}" +}`) + + registry.MustAddTemplate(t, "aws_provider", ` +provider "aws" { + region = "{{ .aws_region }}" +}`) + + registry.MustAddTemplate(t, organizationVPCResource, ` +resource "aiven_organization_vpc" "{{ .resource_name }}" { + organization_id = data.aiven_organization.foo.id + cloud_name = "{{ .cloud_name }}" + network_cidr = "{{ .network_cidr }}" +}`) + + registry.MustAddTemplate(t, awsOrgVPCPeeringResource, ` +resource "aiven_aws_org_vpc_peering_connection" "{{ .resource_name }}" { + organization_id = {{ if .organization_id.IsLiteral }}"{{ .organization_id.Value }}"{{ else }}{{ .organization_id.Value }}{{ end }} + organization_vpc_id = {{ if .organization_vpc_id.IsLiteral }}"{{ .organization_vpc_id.Value }}"{{ else }}{{ .organization_vpc_id.Value }}{{ end }} + aws_account_id = {{ if .aws_account_id.IsLiteral }}"{{ .aws_account_id.Value }}"{{ else }}{{ .aws_account_id.Value }}{{ end }} + aws_vpc_id = {{ if .aws_vpc_id.IsLiteral }}"{{ .aws_vpc_id.Value }}"{{ else }}{{ .aws_vpc_id.Value }}{{ end }} + aws_vpc_region = "{{ .aws_vpc_region }}" +}`) + + registry.MustAddTemplate(t, "peering_datasource", ` +data "aiven_aws_org_vpc_peering_connection" "{{ .resource_name }}" { + organization_id = {{ if .organization_id.IsLiteral }}"{{ .organization_id.Value }}"{{ else }}{{ .organization_id.Value }}{{ end }} + organization_vpc_id = {{ if .organization_vpc_id.IsLiteral }}"{{ .organization_vpc_id.Value }}"{{ else }}{{ .organization_vpc_id.Value }}{{ end }} + aws_account_id = {{ if .aws_account_id.IsLiteral }}"{{ .aws_account_id.Value }}"{{ else }}{{ .aws_account_id.Value }}{{ end }} + aws_vpc_id = {{ if .aws_vpc_id.IsLiteral }}"{{ .aws_vpc_id.Value }}"{{ else }}{{ .aws_vpc_id.Value }}{{ end }} + aws_vpc_region = "{{ .aws_vpc_region }}" +}`) + + // AWS VPC Resource + registry.MustAddTemplate(t, "aws_vpc", ` +resource "aws_vpc" "{{ .resource_name }}" { + cidr_block = "{{ .cidr_block }}" + enable_dns_hostnames = true + enable_dns_support = true + + tags = { + Name = "{{ .vpc_name }}" + } +}`) + + // AWS Route Table + registry.MustAddTemplate(t, "aws_route_table", ` +resource "aws_route_table" "{{ .resource_name }}" { + vpc_id = {{ .vpc_id }} + + tags = { + Name = "{{ .route_table_name }}" + } +}`) + + // AWS VPC Peering Connection Accepter + registry.MustAddTemplate(t, "aws_vpc_peering_accepter", ` +resource "aws_vpc_peering_connection_accepter" "{{ .resource_name }}" { + vpc_peering_connection_id = {{ .peering_connection_id }} + auto_accept = true + + tags = { + Name = "{{ .peering_name }}" + } +}`) + + // AWS Route for Aiven VPC CIDR + registry.MustAddTemplate(t, "aws_route", ` +resource "aws_route" "{{ .resource_name }}" { + route_table_id = {{ .route_table_id }} + destination_cidr_block = {{ .destination_cidr }} + vpc_peering_connection_id = {{ .peering_connection_id }} +}`) + + return registry +} diff --git a/internal/sdkprovider/service/vpc/aws_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/aws_vpc_peering_connection.go index 592a2c913..7163a5c22 100644 --- a/internal/sdkprovider/service/vpc/aws_vpc_peering_connection.go +++ b/internal/sdkprovider/service/vpc/aws_vpc_peering_connection.go @@ -159,7 +159,7 @@ func resourceAWSVPCPeeringConnectionCreate(ctx context.Context, d *schema.Resour } pc = res.(*aiven.VPCPeeringConnection) - diags := getDiagnosticsFromState(pc) + diags := getDiagnosticsFromState(newAivenVPCPeeringState(pc)) d.SetId(schemautil.BuildResourceID(projectName, vpcID, pc.PeerCloudAccount, pc.PeerVPC, *pc.PeerRegion)) diff --git a/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection.go new file mode 100644 index 000000000..d58fc6f64 --- /dev/null +++ b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection.go @@ -0,0 +1,223 @@ +package vpc + +import ( + "context" + "fmt" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/plugin/util" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +var aivenAzureOrgVPCPeeringConnectionSchema = map[string]*schema.Schema{ + "organization_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization.", + }, + "organization_vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization VPC.", + }, + "azure_subscription_id": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("The ID of the Azure subscription in UUID4 format.").ForceNew().Build(), + }, + "vnet_name": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("The name of the Azure VNet.").ForceNew().Build(), + }, + "peer_resource_group": { + Required: true, + ForceNew: true, + Type: schema.TypeString, + Description: userconfig.Desc("The name of the Azure resource group associated with the VNet.").ForceNew().Build(), + }, + "peer_azure_app_id": { + Required: true, + ForceNew: true, + Type: schema.TypeString, + Description: userconfig.Desc("The ID of the Azure app that is allowed to create a peering to the Azure Virtual Network (VNet) in UUID4 format.").ForceNew().Build(), + }, + "peer_azure_tenant_id": { + Required: true, + ForceNew: true, + Type: schema.TypeString, + Description: userconfig.Desc("The Azure tenant ID in UUID4 format.").ForceNew().Build(), + }, + "state": { + Computed: true, + Type: schema.TypeString, + Description: "State of the peering connection", + }, + "peering_connection_id": { + Computed: true, + Type: schema.TypeString, + Description: "The ID of the cloud provider for the peering connection.", + }, +} + +func ResourceAzureOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages an Azure VPC peering connection with an Aiven VPC.", + CreateContext: common.WithGenClientDiag(resourceAzureOrgVPCPeeringConnectionCreate), + ReadContext: common.WithGenClientDiag(resourceAzureOrgVPCPeeringConnectionRead), + DeleteContext: common.WithGenClientDiag(resourceAzureOrgVPCPeeringConnectionDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenAzureOrgVPCPeeringConnectionSchema, + } +} + +func resourceAzureOrgVPCPeeringConnectionCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + vpcID = d.Get("organization_vpc_id").(string) + azureSubscriptionID = d.Get("azure_subscription_id").(string) + vnetName = d.Get("vnet_name").(string) + appID = d.Get("peer_azure_app_id").(string) + tenantID = d.Get("peer_azure_tenant_id").(string) + resourceGroup = d.Get("peer_resource_group").(string) + + req = organizationvpc.OrganizationVpcPeeringConnectionCreateIn{ + PeerAzureAppId: util.ToPtr(appID), + PeerAzureTenantId: util.ToPtr(tenantID), + PeerCloudAccount: azureSubscriptionID, + PeerResourceGroup: util.ToPtr(resourceGroup), + PeerVpc: vnetName, + } + ) + + pCon, err := createPeeringConnection(ctx, orgID, vpcID, client, d, req) + if err != nil { + return diag.Errorf("Error creating VPC peering connection: %s", err) + } + + diags := getDiagnosticsFromState(newOrganizationVPCPeeringState(pCon)) + + d.SetId(schemautil.BuildResourceID(orgID, vpcID, pCon.PeerCloudAccount, pCon.PeerVpc, pCon.PeerResourceGroup)) + + // in case of an error delete VPC peering connection + if diags.HasError() { + deleteDiags := resourceAzureOrgVPCPeeringConnectionDelete(ctx, d, client) + d.SetId("") // Clear the ID after delete + + return append(diags, deleteDiags...) + } + + return append(diags, resourceAzureOrgVPCPeeringConnectionRead(ctx, d, client)...) +} + +func resourceAzureOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, cloudAccount, vnetName, resourceGroup, err := schemautil.SplitResourceID5(d.Id()) + if err != nil { + return diag.Errorf("error parsing Azure peering VPC ID: %s", err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d)) + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + pc := lookupAzurePeeringConnection(vpc, cloudAccount, vnetName, resourceGroup) + if pc == nil { + d.SetId("") // Clear the ID as the resource is not found + + return diag.FromErr(fmt.Errorf("VPC peering connection not found")) + } + + if err = d.Set("organization_id", orgID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("organization_vpc_id", vpcID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("azure_subscription_id", pc.PeerCloudAccount); err != nil { + return diag.FromErr(err) + } + if err = d.Set("vnet_name", pc.PeerVpc); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peer_azure_app_id", pc.PeerAzureAppId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peer_azure_tenant_id", pc.PeerAzureTenantId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peer_resource_group", pc.PeerResourceGroup); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peering_connection_id", *pc.PeeringConnectionId); err != nil { + return diag.FromErr(err) + } + if err = d.Set("state", string(pc.State)); err != nil { + return diag.FromErr(err) + } + + return nil +} + +func resourceAzureOrgVPCPeeringConnectionDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, cloudAccount, vnetName, resourceGroup, err := schemautil.SplitResourceID5(d.Id()) + if err != nil { + return diag.Errorf("error parsing Azure peering VPC ID: %s", err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return nil // consider already deleted + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + if err = deletePeeringConnection( + ctx, + orgID, + vpcID, + client, + d, + lookupAzurePeeringConnection(vpc, cloudAccount, vnetName, resourceGroup), + ); err != nil { + return diag.Errorf("Error deleting Azure Aiven VPC Peering Connection: %s", err) + } + + return nil +} + +func lookupAzurePeeringConnection( + vpc *organizationvpc.OrganizationVpcGetOut, + peerCloudAccount, peerVPC, resourceGroup string, +) *organizationvpc.OrganizationVpcGetPeeringConnectionOut { + for _, pc := range vpc.PeeringConnections { + if pc.PeerCloudAccount == peerCloudAccount && + pc.PeerVpc == peerVPC && + pc.PeerResourceGroup == resourceGroup { + + return &pc + } + } + + return nil +} diff --git a/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_data_source.go b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_data_source.go new file mode 100644 index 000000000..778fe1a38 --- /dev/null +++ b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_data_source.go @@ -0,0 +1,36 @@ +package vpc + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceAzureOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + ReadContext: common.WithGenClientDiag(datasourceAzureOrgVPCPeeringConnectionRead), + Description: "Gets information about about an Azure VPC peering connection.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenAzureOrgVPCPeeringConnectionSchema, + "organization_id", "organization_vpc_id", "azure_subscription_id", + "peer_resource_group", "vnet_name"), + } +} + +func datasourceAzureOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + vpcID = d.Get("organization_vpc_id").(string) + subID = d.Get("azure_subscription_id").(string) + vnet = d.Get("vnet_name").(string) + rg = d.Get("peer_resource_group").(string) + ) + + d.SetId(schemautil.BuildResourceID(orgID, vpcID, subID, vnet, rg)) + + return resourceAzureVPCPeeringConnectionRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_test.go b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_test.go new file mode 100644 index 000000000..2e5fc341e --- /dev/null +++ b/internal/sdkprovider/service/vpc/azure_org_vpc_peering_connection_test.go @@ -0,0 +1,139 @@ +package vpc_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +const ( + azureOrgVPCPeeringResource = "aiven_azure_org_vpc_peering_connection" +) + +func TestAccAivenAzureOrgVPCPeeringConnection(t *testing.T) { + var ( + orgName = acc.SkipIfEnvVarsNotSet(t, "AIVEN_ORGANIZATION_NAME")["AIVEN_ORGANIZATION_NAME"] + registry = preSetAzureOrgVPCPeeringTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("organization_data", map[string]any{ + "organization_name": orgName}) + } + + subscriptionID = "00000000-0000-0000-0000-000000000000" + vnetName = "test-vnet" + resourceGroup = "test-rg" + appID = "00000000-0000-0000-0000-000000000000" + tenantID = "00000000-0000-0000-0000-000000000000" + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckAzureOrgVPCPeeringResourceDestroy, + Steps: []resource.TestStep{ + { + Config: newComposition(). + Add(organizationVPCResource, map[string]any{ + "resource_name": "test_org_vpc", + "cloud_name": "azure-germany-westcentral", + "network_cidr": "10.0.0.0/24", + }). + Add(azureOrgVPCPeeringResource, map[string]any{ + "resource_name": "test_org_vpc_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.test_org_vpc.organization_vpc_id"), + "azure_subscription_id": acc.Literal(subscriptionID), + "vnet_name": acc.Literal(vnetName), + "peer_resource_group": acc.Literal(resourceGroup), + "peer_azure_app_id": acc.Literal(appID), + "peer_azure_tenant_id": acc.Literal(tenantID), + }).MustRender(t), + ExpectError: regexp.MustCompile(`peer_azure_app_id '.*' does not refer to a valid application object`), // Azure app ID is invalid + }, + }, + }) +} + +func testAccCheckAzureOrgVPCPeeringResourceDestroy(s *terraform.State) error { + ctx := context.Background() + + c, err := acc.GetTestGenAivenClient() + if err != nil { + return fmt.Errorf("error initializing Aiven client: %w", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != awsOrgVPCPeeringResource { + continue + } + + orgID, vpcID, cloudAccount, vnetName, resourceGroup, err := schemautil.SplitResourceID5(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error splitting resource with ID: %q - %w", rs.Primary.ID, err) + } + + orgVPC, err := c.OrganizationVpcGet(ctx, orgID, vpcID) + if common.IsCritical(err) { + return fmt.Errorf("error fetching VPC (%q): %w", vpcID, err) + } + + if orgVPC == nil { + return nil // Peering connection was deleted with the VPC + } + + var pc *organizationvpc.OrganizationVpcGetPeeringConnectionOut + for _, pCon := range orgVPC.PeeringConnections { + if pCon.PeerCloudAccount == cloudAccount && pCon.PeerVpc == vnetName && pCon.PeerResourceGroup == resourceGroup { + pc = &pCon + break + } + } + + if pc != nil { + return fmt.Errorf("peering connection %q still exists", *pc.PeeringConnectionId) + } + } + + return nil +} + +func preSetAzureOrgVPCPeeringTemplates(t *testing.T) *acc.TemplateRegistry { + t.Helper() + + registry := acc.NewTemplateRegistry(azureOrgVPCPeeringResource) + + registry.MustAddTemplate(t, "organization_data", ` +data "aiven_organization" "foo" { + name = "{{ .organization_name }}" +}`) + + registry.MustAddTemplate(t, organizationVPCResource, ` +resource "aiven_organization_vpc" "{{ .resource_name }}" { + organization_id = data.aiven_organization.foo.id + cloud_name = "{{ .cloud_name }}" + network_cidr = "{{ .network_cidr }}" +}`) + + registry.MustAddTemplate(t, azureOrgVPCPeeringResource, ` +resource "aiven_azure_org_vpc_peering_connection" "{{ .resource_name }}" { + organization_id = {{ if .organization_id.IsLiteral }}"{{ .organization_id.Value }}"{{ else }}{{ .organization_id.Value }}{{ end }} + organization_vpc_id = {{ if .organization_vpc_id.IsLiteral }}"{{ .organization_vpc_id.Value }}"{{ else }}{{ .organization_vpc_id.Value }}{{ end }} + azure_subscription_id = {{ if .azure_subscription_id.IsLiteral }}"{{ .azure_subscription_id.Value }}"{{ else }}{{ .azure_subscription_id.Value }}{{ end }} + vnet_name = {{ if .vnet_name.IsLiteral }}"{{ .vnet_name.Value }}"{{ else }}{{ .vnet_name.Value }}{{ end }} + peer_resource_group = {{ if .peer_resource_group.IsLiteral }}"{{ .peer_resource_group.Value }}"{{ else }}{{ .peer_resource_group.Value }}{{ end }} + peer_azure_app_id = {{ if .peer_azure_app_id.IsLiteral }}"{{ .peer_azure_app_id.Value }}"{{ else }}{{ .peer_azure_app_id.Value }}{{ end }} + peer_azure_tenant_id = {{ if .peer_azure_tenant_id.IsLiteral }}"{{ .peer_azure_tenant_id.Value }}"{{ else }}{{ .peer_azure_tenant_id.Value }}{{ end }} +}`) + + return registry +} diff --git a/internal/sdkprovider/service/vpc/azure_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/azure_vpc_peering_connection.go index 6135b4422..24d99b11a 100644 --- a/internal/sdkprovider/service/vpc/azure_vpc_peering_connection.go +++ b/internal/sdkprovider/service/vpc/azure_vpc_peering_connection.go @@ -174,7 +174,7 @@ func resourceAzureVPCPeeringConnectionCreate(ctx context.Context, d *schema.Reso } pc = res.(*aiven.VPCPeeringConnection) - diags := getDiagnosticsFromState(pc) + diags := getDiagnosticsFromState(newAivenVPCPeeringState(pc)) d.SetId(schemautil.BuildResourceID(projectName, vpcID, pc.PeerCloudAccount, pc.PeerVPC)) diff --git a/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection.go new file mode 100644 index 000000000..6ec7a574e --- /dev/null +++ b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection.go @@ -0,0 +1,197 @@ +package vpc + +import ( + "context" + "fmt" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +var aivenGCPOrgVPCPeeringConnectionSchema = map[string]*schema.Schema{ + "organization_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization.", + }, + "organization_vpc_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "Identifier of the organization VPC.", + }, + "gcp_project_id": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("Google Cloud project ID.").ForceNew().Build(), + }, + "peer_vpc": { + ForceNew: true, + Required: true, + Type: schema.TypeString, + Description: userconfig.Desc("Google Cloud VPC network name.").ForceNew().Build(), + }, + "state": { + Computed: true, + Type: schema.TypeString, + Description: "State of the peering connection.", + }, + "self_link": { + Computed: true, + Type: schema.TypeString, + Description: "Computed Google Cloud network peering link.", + }, +} + +func ResourceGCPOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages a Google Cloud VPC peering connection.", + CreateContext: common.WithGenClientDiag(resourceGCPOrgVPCPeeringConnectionCreate), + ReadContext: common.WithGenClientDiag(resourceGCPOrgVPCPeeringConnectionRead), + DeleteContext: common.WithGenClientDiag(resourceGCPOrgVPCPeeringConnectionDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenGCPOrgVPCPeeringConnectionSchema, + } +} + +func resourceGCPOrgVPCPeeringConnectionCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + vpcID = d.Get("organization_vpc_id").(string) + gcpProjectID = d.Get("gcp_project_id").(string) + peerVPC = d.Get("peer_vpc").(string) + + req = organizationvpc.OrganizationVpcPeeringConnectionCreateIn{ + PeerCloudAccount: gcpProjectID, + PeerVpc: peerVPC, + } + ) + + pCon, err := createPeeringConnection(ctx, orgID, vpcID, client, d, req) + if err != nil { + return diag.Errorf("Error creating VPC peering connection: %s", err) + } + + diags := getDiagnosticsFromState(newOrganizationVPCPeeringState(pCon)) + + d.SetId(schemautil.BuildResourceID(orgID, vpcID, pCon.PeerCloudAccount, pCon.PeerVpc)) + + // in case of an error delete VPC peering connection + if diags.HasError() { + deleteDiags := resourceAzureOrgVPCPeeringConnectionDelete(ctx, d, client) + d.SetId("") // Clear the ID after delete + + return append(diags, deleteDiags...) + } + + return append(diags, resourceGCPOrgVPCPeeringConnectionRead(ctx, d, client)...) +} + +func resourceGCPOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, cloudAcc, peerVPC, err := schemautil.SplitResourceID4(d.Id()) + if err != nil { + return diag.Errorf("error parsing GCP peering VPC ID: %s", err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return diag.FromErr(schemautil.ResourceReadHandleNotFound(err, d)) + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + pCon := lookupGCPPeeringConnection(vpc, cloudAcc, peerVPC) + if pCon == nil { + d.SetId("") // Set ID to clear the state + + return diag.FromErr(fmt.Errorf("VPC peering connection not found")) + } + + if err = d.Set("organization_id", orgID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("organization_vpc_id", vpcID); err != nil { + return diag.FromErr(err) + } + if err = d.Set("gcp_project_id", pCon.PeerCloudAccount); err != nil { + return diag.FromErr(err) + } + if err = d.Set("peer_vpc", pCon.PeerVpc); err != nil { + return diag.FromErr(err) + } + if err = d.Set("state", string(pCon.State)); err != nil { + return diag.FromErr(err) + } + + // Set self_link, so it can be used for google_compute_network_peering if needed + if pCon.StateInfo.ToProjectId != nil && pCon.StateInfo.ToVpcNetwork != nil { + selfLink := fmt.Sprintf("%s/projects/%s/global/networks/%s", + _gcpAPI, + *pCon.StateInfo.ToProjectId, + *pCon.StateInfo.ToVpcNetwork) + + if err = d.Set("self_link", selfLink); err != nil { + return diag.FromErr(err) + } + } + + return nil +} + +func resourceGCPOrgVPCPeeringConnectionDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + orgID, vpcID, cloudAcc, peerVPC, err := schemautil.SplitResourceID4(d.Id()) + if err != nil { + return diag.Errorf("error parsing GCP peering VPC ID: %s", err) + } + + vpc, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + return nil // consider already deleted + } + + return diag.Errorf("failed to get VPC with ID %q: %s", vpcID, err) + } + + if err = deletePeeringConnection( + ctx, + orgID, + vpcID, + client, + d, + lookupGCPPeeringConnection(vpc, cloudAcc, peerVPC), + ); err != nil { + return diag.Errorf("Error deleting GCP Aiven VPC Peering Connection: %s", err) + } + + return nil +} + +func lookupGCPPeeringConnection( + vpc *organizationvpc.OrganizationVpcGetOut, + cloudAcc, peerVPC string, +) *organizationvpc.OrganizationVpcGetPeeringConnectionOut { + var pCon *organizationvpc.OrganizationVpcGetPeeringConnectionOut + for _, p := range vpc.PeeringConnections { + if p.PeerCloudAccount == cloudAcc && p.PeerVpc == peerVPC && p.PeeringConnectionId != nil { + pCon = &p + break + } + } + + return pCon +} diff --git a/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_data_source.go b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_data_source.go new file mode 100644 index 000000000..2a56a0b0a --- /dev/null +++ b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_data_source.go @@ -0,0 +1,34 @@ +package vpc + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DatasourceGCPOrgVPCPeeringConnection() *schema.Resource { + return &schema.Resource{ + ReadContext: common.WithGenClientDiag(datasourceGCPOrgVPCPeeringConnectionRead), + Description: "The GCP VPC Peering Connection data source provides information about the existing Aiven VPC Peering Connection.", + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenGCPOrgVPCPeeringConnectionSchema, + "organization_id", "organization_vpc_id", "gcp_project_id", "peer_vpc"), + } +} + +func datasourceGCPOrgVPCPeeringConnectionRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) diag.Diagnostics { + var ( + orgID = d.Get("organization_id").(string) + vpcID = d.Get("organization_vpc_id").(string) + gcpProjectID = d.Get("gcp_project_id").(string) + peerVPC = d.Get("peer_vpc").(string) + ) + + d.SetId(schemautil.BuildResourceID(orgID, vpcID, gcpProjectID, peerVPC)) + + return resourceGCPOrgVPCPeeringConnectionRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_test.go b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_test.go new file mode 100644 index 000000000..760e8265f --- /dev/null +++ b/internal/sdkprovider/service/vpc/gcp_org_vpc_peering_connection_test.go @@ -0,0 +1,233 @@ +package vpc_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +const ( + gcpOrgVPCPeeringResource = "aiven_gcp_org_vpc_peering_connection" +) + +// TestAccAivenGCPOrgVPCPeeringConnection tests the GCP VPC peering connection resource functionality. +// Since creating a real GCP VPC peering connection in CI requires valid GCP credentials, this test: +// 1. Sets up a test environment with an invalid GCP project ID and VPC ID +// 2. Attempts to create an Aiven VPC and a peering connection +// 3. Validates that the creation fails with the expected error due to invalid GCP project ID +func TestAccAivenGCPOrgVPCPeeringConnection(t *testing.T) { + var ( + orgName = acc.SkipIfEnvVarsNotSet(t, "AIVEN_ORGANIZATION_NAME")["AIVEN_ORGANIZATION_NAME"] + registry = preSetGcpOrgVPCPeeringTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("organization_data", map[string]any{ + "organization_name": orgName}) + } + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckGCPOrgVPCPeeringResourceDestroy, + Steps: []resource.TestStep{ + { + Config: newComposition(). + Add(organizationVPCResource, map[string]any{ + "resource_name": "test_org_vpc", + "cloud_name": "google-europe-west10", + "network_cidr": "10.0.0.0/24", + }). + Add(gcpOrgVPCPeeringResource, map[string]any{ + "resource_name": "test_org_vpc_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.test_org_vpc.organization_vpc_id"), + "gcp_project_id": acc.Literal("wrong_project_id"), + "peer_vpc": acc.Literal("wrong_peer_vpc"), + }).MustRender(t), + ExpectError: regexp.MustCompile(`peer_cloud_account must be a valid GCP project ID`), // Expected error due to invalid GCP arguments + }, + }, + }) +} + +// TestAccAivenGCPOrgVPCPeeringConnectionFull tests the complete GCP VPC peering connection workflow +// with real GCP resources. This test: +// 1. Creates a GCP VPC and route +// 2. Creates an Aiven Organization VPC +// 3. Establishes VPC peering between GCP and Aiven VPCs +// 4. Sets up routing for the peered VPCs +// +// Note: The test will be skipped in CI environments since it requires real GCP credentials +// and resources. This test is meant for local development and verification for now. +// Prerequisites: +// - Valid GCP credentials with permissions to create/delete VPC resources +// - GCP project with VPC API enabled +// - Required permissions: VPC creation/deletion, VPC peering, route management +func TestAccAivenGCPOrgVPCPeeringConnectionFull(t *testing.T) { + var envVars = acc.SkipIfEnvVarsNotSet( + t, + "AIVEN_ORGANIZATION_NAME", + "GOOGLE_PROJECT", + ) + + var ( + orgName = envVars["AIVEN_ORGANIZATION_NAME"] + gcpProject = envVars["GOOGLE_PROJECT"] + gcpRegion = "europe-west10" + registry = preSetGcpOrgVPCPeeringTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("gcp_provider", map[string]any{ + "gcp_project": gcpProject, + "gcp_region": gcpRegion}). + Add("organization_data", map[string]any{ + "organization_name": orgName}) + } + + resourceName = fmt.Sprintf("%s.%s", gcpOrgVPCPeeringResource, "test_peering") + + randName = acctest.RandStringFromCharSet(10, acctest.CharSetAlphaNum) + serviceName = fmt.Sprintf("test-acc-%s", randName) + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + CheckDestroy: testAccCheckGCPOrgVPCPeeringResourceDestroy, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + ExternalProviders: map[string]resource.ExternalProvider{ + "google": { + Source: "hashicorp/google", + VersionConstraint: "=6.15.0", + }, + }, + Steps: []resource.TestStep{ + { + Config: newComposition(). + Add("google_vpc", map[string]any{ + "resource_name": "example", + "vpc_name": fmt.Sprintf("%s-vpc", serviceName), + }). + Add(organizationVPCResource, map[string]any{ + "resource_name": "example", + "cloud_name": fmt.Sprintf("google-%s", gcpRegion), + "network_cidr": "10.0.0.0/24", + }). + Add(gcpOrgVPCPeeringResource, map[string]any{ + "resource_name": "test_peering", + "organization_id": acc.Reference("data.aiven_organization.foo.id"), + "organization_vpc_id": acc.Reference("aiven_organization_vpc.example.organization_vpc_id"), + "gcp_project_id": acc.Literal(gcpProject), + "peer_vpc": acc.Reference("google_compute_network.example.name"), + }).MustRender(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(resourceName, "organization_id", "data.aiven_organization.foo", "id"), + resource.TestCheckResourceAttrPair(resourceName, "organization_vpc_id", "aiven_organization_vpc.example", "organization_vpc_id"), + resource.TestCheckResourceAttr(resourceName, "gcp_project_id", gcpProject), + resource.TestCheckResourceAttrPair(resourceName, "peer_vpc", "google_compute_network.example", "name"), + resource.TestCheckResourceAttrSet(resourceName, "state"), + resource.TestCheckResourceAttrSet(resourceName, "self_link"), + ), + }, + { + // importing the resource + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + }, + }) +} + +func testAccCheckGCPOrgVPCPeeringResourceDestroy(s *terraform.State) error { + ctx := context.Background() + + c, err := acc.GetTestGenAivenClient() + if err != nil { + return fmt.Errorf("error initializing Aiven client: %w", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != gcpOrgVPCPeeringResource { + continue + } + + orgID, vpcID, cloudAcc, peerVPC, err := schemautil.SplitResourceID4(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error parsing GCP peering VPC ID: %q. %w", rs.Primary.ID, err) + } + + orgVPC, err := c.OrganizationVpcGet(ctx, orgID, vpcID) + if common.IsCritical(err) { + return fmt.Errorf("error fetching VPC (%q): %w", orgID, err) + } + + if orgVPC == nil { + return nil // Peering connection was deleted with the VPC + } + + var pc *organizationvpc.OrganizationVpcGetPeeringConnectionOut + for _, p := range orgVPC.PeeringConnections { + if p.PeerCloudAccount == cloudAcc && p.PeerVpc == peerVPC && p.PeeringConnectionId != nil { + pc = &p + break + } + } + + if pc != nil { + return fmt.Errorf("peering connection %q still exists", *pc.PeeringConnectionId) + } + } + + return nil +} + +func preSetGcpOrgVPCPeeringTemplates(t *testing.T) *acc.TemplateRegistry { + t.Helper() + + registry := acc.NewTemplateRegistry(gcpOrgVPCPeeringResource) + + registry.MustAddTemplate(t, "organization_data", ` +data "aiven_organization" "foo" { + name = "{{ .organization_name }}" +}`) + + registry.MustAddTemplate(t, "gcp_provider", ` +provider "google" { + project = "{{ .gcp_project }}" + region = "{{ .gcp_region }}" +}`) + + registry.MustAddTemplate(t, organizationVPCResource, ` +resource "aiven_organization_vpc" "{{ .resource_name }}" { + organization_id = data.aiven_organization.foo.id + cloud_name = "{{ .cloud_name }}" + network_cidr = "{{ .network_cidr }}" +}`) + + registry.MustAddTemplate(t, gcpOrgVPCPeeringResource, ` +resource "aiven_gcp_org_vpc_peering_connection" "{{ .resource_name }}" { + organization_id = {{ if .organization_id.IsLiteral }}"{{ .organization_id.Value }}"{{ else }}{{ .organization_id.Value }}{{ end }} + organization_vpc_id = {{ if .organization_vpc_id.IsLiteral }}"{{ .organization_vpc_id.Value }}"{{ else }}{{ .organization_vpc_id.Value }}{{ end }} + gcp_project_id = {{ if .gcp_project_id.IsLiteral }}"{{ .gcp_project_id.Value }}"{{ else }}{{ .gcp_project_id.Value }}{{ end }} + peer_vpc = {{ if .peer_vpc.IsLiteral }}"{{ .peer_vpc.Value }}"{{ else }}{{ .peer_vpc.Value }}{{ end }} +}`) + + registry.MustAddTemplate(t, "google_vpc", ` +resource "google_compute_network" "{{ .resource_name }}" { + name = "{{ .vpc_name }}" + auto_create_subnetworks = false +}`) + + return registry +} diff --git a/internal/sdkprovider/service/vpc/gcp_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/gcp_vpc_peering_connection.go index c9bf6d80f..416065917 100644 --- a/internal/sdkprovider/service/vpc/gcp_vpc_peering_connection.go +++ b/internal/sdkprovider/service/vpc/gcp_vpc_peering_connection.go @@ -147,7 +147,7 @@ func resourceGCPVPCPeeringConnectionCreate(ctx context.Context, d *schema.Resour } pc = res.(*aiven.VPCPeeringConnection) - diags := getDiagnosticsFromState(pc) + diags := getDiagnosticsFromState(newAivenVPCPeeringState(pc)) d.SetId(schemautil.BuildResourceID(projectName, vpcID, pc.PeerCloudAccount, pc.PeerVPC)) diff --git a/internal/sdkprovider/service/vpc/org_vpc_peering_connection.go b/internal/sdkprovider/service/vpc/org_vpc_peering_connection.go new file mode 100644 index 000000000..26f3ba79b --- /dev/null +++ b/internal/sdkprovider/service/vpc/org_vpc_peering_connection.go @@ -0,0 +1,149 @@ +package vpc + +import ( + "context" + "fmt" + "time" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" +) + +var ( + pollInterval = common.DefaultStateChangeMinTimeout + pollDelay = 1 * time.Second +) + +func createPeeringConnection( + ctx context.Context, + orgID, orgVpcID string, + client avngen.Client, + d *schema.ResourceData, + req organizationvpc.OrganizationVpcPeeringConnectionCreateIn, +) (*organizationvpc.OrganizationVpcGetPeeringConnectionOut, error) { + pc, err := client.OrganizationVpcPeeringConnectionCreate( + ctx, + orgID, + orgVpcID, + &req, + ) + if err != nil { + return nil, fmt.Errorf("error creating VPC peering connection: %w", err) + } + + if pc.PeeringConnectionId == nil { + return nil, fmt.Errorf("error creating VPC peering connection: missing peering connection ID") + } + + // wait for the VPC peering connection to be approved + stateChangeConf := &retry.StateChangeConf{ + Target: []string{""}, // empty target means we don't care about the target state + Pending: []string{string(organizationvpc.VpcPeeringConnectionStateTypeApproved)}, + Refresh: func() (any, string, error) { + resp, err := client.OrganizationVpcGet(ctx, orgID, orgVpcID) + if err != nil { + return nil, "", fmt.Errorf("error getting VPC: %w", err) + } + + pCon := lookupPeeringConnection(resp, *pc.PeeringConnectionId) + if pCon == nil { + return nil, "", fmt.Errorf("VPC peering connection not found") + } + + if pCon.State == organizationvpc.VpcPeeringConnectionStateTypeApproved { + return pCon, string(pCon.State), nil + } + + return pCon, "", nil // return empty target state to stop the loop + }, + Delay: pollDelay, + MinTimeout: pollInterval, + Timeout: d.Timeout(schema.TimeoutCreate), + } + + resp, err := stateChangeConf.WaitForStateContext(ctx) + if err != nil { + return nil, fmt.Errorf("error waiting for VPC peering connection to change state: %w", err) + } + + pCon, ok := resp.(*organizationvpc.OrganizationVpcGetPeeringConnectionOut) + if !ok { + return nil, fmt.Errorf("error creating VPC peering connection: invalid response") // this should never happen + } + + return pCon, nil +} + +func deletePeeringConnection( + ctx context.Context, + orgID, orgVpcID string, + client avngen.Client, + d *schema.ResourceData, + pc *organizationvpc.OrganizationVpcGetPeeringConnectionOut, +) error { + if pc == nil { + return nil // consider already deleted + } + + _, err := client.OrganizationVpcPeeringConnectionDeleteById( + ctx, + orgID, + orgVpcID, + *pc.PeeringConnectionId, + ) + if err != nil { + if avngen.IsNotFound(err) { + return nil // consider already deleted + } + + return fmt.Errorf("error deleting VPC peering connection: %w", err) + } + + stateChangeConf := &retry.StateChangeConf{ + Target: []string{string(organizationvpc.VpcPeeringConnectionStateTypeDeleted)}, + Refresh: func() (interface{}, string, error) { + resp, err := client.OrganizationVpcGet(ctx, orgID, orgVpcID) + if err != nil { + if avngen.IsNotFound(err) { + return struct{}{}, string(organizationvpc.VpcPeeringConnectionStateTypeDeleted), nil + } + + return nil, "", fmt.Errorf("error getting VPC: %w", err) + } + + pCon := lookupPeeringConnection(resp, *pc.PeeringConnectionId) + if pCon == nil { + // return empty struct to signal to the state change function that the resource is deleted + return struct{}{}, string(organizationvpc.VpcPeeringConnectionStateTypeDeleted), nil + } + + return pCon, string(pCon.State), nil + }, + Delay: pollDelay, + Timeout: d.Timeout(schema.TimeoutDelete), + MinTimeout: pollInterval, + } + + if _, err = stateChangeConf.WaitForStateContext(ctx); err != nil && !avngen.IsNotFound(err) { + return fmt.Errorf("error waiting for deletion: %w", err) + } + + return nil +} + +func lookupPeeringConnection( + vpc *organizationvpc.OrganizationVpcGetOut, + peeringConnectionID string, +) *organizationvpc.OrganizationVpcGetPeeringConnectionOut { + for _, pCon := range vpc.PeeringConnections { + if pCon.PeeringConnectionId != nil && *pCon.PeeringConnectionId == peeringConnectionID { + return &pCon + } + } + + return nil +} diff --git a/internal/sdkprovider/service/vpc/org_vpc_peering_connection_test.go b/internal/sdkprovider/service/vpc/org_vpc_peering_connection_test.go new file mode 100644 index 000000000..f4da22f57 --- /dev/null +++ b/internal/sdkprovider/service/vpc/org_vpc_peering_connection_test.go @@ -0,0 +1,454 @@ +package vpc + +import ( + "context" + "errors" + "reflect" + "sync" + "testing" + "time" + "unsafe" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/samber/lo" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + + "github.com/aiven/terraform-provider-aiven/mocks" +) + +var testMu = &sync.RWMutex{} + +func TestCreatePeeringConnection(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + mockClient = mocks.NewMockClient(t) + d = schema.TestResourceDataRaw(t, nil, nil) + + orgID = uuid.New().String() + vpcID = uuid.New().String() + ) + + testCases := []struct { + name string + setupMocks func() *mocks.MockClient + expectedState organizationvpc.VpcPeeringConnectionStateType + expectError bool + }{ + { + name: "successful creation and approval", + expectedState: organizationvpc.VpcPeeringConnectionStateTypeActive, + setupMocks: func() *mocks.MockClient { + pcID := uuid.New().String() + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 1*time.Second) + + // Setup create response + mc.On("OrganizationVpcPeeringConnectionCreate", + ctx, + orgID, + vpcID, + mock.AnythingOfType("*organizationvpc.OrganizationVpcPeeringConnectionCreateIn"), + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionCreateOut{ + PeeringConnectionId: &pcID, + }, nil).Once() + + // Setup first get response (pending) + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeApproved, + }, + }, + }, nil).Once() + + // Setup second get response (approved) + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeActive, + }, + }, + }, nil).Once() + + return mc + }, + }, + { + name: "creation fails", + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + + mc.On("OrganizationVpcPeeringConnectionCreate", + ctx, + orgID, + vpcID, + mock.AnythingOfType("*organizationvpc.OrganizationVpcPeeringConnectionCreateIn"), + ).Return(nil, errors.New("creation failed")).Once() + + return mc + }, + expectError: true, + }, + { + name: "approval timeout", + setupMocks: func() *mocks.MockClient { + pcID := uuid.New().String() + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 100*time.Millisecond) + + mc.On("OrganizationVpcPeeringConnectionCreate", + ctx, + orgID, + vpcID, + mock.AnythingOfType("*organizationvpc.OrganizationVpcPeeringConnectionCreateIn"), + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionCreateOut{ + PeeringConnectionId: &pcID, + }, nil) + + // Always return pending state + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeApproved, + }, + }, + }, nil) + + return mc + }, + expectError: true, + }, + { + name: "peering connection disappears", + setupMocks: func() *mocks.MockClient { + pcID := uuid.New().String() + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 1*time.Second) + + mc.On("OrganizationVpcPeeringConnectionCreate", + ctx, + orgID, + vpcID, + mock.AnythingOfType("*organizationvpc.OrganizationVpcPeeringConnectionCreateIn"), + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionCreateOut{ + PeeringConnectionId: &pcID, + }, nil).Once() + + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{}, + }, nil).Once() + + return mc + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mc := tc.setupMocks() + + result, err := createPeeringConnection( + ctx, + orgID, + vpcID, + mc, + d, + organizationvpc.OrganizationVpcPeeringConnectionCreateIn{}, + ) + + if tc.expectError { + assert.Error(t, err) + assert.Nil(t, result) + } else { + assert.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, tc.expectedState, result.State) + } + + mockClient.AssertExpectations(t) + }) + } +} + +func setTimeouts(t *testing.T, rd *schema.ResourceData, delay, interval, timeout time.Duration) { + t.Helper() + + testMu.Lock() + originalDelay := pollDelay + originalInterval := pollInterval + pollDelay = delay + pollInterval = interval + testMu.Unlock() + + t.Cleanup(func() { + testMu.Lock() + pollDelay = originalDelay + pollInterval = originalInterval + testMu.Unlock() + }) + + timeouts := &schema.ResourceTimeout{ + Create: lo.ToPtr(timeout), + Read: lo.ToPtr(timeout), + Update: lo.ToPtr(timeout), + Delete: lo.ToPtr(timeout), + } + + val := reflect.ValueOf(rd).Elem() + field := val.FieldByName("timeouts") + reflect.NewAt(field.Type(), unsafe.Pointer(field.UnsafeAddr())).Elem().Set(reflect.ValueOf(timeouts)) +} + +func TestDeletePeeringConnection(t *testing.T) { + t.Parallel() + + var ( + ctx = context.Background() + d = schema.TestResourceDataRaw(t, nil, nil) + orgID = uuid.New().String() + vpcID = uuid.New().String() + pcID = uuid.New().String() + pc = &organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeActive, + } + ) + + testCases := []struct { + name string + setupMocks func() *mocks.MockClient + inputPC *organizationvpc.OrganizationVpcGetPeeringConnectionOut + expectError bool + }{ + { + name: "nil peering connection", + inputPC: nil, + setupMocks: func() *mocks.MockClient { + return mocks.NewMockClient(t) + }, + expectError: false, + }, + { + name: "successful deletion", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 2*time.Second) + + // Setup delete response + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionDeleteByIdOut{}, nil).Once() + + // First get after delete shows connection still exists + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeDeleting, + }, + }, + }, nil).Once() + + // Second get shows connection is gone + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeDeleted, + }, + }, + }, nil).Once() + + return mc + }, + expectError: false, + }, + { + name: "delete returns not found", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 2*time.Second) + + // Setup delete response with not found error + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(nil, avngen.Error{Status: 404}).Once() + + return mc + }, + expectError: false, + }, + { + name: "delete fails with error", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 2*time.Second) + + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(nil, errors.New("delete failed")).Once() + + return mc + }, + expectError: true, + }, + { + name: "get after delete fails with non-404 error", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 2*time.Second) + + // Setup delete response + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionDeleteByIdOut{}, nil).Once() + + // Get fails with error + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(nil, errors.New("get failed")).Once() + + return mc + }, + expectError: true, + }, + { + name: "get after delete returns 404", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 2*time.Second) + + // Setup delete response + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionDeleteByIdOut{}, nil).Once() + + // Get returns 404 + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(nil, avngen.Error{Status: 404}).Once() + + return mc + }, + expectError: false, + }, + { + name: "deletion timeout", + inputPC: pc, + setupMocks: func() *mocks.MockClient { + mc := mocks.NewMockClient(t) + setTimeouts(t, d, 10*time.Millisecond, 10*time.Millisecond, 1*time.Second) + + // Setup delete response + mc.On("OrganizationVpcPeeringConnectionDeleteById", + ctx, + orgID, + vpcID, + pcID, + ).Return(&organizationvpc.OrganizationVpcPeeringConnectionDeleteByIdOut{}, nil).Once() + + // Always return the connection in deleting state + mc.On("OrganizationVpcGet", + ctx, + orgID, + vpcID, + ).Return(&organizationvpc.OrganizationVpcGetOut{ + PeeringConnections: []organizationvpc.OrganizationVpcGetPeeringConnectionOut{ + { + PeeringConnectionId: &pcID, + State: organizationvpc.VpcPeeringConnectionStateTypeDeleting, + }, + }, + }, nil) + + return mc + }, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + mc := tc.setupMocks() + + err := deletePeeringConnection( + ctx, + orgID, + vpcID, + mc, + d, + tc.inputPC, + ) + + if tc.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + + mc.AssertExpectations(t) + }) + } +} diff --git a/internal/sdkprovider/service/vpc/organization_vpc.go b/internal/sdkprovider/service/vpc/organization_vpc.go new file mode 100644 index 000000000..ee4ba548a --- /dev/null +++ b/internal/sdkprovider/service/vpc/organization_vpc.go @@ -0,0 +1,203 @@ +package vpc + +import ( + "context" + "fmt" + "time" + + avngen "github.com/aiven/go-client-codegen" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/validation" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" + "github.com/aiven/terraform-provider-aiven/internal/schemautil/userconfig" +) + +var aivenOrganizationVPCSchema = map[string]*schema.Schema{ + "organization_id": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: "The ID of the organization.", + }, + "cloud_name": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + Description: userconfig.Desc("The cloud provider and region where the service is hosted in the format `CLOUD_PROVIDER-REGION_NAME`. For example, `google-europe-west1` or `aws-us-east-2`.").ForceNew().Build(), + }, + "network_cidr": { + Type: schema.TypeString, + Required: true, + ForceNew: true, + ValidateFunc: validation.IsCIDR, + Description: "Network address range used by the VPC. For example, `192.168.0.0/24`.", + }, + "organization_vpc_id": { + Type: schema.TypeString, + Computed: true, + Description: "The ID of the Aiven Organization VPC.", + }, + "state": { + Type: schema.TypeString, + Computed: true, + Description: userconfig.Desc("State of the VPC.").PossibleValuesString(organizationvpc.VpcStateTypeChoices()...).Build(), + }, + "create_time": { + Type: schema.TypeString, + Computed: true, + Description: "Time of creation of the VPC.", + }, + "update_time": { + Type: schema.TypeString, + Computed: true, + Description: "Time of the last update of the VPC.", + }, +} + +func ResourceOrganizationVPC() *schema.Resource { + return &schema.Resource{ + Description: "Creates and manages a VPC for an Aiven organization.", + CreateContext: common.WithGenClient(resourceOrganizationVPCCreate), + ReadContext: common.WithGenClient(resourceOrganizationVPCRead), + DeleteContext: common.WithGenClient(resourceOrganizationVPCDelete), + Importer: &schema.ResourceImporter{ + StateContext: schema.ImportStatePassthroughContext, + }, + Timeouts: schemautil.DefaultResourceTimeouts(), + + Schema: aivenOrganizationVPCSchema, + } +} + +func resourceOrganizationVPCCreate(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + var ( + orgID = d.Get("organization_id").(string) + cloud = d.Get("cloud_name").(string) + cidr = d.Get("network_cidr").(string) + ) + + resp, err := client.OrganizationVpcCreate(ctx, orgID, &organizationvpc.OrganizationVpcCreateIn{ + Clouds: []organizationvpc.CloudIn{ + { + CloudName: cloud, + NetworkCidr: cidr, + }, + }, + PeeringConnections: make([]organizationvpc.PeeringConnectionIn, 0), // nil here would cause an error from the API + }) + if err != nil { + return err + } + + // Wait for VPC to become active + stateConf := &retry.StateChangeConf{ + Pending: []string{string(organizationvpc.VpcStateTypeApproved)}, + Target: []string{string(organizationvpc.VpcStateTypeActive)}, + Refresh: func() (interface{}, string, error) { + orgVPC, err := client.OrganizationVpcGet(ctx, orgID, resp.OrganizationVpcId) + if err != nil { + return nil, "", err + } + + return orgVPC, string(orgVPC.State), nil + }, + Timeout: d.Timeout(schema.TimeoutCreate), + Delay: 1 * time.Second, + MinTimeout: common.DefaultStateChangeMinTimeout, + } + + _, err = stateConf.WaitForStateContext(ctx) + if err != nil { + return fmt.Errorf("error waiting for VPC (%q) to become active: %w", resp.OrganizationVpcId, err) + } + + d.SetId(schemautil.BuildResourceID(orgID, resp.OrganizationVpcId)) + + return resourceOrganizationVPCRead(ctx, d, client) +} + +func resourceOrganizationVPCRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + orgID, vpcID, err := schemautil.SplitResourceID2(d.Id()) + if err != nil { + return err + } + + resp, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + return err + } + + // currently we support only 1 cloud per VPC + if len(resp.Clouds) != 1 { + return fmt.Errorf("expected exactly 1 cloud, got %d", len(resp.Clouds)) + } + + if err = d.Set("organization_id", orgID); err != nil { + return err + } + if err = d.Set("organization_vpc_id", resp.OrganizationVpcId); err != nil { + return err + } + if err = d.Set("cloud_name", resp.Clouds[0].CloudName); err != nil { + return err + } + if err = d.Set("network_cidr", resp.Clouds[0].NetworkCidr); err != nil { + return err + } + if err = d.Set("state", resp.State); err != nil { + return err + } + if err = d.Set("create_time", resp.CreateTime.String()); err != nil { + return err + } + if err = d.Set("update_time", resp.UpdateTime.String()); err != nil { + return err + } + + return nil +} + +func resourceOrganizationVPCDelete(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + orgID, vpcID, err := schemautil.SplitResourceID2(d.Id()) + if err != nil { + return err + } + + _, err = client.OrganizationVpcDelete(ctx, orgID, vpcID) + if common.IsCritical(err) { + return err + } + + // Wait for VPC to be deleted + stateConf := &retry.StateChangeConf{ + Target: []string{string(organizationvpc.VpcStateTypeDeleted)}, + Refresh: func() (interface{}, string, error) { + orgVPC, err := client.OrganizationVpcGet(ctx, orgID, vpcID) + if err != nil { + if avngen.IsNotFound(err) { + // if resource is not found, it's considered deleted + // return empty struct instead of nil to avoid the "not found" counter behavior in StateChangeConf when Target states are specified + return struct{}{}, string(organizationvpc.VpcStateTypeDeleted), nil + } + + return nil, "", err + } + + return orgVPC, string(orgVPC.State), nil + }, + Timeout: d.Timeout(schema.TimeoutDelete), + Delay: 1 * time.Second, + MinTimeout: common.DefaultStateChangeMinTimeout, + } + + _, err = stateConf.WaitForStateContext(ctx) + if common.IsCritical(err) { + return err + } + + return nil +} diff --git a/internal/sdkprovider/service/vpc/organization_vpc_data_source.go b/internal/sdkprovider/service/vpc/organization_vpc_data_source.go new file mode 100644 index 000000000..0c9d7efc9 --- /dev/null +++ b/internal/sdkprovider/service/vpc/organization_vpc_data_source.go @@ -0,0 +1,31 @@ +package vpc + +import ( + "context" + + avngen "github.com/aiven/go-client-codegen" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +func DataSourceOrganizationVPC() *schema.Resource { + return &schema.Resource{ + Description: "Gets information about an existing VPC in an Aiven organization.", + ReadContext: common.WithGenClient(datasourceOrganizationVPCRead), + Schema: schemautil.ResourceSchemaAsDatasourceSchema(aivenOrganizationVPCSchema, + "organization_id", "organization_vpc_id"), + } +} + +func datasourceOrganizationVPCRead(ctx context.Context, d *schema.ResourceData, client avngen.Client) error { + var ( + orgID = d.Get("organization_id").(string) + orgVpcID = d.Get("organization_vpc_id").(string) + ) + + d.SetId(schemautil.BuildResourceID(orgID, orgVpcID)) + + return resourceOrganizationVPCRead(ctx, d, client) +} diff --git a/internal/sdkprovider/service/vpc/organization_vpc_test.go b/internal/sdkprovider/service/vpc/organization_vpc_test.go new file mode 100644 index 000000000..43c9dd6f0 --- /dev/null +++ b/internal/sdkprovider/service/vpc/organization_vpc_test.go @@ -0,0 +1,178 @@ +package vpc_test + +import ( + "context" + "fmt" + "regexp" + "testing" + + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/plancheck" + "github.com/hashicorp/terraform-plugin-testing/terraform" + + acc "github.com/aiven/terraform-provider-aiven/internal/acctest" + "github.com/aiven/terraform-provider-aiven/internal/common" + "github.com/aiven/terraform-provider-aiven/internal/schemautil" +) + +const ( + organizationVPCResource = "aiven_organization_vpc" + organizationVPCDataSource = "data_aiven_organization_vpc" +) + +func TestAccAivenOrganizationVPC(t *testing.T) { + var ( + orgName = acc.SkipIfEnvVarsNotSet(t, "AIVEN_ORGANIZATION_NAME")["AIVEN_ORGANIZATION_NAME"] + registry = preSetOrgVPCTemplates(t) + newComposition = func() *acc.CompositionBuilder { + return registry.NewCompositionBuilder(). + Add("organization_data", map[string]interface{}{ + "organization_name": orgName}) + } + + resourceName = fmt.Sprintf("%s.%s", organizationVPCResource, "test_org_vpc") + dataSourceName = fmt.Sprintf("data.%s.%s", organizationVPCResource, "vpc_ds") + ) + + resource.ParallelTest(t, resource.TestCase{ + PreCheck: func() { acc.TestAccPreCheck(t) }, + ProtoV6ProviderFactories: acc.TestProtoV6ProviderFactories, + CheckDestroy: testAccCheckOrganizationVPCResourceDestroy, + Steps: []resource.TestStep{ + { + // invalid CIDR range + Config: ` +resource "aiven_organization_vpc" "test_validation" { + organization_id = "test-org-id" + cloud_name = "aws-eu-west-1" + network_cidr = "256.256.256.256/24" +}`, + ExpectError: regexp.MustCompile(`expected "network_cidr" to be a valid CIDR Value`), + }, + { + // basic VPC creation + Config: newComposition(). + Add(organizationVPCResource, map[string]interface{}{ + "resource_name": "test_org_vpc", + "cloud_name": "aws-eu-west-1", + "network_cidr": "10.0.0.0/24", + }).MustRender(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "cloud_name", "aws-eu-west-1"), + resource.TestCheckResourceAttr(resourceName, "network_cidr", "10.0.0.0/24"), + resource.TestCheckResourceAttr(resourceName, "state", string(organizationvpc.VpcStateTypeActive)), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + ), + }, + { + // test ForceNew on network_cidr change + Config: newComposition(). + Add(organizationVPCResource, map[string]interface{}{ + "resource_name": "test_org_vpc", + "cloud_name": "aws-eu-west-1", + "network_cidr": "10.1.0.0/24", // Changed CIDR + }).MustRender(t), + ConfigPlanChecks: resource.ConfigPlanChecks{ + PreApply: []plancheck.PlanCheck{ + plancheck.ExpectResourceAction(resourceName, plancheck.ResourceActionReplace), + }, + }, + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttr(resourceName, "cloud_name", "aws-eu-west-1"), + resource.TestCheckResourceAttr(resourceName, "network_cidr", "10.1.0.0/24"), + resource.TestCheckResourceAttr(resourceName, "state", string(organizationvpc.VpcStateTypeActive)), + resource.TestCheckResourceAttrSet(resourceName, "organization_id"), + resource.TestCheckResourceAttrSet(resourceName, "create_time"), + ), + }, + { + // import state + ResourceName: resourceName, + ImportState: true, + ImportStateVerify: true, + }, + { + // test the data source + Config: newComposition(). + Add(organizationVPCResource, map[string]interface{}{ + "resource_name": "test_org_vpc", + "cloud_name": "aws-eu-west-1", + "network_cidr": "10.0.0.0/24", + }). + Add(organizationVPCDataSource, map[string]interface{}{ + "resource_name": "vpc_ds", + "organization_vpc_id": acc.Reference(fmt.Sprintf("%s.organization_vpc_id", resourceName)), + }).MustRender(t), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataSourceName, "organization_id", resourceName, "organization_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "cloud_name", resourceName, "cloud_name"), + resource.TestCheckResourceAttrPair(dataSourceName, "network_cidr", resourceName, "network_cidr"), + resource.TestCheckResourceAttrPair(dataSourceName, "state", resourceName, "state"), + resource.TestCheckResourceAttrPair(dataSourceName, "create_time", resourceName, "create_time"), + resource.TestCheckResourceAttr(dataSourceName, "state", string(organizationvpc.VpcStateTypeActive)), + resource.TestCheckResourceAttrSet(dataSourceName, "organization_vpc_id"), + resource.TestCheckResourceAttrPair(dataSourceName, "organization_vpc_id", resourceName, "organization_vpc_id"), + ), + }, + }, + }) +} + +func testAccCheckOrganizationVPCResourceDestroy(s *terraform.State) error { + ctx := context.Background() + + c, err := acc.GetTestGenAivenClient() + if err != nil { + return fmt.Errorf("error initializing Aiven client: %w", err) + } + + for _, rs := range s.RootModule().Resources { + if rs.Type != organizationVPCResource { + continue + } + + orgID, vpcID, err := schemautil.SplitResourceID2(rs.Primary.ID) + if err != nil { + return fmt.Errorf("error splitting resource ID: %w", err) + } + + orgVPC, err := c.OrganizationVpcGet(ctx, orgID, vpcID) + if common.IsCritical(err) { + return fmt.Errorf("error fetching VPC (%q): %w", vpcID, err) + } + + if orgVPC != nil { + return fmt.Errorf("VPC (%q) for organization (%q) still exists", vpcID, orgID) + } + } + + return nil +} + +func preSetOrgVPCTemplates(t *testing.T) *acc.TemplateRegistry { + t.Helper() + + var registry = acc.NewTemplateRegistry(organizationVPCResource) + + registry.MustAddTemplate(t, "organization_data", ` +data "aiven_organization" "foo" { + name = "{{ .organization_name }}" +}`) + + registry.MustAddTemplate(t, organizationVPCResource, ` +resource "aiven_organization_vpc" "{{ .resource_name }}" { + organization_id = data.aiven_organization.foo.id + cloud_name = "{{ .cloud_name }}" + network_cidr = "{{ .network_cidr }}" +}`) + + registry.MustAddTemplate(t, organizationVPCDataSource, ` +data "aiven_organization_vpc" "{{ .resource_name }}" { + organization_id = data.aiven_organization.foo.id + organization_vpc_id = {{ if .organization_vpc_id.IsLiteral }}"{{ .organization_vpc_id.Value }}"{{ else }}{{ .organization_vpc_id.Value }}{{ end }} +}`) + + return registry +} diff --git a/internal/sdkprovider/service/vpc/sweep.go b/internal/sdkprovider/service/vpc/sweep.go index f4462ce18..c33e7c477 100644 --- a/internal/sdkprovider/service/vpc/sweep.go +++ b/internal/sdkprovider/service/vpc/sweep.go @@ -40,6 +40,14 @@ func init() { }, }) + sweep.AddTestSweepers("aiven_organization_vpc", &resource.Sweeper{ + Name: "aiven_organization_vpc", + F: sweepOrgVPCs(ctx), + Dependencies: []string{ + "aiven_organization", + }, + }) + sweep.AddTestSweepers("aiven_aws_vpc_peering_connection", &resource.Sweeper{ Name: "aiven_aws_vpc_peering_connection", F: sweepVPCPeeringCons(ctx), @@ -48,6 +56,14 @@ func init() { }, }) + sweep.AddTestSweepers("aiven_aws_org_vpc_peering_connection", &resource.Sweeper{ + Name: "aiven_aws_org_vpc_peering_connection", + F: sweepOrgVPCPeeringCons(ctx), + Dependencies: []string{ + "aiven_organization_vpc", + }, + }) + sweep.AddTestSweepers("aiven_azure_vpc_peering_connection", &resource.Sweeper{ Name: "aiven_azure_vpc_peering_connection", F: sweepVPCPeeringCons(ctx), @@ -56,6 +72,14 @@ func init() { }, }) + sweep.AddTestSweepers("aiven_azure_org_vpc_peering_connection", &resource.Sweeper{ + Name: "aiven_azure_org_vpc_peering_connection", + F: sweepVPCPeeringCons(ctx), + Dependencies: []string{ + "aiven_organization_vpc", + }, + }) + sweep.AddTestSweepers("aiven_gcp_vpc_peering_connection", &resource.Sweeper{ Name: "aiven_gcp_vpc_peering_connection", F: sweepVPCPeeringCons(ctx), @@ -64,6 +88,14 @@ func init() { }, }) + sweep.AddTestSweepers("aiven_gcp_org_vpc_peering_connection", &resource.Sweeper{ + Name: "aiven_gcp_org_vpc_peering_connection", + F: sweepVPCPeeringCons(ctx), + Dependencies: []string{ + "aiven_organization_vpc", + }, + }) + sweep.AddTestSweepers("aiven_transit_gateway_vpc_attachment", &resource.Sweeper{ Name: "aiven_transit_gateway_vpc_attachment", F: sweepVPCPeeringCons(ctx), @@ -115,6 +147,41 @@ func sweepVPCs(ctx context.Context) func(string) error { } } +func sweepOrgVPCs(ctx context.Context) func(string) error { + return func(_ string) error { + orgName := os.Getenv("AIVEN_ORGANIZATION_NAME") + client, err := sweep.SharedGenClient() + if err != nil { + return err + } + + list, err := client.AccountList(ctx) + if err != nil { + return fmt.Errorf("error retrieving a list of organizations : %w", err) + } + + for _, org := range list { + if org.AccountName != orgName { + continue + } + + VPCs, err := client.OrganizationVpcList(ctx, org.OrganizationId) + if err != nil { + return fmt.Errorf("error retrieving a list of vpcs for a organization : %w", err) + } + + for _, vpc := range VPCs { + _, err := client.OrganizationVpcDelete(ctx, org.OrganizationId, vpc.OrganizationVpcId) + if common.IsCritical(err) { + return fmt.Errorf("error deleting vpc %s: %w", vpc.OrganizationVpcId, err) + } + } + } + + return nil + } +} + func sweepVPCPeeringCons(ctx context.Context) func(string) error { return func(_ string) error { projectName := os.Getenv("AIVEN_PROJECT_NAME") @@ -178,6 +245,58 @@ func sweepVPCPeeringCons(ctx context.Context) func(string) error { } } +func sweepOrgVPCPeeringCons(ctx context.Context) func(string) error { + return func(_ string) error { + orgName := os.Getenv("AIVEN_ORGANIZATION_NAME") + client, err := sweep.SharedGenClient() + if err != nil { + return err + } + + list, err := client.AccountList(ctx) + if err != nil { + return fmt.Errorf("error retrieving a list of organizations : %w", err) + } + + for _, org := range list { + if org.AccountName != orgName { + continue + } + + VPCs, err := client.OrganizationVpcList(ctx, org.OrganizationId) + if common.IsCritical(err) { + return fmt.Errorf("error retrieving a list of vpcs for a project : %w", err) + } + + for _, vpc := range VPCs { + orgVPC, err := client.OrganizationVpcGet(ctx, org.OrganizationId, vpc.OrganizationVpcId) + if common.IsCritical(err) { + return fmt.Errorf("error retrieving a list of vpcs for a project : %w", err) + } + + for _, peeringCon := range orgVPC.PeeringConnections { + if peeringCon.PeeringConnectionId == nil { + continue // should not happen + } + + _, err = client.OrganizationVpcPeeringConnectionDeleteById(ctx, org.OrganizationId, vpc.OrganizationVpcId, *peeringCon.PeeringConnectionId) + if common.IsCritical(err) { + return fmt.Errorf("error deleting vpc peering connection %s/%s/%s: %w", + vpc.OrganizationVpcId, + vpc.OrganizationVpcId, + *peeringCon.PeeringConnectionId, + err) + } + + } + + } + } + + return nil + } +} + func sweepAWSPrivatelinks(ctx context.Context) func(string) error { return func(_ string) error { projectName := os.Getenv("AIVEN_PROJECT_NAME") diff --git a/internal/sdkprovider/service/vpc/vpc_peering_connection.go b/internal/sdkprovider/service/vpc/vpc_peering_connection.go index d8b7a27fa..42b2553d0 100644 --- a/internal/sdkprovider/service/vpc/vpc_peering_connection.go +++ b/internal/sdkprovider/service/vpc/vpc_peering_connection.go @@ -109,7 +109,7 @@ func resourceVPCPeeringConnectionCreate(ctx context.Context, d *schema.ResourceD } pc = res.(*aiven.VPCPeeringConnection) - diags := getDiagnosticsFromState(pc) + diags := getDiagnosticsFromState(newAivenVPCPeeringState(pc)) if peerRegion != "" { d.SetId(schemautil.BuildResourceID(projectName, vpcID, pc.PeerCloudAccount, pc.PeerVPC, *pc.PeerRegion)) @@ -125,30 +125,6 @@ func resourceVPCPeeringConnectionCreate(ctx context.Context, d *schema.ResourceD return append(diags, resourceVPCPeeringConnectionRead(ctx, d, m)...) } -// stateInfoToString converts VPC peering connection state_info to a string -func stateInfoToString(s *map[string]interface{}) string { - if len(*s) == 0 { - return "" - } - - var str string - // Print message first - if m, ok := (*s)["message"]; ok { - str = fmt.Sprintf("%s", m) - delete(*s, "message") - } - - for k, v := range *s { - if _, ok := v.(string); ok { - str += fmt.Sprintf("\n %q:%q", k, v) - } else { - str += fmt.Sprintf("\n %q:`%+v`", k, v) - } - } - - return str -} - type peeringVPCID struct { projectName string vpcID string @@ -489,37 +465,6 @@ func isAzureVPCPeeringConnection(ctx context.Context, d *schema.ResourceData, c return false, nil } -func getDiagnosticsFromState(pc *aiven.VPCPeeringConnection) diag.Diagnostics { - if pc.State != "ACTIVE" { - switch pc.State { - case "PENDING_PEER": - return diag.Diagnostics{{ - Severity: diag.Warning, - Summary: fmt.Sprintf("Aiven platform has created a connection to the specified "+ - "peer successfully in the cloud, but the connection is not active until the user "+ - "completes the setup in their cloud account. The steps needed in the user cloud "+ - "account depend on the used cloud provider. Find more in the state info: %s", - stateInfoToString(pc.StateInfo))}} - case "DELETED": - return diag.Errorf("A user has deleted the peering connection through the Aiven " + - "Terraform provider, or Aiven Web Console or directly via Aiven API. There are no " + - "transitions from this state") - case "DELETED_BY_PEER": - return diag.Errorf("A user deleted the peering cloud resource in their account. " + - "There are no transitions from this state") - case "REJECTED_BY_PEER": - return diag.Errorf("VPC peering connection request was rejected, state info: %s", - stateInfoToString(pc.StateInfo)) - case "INVALID_SPECIFICATION": - return diag.Errorf("VPC peering connection cannot be created, more in the state info: %s", - stateInfoToString(pc.StateInfo)) - default: - return diag.Errorf("Unknown VPC peering connection state: %s", pc.State) - } - } - return nil -} - func validateVPCID(i interface{}, k string) (warnings []string, errors []error) { v, ok := i.(string) if !ok { diff --git a/internal/sdkprovider/service/vpc/vpc_peering_connection_state.go b/internal/sdkprovider/service/vpc/vpc_peering_connection_state.go new file mode 100644 index 000000000..9e7808bb0 --- /dev/null +++ b/internal/sdkprovider/service/vpc/vpc_peering_connection_state.go @@ -0,0 +1,111 @@ +package vpc + +import ( + "fmt" + + "github.com/aiven/aiven-go-client/v2" + "github.com/aiven/go-client-codegen/handler/organizationvpc" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" +) + +// this code provides adapter implementations for VPC peering connection state handling +// between different HTTP clients (aiven-go-client and go-client-codegen). It defines a common +// interface peeringConnectionState and wrapper types that adapt different client responses to +// this interface, enabling state checks and diagnostics without major code refactoring. +type peeringConnectionState interface { + GetState() string + GetStateInfo() *map[string]any +} + +func getDiagnosticsFromState(pc peeringConnectionState) diag.Diagnostics { + if pc.GetState() != "ACTIVE" { + switch pc.GetState() { + case "PENDING_PEER": + return diag.Diagnostics{{ + Severity: diag.Warning, + Summary: fmt.Sprintf("Aiven platform has created a connection to the specified "+ + "peer successfully in the cloud, but the connection is not active until the user "+ + "completes the setup in their cloud account. The steps needed in the user cloud "+ + "account depend on the used cloud provider. Find more in the state info: %s", + stateInfoToString(pc.GetStateInfo()))}} + case "DELETED": + return diag.Errorf("A user has deleted the peering connection through the Aiven " + + "Terraform provider, or Aiven Web Console or directly via Aiven API. There are no " + + "transitions from this state") + case "DELETED_BY_PEER": + return diag.Errorf("A user deleted the peering cloud resource in their account. " + + "There are no transitions from this state") + case "REJECTED_BY_PEER": + return diag.Errorf("VPC peering connection request was rejected, state info: %s", + stateInfoToString(pc.GetStateInfo())) + case "INVALID_SPECIFICATION": + return diag.Errorf("VPC peering connection cannot be created, more in the state info: %s", + stateInfoToString(pc.GetStateInfo())) + default: + return diag.Errorf("Unknown VPC peering connection state: %s", pc.GetState()) + } + } + return nil +} + +// stateInfoToString converts VPC peering connection state_info to a string +func stateInfoToString(s *map[string]interface{}) string { + if s == nil || len(*s) == 0 { + return "" + } + + var str string + // Print message first + if m, ok := (*s)["message"]; ok { + str = fmt.Sprintf("%s", m) + delete(*s, "message") + } + + for k, v := range *s { + if _, ok := v.(string); ok { + str += fmt.Sprintf("\n %q:%q", k, v) + } else { + str += fmt.Sprintf("\n %q:`%+v`", k, v) + } + } + + return str +} + +type aivenVPCPeeringWrapper struct { + *aiven.VPCPeeringConnection +} + +// Create wrapper functions instead of methods +func newAivenVPCPeeringState(pc *aiven.VPCPeeringConnection) peeringConnectionState { + return &aivenVPCPeeringWrapper{pc} +} + +func (w *aivenVPCPeeringWrapper) GetState() string { + return w.State +} + +func (w *aivenVPCPeeringWrapper) GetStateInfo() *map[string]any { + return w.StateInfo +} + +type organizationVPCPeeringWrapper struct { + *organizationvpc.OrganizationVpcGetPeeringConnectionOut +} + +func newOrganizationVPCPeeringState(pc *organizationvpc.OrganizationVpcGetPeeringConnectionOut) *organizationVPCPeeringWrapper { + return &organizationVPCPeeringWrapper{pc} +} + +func (w *organizationVPCPeeringWrapper) GetState() string { + return string(w.State) +} + +func (w *organizationVPCPeeringWrapper) GetStateInfo() *map[string]any { + stateInfo := make(map[string]any) + + stateInfo["message"] = w.StateInfo.Message + stateInfo["type"] = w.StateInfo.Type + + return &stateInfo +}