diff --git a/docs/data-sources/cdn_custom_domain.md b/docs/data-sources/cdn_custom_domain.md index 38c2da3e..7839b991 100644 --- a/docs/data-sources/cdn_custom_domain.md +++ b/docs/data-sources/cdn_custom_domain.md @@ -4,14 +4,14 @@ page_title: "stackit_cdn_custom_domain Data Source - stackit" subcategory: "" description: |- CDN distribution data source schema. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_cdn_custom_domain (Data Source) CDN distribution data source schema. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/cdn_distribution.md b/docs/data-sources/cdn_distribution.md index 641e9698..799d29c2 100644 --- a/docs/data-sources/cdn_distribution.md +++ b/docs/data-sources/cdn_distribution.md @@ -4,14 +4,14 @@ page_title: "stackit_cdn_distribution Data Source - stackit" subcategory: "" description: |- CDN distribution data source schema. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_cdn_distribution (Data Source) CDN distribution data source schema. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/git.md b/docs/data-sources/git.md index c4865611..8cbd29ef 100644 --- a/docs/data-sources/git.md +++ b/docs/data-sources/git.md @@ -4,14 +4,14 @@ page_title: "stackit_git Data Source - stackit" subcategory: "" description: |- Git Instance datasource schema. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_git (Data Source) Git Instance datasource schema. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/routing_table.md b/docs/data-sources/routing_table.md new file mode 100644 index 00000000..748431d9 --- /dev/null +++ b/docs/data-sources/routing_table.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_table Data Source - stackit" +subcategory: "" +description: |- + Routing table datasource schema. Must have a region specified in the provider configuration. + ~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_table (Data Source) + +Routing table datasource schema. Must have a `region` specified in the provider configuration. + +~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +data "stackit_routing_table" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. +- `routing_table_id` (String) The routing tables ID. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `created_at` (String) Date-time when the routing table was created +- `default` (Boolean) When true this is the default routing table for this network area. It can't be deleted and is used if the user does not specify it otherwise. +- `description` (String) Description of the routing table. +- `id` (String) Terraform's internal datasource ID. It is structured as "`organization_id`,`region`,`network_area_id`,`routing_table_id`". +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `name` (String) The name of the routing table. +- `system_routes` (Boolean) +- `updated_at` (String) Date-time when the routing table was updated diff --git a/docs/data-sources/routing_table_route.md b/docs/data-sources/routing_table_route.md new file mode 100644 index 00000000..a637284b --- /dev/null +++ b/docs/data-sources/routing_table_route.md @@ -0,0 +1,65 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_table_route Data Source - stackit" +subcategory: "" +description: |- + Routing table route datasource schema. Must have a region specified in the provider configuration. + ~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_table_route (Data Source) + +Routing table route datasource schema. Must have a `region` specified in the provider configuration. + +~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +data "stackit_routing_table_route" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + route_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. +- `route_id` (String) Route ID. +- `routing_table_id` (String) The routing tables ID. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `created_at` (String) Date-time when the route was created +- `destination` (Attributes) Destination of the route. (see [below for nested schema](#nestedatt--destination)) +- `id` (String) Terraform's internal datasource ID. It is structured as "`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`". +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `next_hop` (Attributes) Next hop destination. (see [below for nested schema](#nestedatt--next_hop)) +- `updated_at` (String) Date-time when the route was updated + + +### Nested Schema for `destination` + +Read-Only: + +- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. Only `cidrv4` is supported during experimental stage. +- `value` (String) An CIDR string. + + + +### Nested Schema for `next_hop` + +Read-Only: + +- `type` (String) Possible values are: `blackhole`, `internet`, `ipv4`, `ipv6`. Only `cidrv4` is supported during experimental stage.. +- `value` (String) Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage. diff --git a/docs/data-sources/routing_table_routes.md b/docs/data-sources/routing_table_routes.md new file mode 100644 index 00000000..0d79e464 --- /dev/null +++ b/docs/data-sources/routing_table_routes.md @@ -0,0 +1,71 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_table_routes Data Source - stackit" +subcategory: "" +description: |- + Routing table routes datasource schema. Must have a region specified in the provider configuration. + ~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_table_routes (Data Source) + +Routing table routes datasource schema. Must have a `region` specified in the provider configuration. + +~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +data "stackit_routing_table_routes" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. +- `routing_table_id` (String) The routing tables ID. + +### Optional + +- `region` (String) The datasource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal datasource ID. It is structured as "`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`". +- `routes` (Attributes List) List of routes. (see [below for nested schema](#nestedatt--routes)) + + +### Nested Schema for `routes` + +Read-Only: + +- `created_at` (String) Date-time when the route was created +- `destination` (Attributes) Destination of the route. (see [below for nested schema](#nestedatt--routes--destination)) +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `next_hop` (Attributes) Next hop destination. (see [below for nested schema](#nestedatt--routes--next_hop)) +- `route_id` (String) Route ID. +- `updated_at` (String) Date-time when the route was updated + + +### Nested Schema for `routes.destination` + +Read-Only: + +- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. Only `cidrv4` is supported during experimental stage. +- `value` (String) An CIDR string. + + + +### Nested Schema for `routes.next_hop` + +Read-Only: + +- `type` (String) Possible values are: `blackhole`, `internet`, `ipv4`, `ipv6`. Only `cidrv4` is supported during experimental stage.. +- `value` (String) Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage. diff --git a/docs/data-sources/routing_tables.md b/docs/data-sources/routing_tables.md new file mode 100644 index 00000000..787a3261 --- /dev/null +++ b/docs/data-sources/routing_tables.md @@ -0,0 +1,54 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_tables Data Source - stackit" +subcategory: "" +description: |- + Routing table datasource schema. Must have a region specified in the provider configuration. + ~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_tables (Data Source) + +Routing table datasource schema. Must have a `region` specified in the provider configuration. + +~> This datasource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +data "stackit_routing_tables" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} +``` + + +## Schema + +### Required + +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. + +### Optional + +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `id` (String) Terraform's internal datasource ID. It is structured as "`organization_id`,`region`,`network_area_id`". +- `items` (Attributes List) List of routing tables. (see [below for nested schema](#nestedatt--items)) + + +### Nested Schema for `items` + +Read-Only: + +- `created_at` (String) Date-time when the routing table was created +- `default` (Boolean) When true this is the default routing table for this network area. It can't be deleted and is used if the user does not specify it otherwise. +- `description` (String) Description of the routing table. +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `name` (String) The name of the routing table. +- `routing_table_id` (String) The routing tables ID. +- `system_routes` (Boolean) +- `updated_at` (String) Date-time when the routing table was updated diff --git a/docs/data-sources/server_backup_schedule.md b/docs/data-sources/server_backup_schedule.md index 0d5422a2..16126086 100644 --- a/docs/data-sources/server_backup_schedule.md +++ b/docs/data-sources/server_backup_schedule.md @@ -4,14 +4,14 @@ page_title: "stackit_server_backup_schedule Data Source - stackit" subcategory: "" description: |- Server backup schedule datasource schema. Must have a region specified in the provider configuration. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_server_backup_schedule (Data Source) Server backup schedule datasource schema. Must have a `region` specified in the provider configuration. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/server_backup_schedules.md b/docs/data-sources/server_backup_schedules.md index 30b3cf9b..44c21612 100644 --- a/docs/data-sources/server_backup_schedules.md +++ b/docs/data-sources/server_backup_schedules.md @@ -4,14 +4,14 @@ page_title: "stackit_server_backup_schedules Data Source - stackit" subcategory: "" description: |- Server backup schedules datasource schema. Must have a region specified in the provider configuration. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_server_backup_schedules (Data Source) Server backup schedules datasource schema. Must have a `region` specified in the provider configuration. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/server_update_schedule.md b/docs/data-sources/server_update_schedule.md index 62a10db0..ff4a0c4a 100644 --- a/docs/data-sources/server_update_schedule.md +++ b/docs/data-sources/server_update_schedule.md @@ -4,14 +4,14 @@ page_title: "stackit_server_update_schedule Data Source - stackit" subcategory: "" description: |- Server update schedule datasource schema. Must have a region specified in the provider configuration. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_server_update_schedule (Data Source) Server update schedule datasource schema. Must have a `region` specified in the provider configuration. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/data-sources/server_update_schedules.md b/docs/data-sources/server_update_schedules.md index cf34faae..2ccfe2b5 100644 --- a/docs/data-sources/server_update_schedules.md +++ b/docs/data-sources/server_update_schedules.md @@ -4,14 +4,14 @@ page_title: "stackit_server_update_schedules Data Source - stackit" subcategory: "" description: |- Server update schedules datasource schema. Must have a region specified in the provider configuration. - ~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. + ~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our guide https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources for how to opt-in to use beta resources. --- # stackit_server_update_schedules (Data Source) Server update schedules datasource schema. Must have a `region` specified in the provider configuration. -~> This resource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. +~> This datasource is in beta and may be subject to breaking changes in the future. Use with caution. See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources. ## Example Usage diff --git a/docs/index.md b/docs/index.md index 51aee8c5..d2eb290e 100644 --- a/docs/index.md +++ b/docs/index.md @@ -157,7 +157,7 @@ Note: AWS specific checks must be skipped as they do not work on STACKIT. For de - `default_region` (String) Region will be used as the default location for regional services. Not all services require a region, some are global - `dns_custom_endpoint` (String) Custom endpoint for the DNS service - `enable_beta_resources` (Boolean) Enable beta resources. Default is false. -- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: [iam] +- `experiments` (List of String) Enables experiments. These are unstable features without official support. More information can be found in the README. Available Experiments: [iam routing-tables] - `git_custom_endpoint` (String) Custom endpoint for the Git service - `iaas_custom_endpoint` (String) Custom endpoint for the IaaS service - `loadbalancer_custom_endpoint` (String) Custom endpoint for the Load Balancer service diff --git a/docs/resources/routing_table.md b/docs/resources/routing_table.md new file mode 100644 index 00000000..bd6c7e72 --- /dev/null +++ b/docs/resources/routing_table.md @@ -0,0 +1,50 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_table Resource - stackit" +subcategory: "" +description: |- + Routing table resource schema. Must have a region specified in the provider configuration. + ~> This resource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_table (Resource) + +Routing table resource schema. Must have a `region` specified in the provider configuration. + +~> This resource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +resource "stackit_routing_table" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example" + labels = { + "key" = "value" + } +} +``` + + +## Schema + +### Required + +- `name` (String) The name of the routing table. +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. + +### Optional + +- `description` (String) Description of the routing table. +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `region` (String) The resource region. If not defined, the provider region is used. +- `system_routes` (Boolean) + +### Read-Only + +- `created_at` (String) Date-time when the routing table was created +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`region`,`network_area_id`,`routing_table_id`". +- `routing_table_id` (String) The routing tables ID. +- `updated_at` (String) Date-time when the routing table was updated diff --git a/docs/resources/routing_table_route.md b/docs/resources/routing_table_route.md new file mode 100644 index 00000000..5b37be89 --- /dev/null +++ b/docs/resources/routing_table_route.md @@ -0,0 +1,78 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "stackit_routing_table_route Resource - stackit" +subcategory: "" +description: |- + Routing table route resource schema. Must have a region specified in the provider configuration. + ~> This resource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. +--- + +# stackit_routing_table_route (Resource) + +Routing table route resource schema. Must have a `region` specified in the provider configuration. + +~> This resource is part of the routing-tables experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion. + +## Example Usage + +```terraform +resource "stackit_routing_table_route" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + destination = { + type = "cidrv4" + value = "192.168.178.0/24" + } + next_hop = { + type = "ipv4" + value = "192.168.178.1" + } + labels = { + "key" = "value" + } +} +``` + + +## Schema + +### Required + +- `destination` (Attributes) Destination of the route. (see [below for nested schema](#nestedatt--destination)) +- `network_area_id` (String) The network area ID to which the routing table is associated. +- `next_hop` (Attributes) Next hop destination. (see [below for nested schema](#nestedatt--next_hop)) +- `organization_id` (String) STACKIT organization ID to which the routing table is associated. +- `routing_table_id` (String) The routing tables ID. + +### Optional + +- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container +- `region` (String) The resource region. If not defined, the provider region is used. + +### Read-Only + +- `created_at` (String) Date-time when the route was created. +- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`". +- `route_id` (String) The ID of the route. +- `updated_at` (String) Date-time when the route was updated. + + +### Nested Schema for `destination` + +Required: + +- `type` (String) CIDRV type. Possible values are: `cidrv4`, `cidrv6`. Only `cidrv4` is supported during experimental stage. +- `value` (String) An CIDR string. + + + +### Nested Schema for `next_hop` + +Required: + +- `type` (String) Possible values are: `blackhole`, `internet`, `ipv4`, `ipv6`. Only `cidrv4` is supported during experimental stage.. + +Optional: + +- `value` (String) Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage. diff --git a/examples/data-sources/stackit_routing_table/data-source.tf b/examples/data-sources/stackit_routing_table/data-source.tf new file mode 100644 index 00000000..575ab17d --- /dev/null +++ b/examples/data-sources/stackit_routing_table/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_routing_table" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/data-sources/stackit_routing_table_route/data-source.tf b/examples/data-sources/stackit_routing_table_route/data-source.tf new file mode 100644 index 00000000..630b9dec --- /dev/null +++ b/examples/data-sources/stackit_routing_table_route/data-source.tf @@ -0,0 +1,6 @@ +data "stackit_routing_table_route" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + route_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/data-sources/stackit_routing_table_routes/data-source.tf b/examples/data-sources/stackit_routing_table_routes/data-source.tf new file mode 100644 index 00000000..badf79c3 --- /dev/null +++ b/examples/data-sources/stackit_routing_table_routes/data-source.tf @@ -0,0 +1,5 @@ +data "stackit_routing_table_routes" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/data-sources/stackit_routing_tables/data-source.tf b/examples/data-sources/stackit_routing_tables/data-source.tf new file mode 100644 index 00000000..f71527d7 --- /dev/null +++ b/examples/data-sources/stackit_routing_tables/data-source.tf @@ -0,0 +1,4 @@ +data "stackit_routing_tables" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" +} diff --git a/examples/resources/stackit_routing_table/resource.tf b/examples/resources/stackit_routing_table/resource.tf new file mode 100644 index 00000000..8841401f --- /dev/null +++ b/examples/resources/stackit_routing_table/resource.tf @@ -0,0 +1,8 @@ +resource "stackit_routing_table" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + name = "example" + labels = { + "key" = "value" + } +} diff --git a/examples/resources/stackit_routing_table_route/resource.tf b/examples/resources/stackit_routing_table_route/resource.tf new file mode 100644 index 00000000..820da292 --- /dev/null +++ b/examples/resources/stackit_routing_table_route/resource.tf @@ -0,0 +1,16 @@ +resource "stackit_routing_table_route" "example" { + organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + routing_table_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + destination = { + type = "cidrv4" + value = "192.168.178.0/24" + } + next_hop = { + type = "ipv4" + value = "192.168.178.1" + } + labels = { + "key" = "value" + } +} \ No newline at end of file diff --git a/go.mod b/go.mod index 9aa54d43..4f01ddac 100644 --- a/go.mod +++ b/go.mod @@ -16,6 +16,7 @@ require ( github.com/stackitcloud/stackit-sdk-go/services/dns v0.17.0 github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0 github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0 + github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0 github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.25.0 diff --git a/go.sum b/go.sum index f518c651..c2bcef40 100644 --- a/go.sum +++ b/go.sum @@ -162,6 +162,8 @@ github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0 h1:C+8z3MdvnTngcH9L72 github.com/stackitcloud/stackit-sdk-go/services/git v0.6.0/go.mod h1:agI7SONeLR/IZL3TOgn1tDzfS63O2rWKQE8+huRjEzU= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0 h1:7qm/Tft79wFlHomPdgjUJ9uJU8kEk+k9ficMGRoHtf0= github.com/stackitcloud/stackit-sdk-go/services/iaas v0.26.0/go.mod h1:lUGkcbyMkd4nRBDFmKohIwlgtOZqQo4Ek5S5ajw90Xg= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha h1:HnQyJSXbtYzN9IhTO02zxLrcSxyauIbeJD+GTf23A50= +github.com/stackitcloud/stackit-sdk-go/services/iaasalpha v0.1.19-alpha/go.mod h1:Wt77ucOwpe9g/84LijU+YhWbn3vLcpkAoRy2i+FobNQ= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0 h1:Ef4SyTBjIkfwaws4mssa6AoK+OokHFtr7ZIflUpoXVE= github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v1.4.0/go.mod h1:FiVhDlw9+yuTiUmnyGLn2qpsLW26w9OC4TS1y78czvg= github.com/stackitcloud/stackit-sdk-go/services/logme v0.25.0 h1:QKOfaB7EcuJmBCxpFXN2K7g2ih0gQM6cyZ1VhTmtQfI= diff --git a/stackit/internal/core/core.go b/stackit/internal/core/core.go index 293afa32..e477c906 100644 --- a/stackit/internal/core/core.go +++ b/stackit/internal/core/core.go @@ -15,6 +15,13 @@ import ( // Separator used for concatenation of TF-internal resource ID const Separator = "," +type ResourceType string + +const ( + Resource ResourceType = "resource" + Datasource ResourceType = "datasource" +) + type ProviderData struct { RoundTripper http.RoundTripper ServiceAccountEmail string // Deprecated: ServiceAccountEmail is not required and will be removed after 12th June 2025. @@ -100,14 +107,14 @@ func LogAndAddWarning(ctx context.Context, diags *diag.Diagnostics, summary, det diags.AddWarning(summary, detail) } -func LogAndAddWarningBeta(ctx context.Context, diags *diag.Diagnostics, name, resourceType string) { +func LogAndAddWarningBeta(ctx context.Context, diags *diag.Diagnostics, name string, resourceType ResourceType) { warnTitle := fmt.Sprintf("The %s %q is in beta", resourceType, name) warnContent := fmt.Sprintf("The %s %q is in beta and may be subject to breaking changes in the future. Use with caution.", resourceType, name) tflog.Warn(ctx, fmt.Sprintf("%s | %s", warnTitle, warnContent)) diags.AddWarning(warnTitle, warnContent) } -func LogAndAddErrorBeta(ctx context.Context, diags *diag.Diagnostics, name, resourceType string) { +func LogAndAddErrorBeta(ctx context.Context, diags *diag.Diagnostics, name string, resourceType ResourceType) { errTitle := fmt.Sprintf("The %s %q is in beta and beta is not enabled", resourceType, name) errContent := fmt.Sprintf(`The %s %q is in beta and the beta functionality is currently not enabled. To enable it, set the environment variable STACKIT_TF_ENABLE_BETA_RESOURCES to "true" or set the "enable_beta_resources" provider field to true.`, resourceType, name) tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) diff --git a/stackit/internal/features/beta.go b/stackit/internal/features/beta.go index afc599ed..4354fbd5 100644 --- a/stackit/internal/features/beta.go +++ b/stackit/internal/features/beta.go @@ -39,7 +39,7 @@ Defaulting to the provider feature flag.`, value) // // Should be called in the Configure method of a beta resource. // Then, check for Errors in the diags using the diags.HasError() method. -func CheckBetaResourcesEnabled(ctx context.Context, data *core.ProviderData, diags *diag.Diagnostics, resourceName, resourceType string) { +func CheckBetaResourcesEnabled(ctx context.Context, data *core.ProviderData, diags *diag.Diagnostics, resourceName string, resourceType core.ResourceType) { if !BetaResourcesEnabled(ctx, data, diags) { core.LogAndAddErrorBeta(ctx, diags, resourceName, resourceType) return @@ -47,11 +47,11 @@ func CheckBetaResourcesEnabled(ctx context.Context, data *core.ProviderData, dia core.LogAndAddWarningBeta(ctx, diags, resourceName, resourceType) } -func AddBetaDescription(description string) string { +func AddBetaDescription(description string, resourceType core.ResourceType) string { // Callout block: https://developer.hashicorp.com/terraform/registry/providers/docs#callouts return fmt.Sprintf("%s\n\n~> %s %s", description, - "This resource is in beta and may be subject to breaking changes in the future. Use with caution.", + fmt.Sprintf("This %s is in beta and may be subject to breaking changes in the future. Use with caution.", resourceType), "See our [guide](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/guides/opting_into_beta_resources) for how to opt-in to use beta resources.", ) } diff --git a/stackit/internal/features/experiments.go b/stackit/internal/features/experiments.go index aee7debe..a2d2f622 100644 --- a/stackit/internal/features/experiments.go +++ b/stackit/internal/features/experiments.go @@ -11,7 +11,11 @@ import ( "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" ) -var AvailableExperiments []string = []string{"iam"} +const ( + RoutingTablesExperiment = "routing-tables" +) + +var AvailableExperiments = []string{"iam", RoutingTablesExperiment} // Check if an experiment is valid. func ValidExperiment(experiment string, diags *diag.Diagnostics) bool { @@ -26,7 +30,7 @@ func ValidExperiment(experiment string, diags *diag.Diagnostics) bool { } // Check if an experiment is enabled. -func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experiment, resourceType string, diags *diag.Diagnostics) { +func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experiment, resourceName string, resourceType core.ResourceType, diags *diag.Diagnostics) { if !ValidExperiment(experiment, diags) { errTitle := fmt.Sprintf("The experiment %s does not exist.", experiment) errContent := "This is a bug in the STACKIT Terraform Provider. Please open an issue here: https://github.com/stackitcloud/terraform-provider-stackit/issues" @@ -38,23 +42,25 @@ func CheckExperimentEnabled(ctx context.Context, data *core.ProviderData, experi }) if experimentActive { - warnTitle := fmt.Sprintf("%s is part of the %s experiment.", resourceType, experiment) - warnContent := fmt.Sprintf("This resource is part of the %s experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.", experiment) + warnTitle := fmt.Sprintf("%s is part of the %s experiment.", resourceName, experiment) + warnContent := fmt.Sprintf("This %s is part of the %s experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.", resourceType, experiment) tflog.Warn(ctx, fmt.Sprintf("%s | %s", warnTitle, warnContent)) diags.AddWarning(warnTitle, warnContent) return } - errTitle := fmt.Sprintf("%s is part of the %s experiment, which is currently disabled by default", resourceType, experiment) + errTitle := fmt.Sprintf("%s is part of the %s experiment, which is currently disabled by default", resourceName, experiment) errContent := fmt.Sprintf(`Enable the %s experiment by adding it into your provider block.`, experiment) tflog.Error(ctx, fmt.Sprintf("%s | %s", errTitle, errContent)) diags.AddError(errTitle, errContent) } -func AddExperimentDescription(description, experiment string) string { +func AddExperimentDescription(description, experiment string, resourceType core.ResourceType) string { // Callout block: https://developer.hashicorp.com/terraform/registry/providers/docs#callouts - return fmt.Sprintf("%s\n\n~> %s%s%s", + return fmt.Sprintf("%s\n\n~> %s%s%s%s%s", description, - "This resource is part of the ", + "This ", + resourceType, + " is part of the ", experiment, " experiment and is likely going to undergo significant changes or be removed in the future. Use it at your own discretion.", ) diff --git a/stackit/internal/features/experiments_test.go b/stackit/internal/features/experiments_test.go index bf26f3b9..392469ae 100644 --- a/stackit/internal/features/experiments_test.go +++ b/stackit/internal/features/experiments_test.go @@ -49,7 +49,8 @@ func TestCheckExperimentEnabled(t *testing.T) { ctx context.Context data *core.ProviderData experiment string - resourceType string + resourceName string + resourceType core.ResourceType diags *diag.Diagnostics } tests := []struct { @@ -65,8 +66,9 @@ func TestCheckExperimentEnabled(t *testing.T) { data: &core.ProviderData{ Experiments: []string{"iam"}, }, - experiment: "iam", - diags: &diag.Diagnostics{}, + experiment: "iam", + resourceType: core.Resource, + diags: &diag.Diagnostics{}, }, wantDiagsErr: false, wantDiagsWarning: true, @@ -78,8 +80,9 @@ func TestCheckExperimentEnabled(t *testing.T) { data: &core.ProviderData{ Experiments: []string{}, }, - experiment: "iam", - diags: &diag.Diagnostics{}, + experiment: "iam", + resourceType: core.Resource, + diags: &diag.Diagnostics{}, }, wantDiagsErr: true, wantDiagsWarning: false, @@ -92,7 +95,7 @@ func TestCheckExperimentEnabled(t *testing.T) { Experiments: []string{"iam"}, }, experiment: "foobar", - resourceType: "provider", + resourceType: core.Resource, diags: &diag.Diagnostics{}, }, wantDiagsErr: true, @@ -101,7 +104,7 @@ func TestCheckExperimentEnabled(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - CheckExperimentEnabled(tt.args.ctx, tt.args.data, tt.args.experiment, tt.args.resourceType, tt.args.diags) + CheckExperimentEnabled(tt.args.ctx, tt.args.data, tt.args.experiment, tt.args.resourceName, tt.args.resourceType, tt.args.diags) if got := tt.args.diags.HasError(); got != tt.wantDiagsErr { t.Errorf("CheckExperimentEnabled() diags.HasError() = %v, want %v", got, tt.wantDiagsErr) } diff --git a/stackit/internal/services/authorization/roleassignments/resource.go b/stackit/internal/services/authorization/roleassignments/resource.go index 0bfc2f6d..7b106bff 100644 --- a/stackit/internal/services/authorization/roleassignments/resource.go +++ b/stackit/internal/services/authorization/roleassignments/resource.go @@ -84,7 +84,7 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con return } - features.CheckExperimentEnabled(ctx, &providerData, experiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), &resp.Diagnostics) + features.CheckExperimentEnabled(ctx, &providerData, experiment, fmt.Sprintf("stackit_authorization_%s_role_assignment", r.apiName), core.Resource, &resp.Diagnostics) if resp.Diagnostics.HasError() { return } @@ -100,7 +100,7 @@ func (r *roleAssignmentResource) Configure(ctx context.Context, req resource.Con // Schema defines the schema for the resource. func (r *roleAssignmentResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { descriptions := map[string]string{ - "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", r.apiName), experiment), + "main": features.AddExperimentDescription(fmt.Sprintf("%s Role Assignment resource schema.", r.apiName), experiment, core.Resource), "id": "Terraform's internal resource identifier. It is structured as \"[resource_id],[role],[subject]\".", "resource_id": fmt.Sprintf("%s Resource to assign the role to.", r.apiName), "role": "Role to be assigned", diff --git a/stackit/internal/services/cdn/customdomain/datasource.go b/stackit/internal/services/cdn/customdomain/datasource.go index acf3f4df..19bb27f3 100644 --- a/stackit/internal/services/cdn/customdomain/datasource.go +++ b/stackit/internal/services/cdn/customdomain/datasource.go @@ -41,7 +41,7 @@ func (d *customDomainDataSource) Configure(ctx context.Context, req datasource.C return } - features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", "datasource") + features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_cdn_custom_domain", core.Datasource) if resp.Diagnostics.HasError() { return } @@ -60,7 +60,7 @@ func (r *customDomainDataSource) Metadata(_ context.Context, req datasource.Meta func (r *customDomainDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."), + MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource), Description: "CDN distribution data source schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/cdn/customdomain/resource.go b/stackit/internal/services/cdn/customdomain/resource.go index 45a32604..5e7bc94d 100644 --- a/stackit/internal/services/cdn/customdomain/resource.go +++ b/stackit/internal/services/cdn/customdomain/resource.go @@ -88,7 +88,7 @@ func (r *customDomainResource) Metadata(_ context.Context, req resource.Metadata func (r *customDomainResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."), + MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), Description: "CDN distribution data source schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/cdn/distribution/datasource.go b/stackit/internal/services/cdn/distribution/datasource.go index cfad1f5a..d692f3e3 100644 --- a/stackit/internal/services/cdn/distribution/datasource.go +++ b/stackit/internal/services/cdn/distribution/datasource.go @@ -58,7 +58,7 @@ func (r *distributionDataSource) Metadata(_ context.Context, req datasource.Meta func (r *distributionDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { backendOptions := []string{"http"} resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."), + MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Datasource), Description: "CDN distribution data source schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/cdn/distribution/resource.go b/stackit/internal/services/cdn/distribution/resource.go index 39b66310..22e4af62 100644 --- a/stackit/internal/services/cdn/distribution/resource.go +++ b/stackit/internal/services/cdn/distribution/resource.go @@ -137,7 +137,7 @@ func (r *distributionResource) Metadata(_ context.Context, req resource.Metadata func (r *distributionResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { backendOptions := []string{"http"} resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema."), + MarkdownDescription: features.AddBetaDescription("CDN distribution data source schema.", core.Resource), Description: "CDN distribution data source schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/git/instance/datasource.go b/stackit/internal/services/git/instance/datasource.go index 48931d77..b54d189a 100644 --- a/stackit/internal/services/git/instance/datasource.go +++ b/stackit/internal/services/git/instance/datasource.go @@ -63,7 +63,7 @@ func (g *gitDataSource) Metadata(_ context.Context, req datasource.MetadataReque // Schema defines the schema for the git data source. func (g *gitDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("Git Instance datasource schema."), + MarkdownDescription: features.AddBetaDescription("Git Instance datasource schema.", core.Datasource), Description: "Git Instance datasource schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/git/instance/resource.go b/stackit/internal/services/git/instance/resource.go index 19226bab..ccc963eb 100644 --- a/stackit/internal/services/git/instance/resource.go +++ b/stackit/internal/services/git/instance/resource.go @@ -94,7 +94,7 @@ func (g *gitResource) Metadata(_ context.Context, req resource.MetadataRequest, // Schema defines the schema for the resource. func (g *gitResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ - MarkdownDescription: features.AddBetaDescription("Git Instance resource schema."), + MarkdownDescription: features.AddBetaDescription("Git Instance resource schema.", core.Resource), Description: "Git Instance resource schema.", Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ diff --git a/stackit/internal/services/iaas/securitygrouprule/resource.go b/stackit/internal/services/iaas/securitygrouprule/resource.go index c8e4722e..735e88cc 100644 --- a/stackit/internal/services/iaas/securitygrouprule/resource.go +++ b/stackit/internal/services/iaas/securitygrouprule/resource.go @@ -333,7 +333,7 @@ func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaR }, Attributes: map[string]schema.Attribute{ "name": schema.StringAttribute{ - Description: fmt.Sprintf("The protocol name which the rule should match. Either `name` or `number` must be provided. %s", utils.FormatPossibleValues(protocolsPossibleValues)), + Description: fmt.Sprintf("The protocol name which the rule should match. Either `name` or `number` must be provided. %s", utils.FormatPossibleValues(protocolsPossibleValues...)), Optional: true, Computed: true, Validators: []validator.String{ diff --git a/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go new file mode 100644 index 00000000..2522a7fb --- /dev/null +++ b/stackit/internal/services/iaasalpha/iaasalpha_acc_test.go @@ -0,0 +1,832 @@ +package iaasalpha_test + +import ( + "context" + _ "embed" + "errors" + "fmt" + "net/http" + "strings" + "sync" + "testing" + + "github.com/hashicorp/terraform-plugin-testing/config" + "github.com/hashicorp/terraform-plugin-testing/helper/acctest" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + stackitSdkConfig "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/core/oapierror" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + + "maps" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/testutil" +) + +// TODO: create network area using terraform resource instead once it's out of experimental stage and GA +const ( + testNetworkAreaId = "25bbf23a-8134-4439-9f5e-1641caf8354e" +) + +var ( + //go:embed testdata/resource-routingtable-min.tf + resourceRoutingTableMinConfig string + + //go:embed testdata/resource-routingtable-max.tf + resourceRoutingTableMaxConfig string + + //go:embed testdata/resource-routingtable-route-min.tf + resourceRoutingTableRouteMinConfig string + + //go:embed testdata/resource-routingtable-route-max.tf + resourceRoutingTableRouteMaxConfig string +) + +var testConfigRoutingTableMin = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "network_area_id": config.StringVariable(testNetworkAreaId), + "name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), +} + +var testConfigRoutingTableMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigRoutingTableMin) + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))) + return updatedConfig +}() + +var testConfigRoutingTableMax = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "network_area_id": config.StringVariable(testNetworkAreaId), + "name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "description": config.StringVariable("This is the description of the routing table."), + "label": config.StringVariable("routing-table-label-01"), + "system_routes": config.BoolVariable(false), + "region": config.StringVariable(testutil.Region), +} + +var testConfigRoutingTableMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + for k, v := range testConfigRoutingTableMax { + updatedConfig[k] = v + } + updatedConfig["name"] = config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))) + updatedConfig["description"] = config.StringVariable("This is the updated description of the routing table.") + updatedConfig["label"] = config.StringVariable("routing-table-updated-label-01") + return updatedConfig +}() + +var testConfigRoutingTableRouteMin = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "network_area_id": config.StringVariable(testNetworkAreaId), + "routing_table_name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "destination_type": config.StringVariable("cidrv4"), + "destination_value": config.StringVariable("192.168.178.0/24"), + "next_hop_type": config.StringVariable("ipv4"), + "next_hop_value": config.StringVariable("192.168.178.1"), +} + +var testConfigRoutingTableRouteMinUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigRoutingTableRouteMin) + // nothing possible to update of the required attributes... + return updatedConfig +}() + +var testConfigRoutingTableRouteMax = config.Variables{ + "organization_id": config.StringVariable(testutil.OrganizationId), + "network_area_id": config.StringVariable(testNetworkAreaId), + "routing_table_name": config.StringVariable(fmt.Sprintf("acc-test-%s", acctest.RandStringFromCharSet(5, acctest.CharSetAlphaNum))), + "destination_type": config.StringVariable("cidrv4"), // TODO: use cidrv6 once it's supported as we already test cidrv4 in the min test + "destination_value": config.StringVariable("192.168.178.0/24"), + "next_hop_type": config.StringVariable("ipv4"), // TODO: use ipv6, internet or blackhole once they are supported as we already test ipv4 in the min test + "next_hop_value": config.StringVariable("192.168.178.1"), + "label": config.StringVariable("route-label-01"), +} + +var testConfigRoutingTableRouteMaxUpdated = func() config.Variables { + updatedConfig := config.Variables{} + maps.Copy(updatedConfig, testConfigRoutingTableRouteMax) + updatedConfig["label"] = config.StringVariable("route-updated-label-01") + return updatedConfig +}() + +// execute routingtable and routingtable route min and max tests with t.Run() to prevent parallel runs (needed for tests of stackit_routing_tables datasource) +func TestAccRoutingTable(t *testing.T) { + t.Run("TestAccRoutingTableMin", func(t *testing.T) { + t.Logf("TestAccRoutingTableMin name: %s", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigRoutingTableMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Data sources + { + ConfigVariables: testConfigRoutingTableMin, + Config: fmt.Sprintf(` + %s + %s + + # single routing table + data "stackit_routing_table" "routing_table" { + organization_id = stackit_routing_table.routing_table.organization_id + network_area_id = stackit_routing_table.routing_table.network_area_id + routing_table_id = stackit_routing_table.routing_table.routing_table_id + } + + # all routing tables in network area + data "stackit_routing_tables" "routing_tables" { + organization_id = stackit_routing_table.routing_table.organization_id + network_area_id = stackit_routing_table.routing_table.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "data.stackit_routing_table.routing_table", "routing_table_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("data.stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "default", "false"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), + + // Routing tables + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMin["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "region", testutil.Region), + // there will be always two routing tables because of the main routing table of the network area + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.#", "2"), + + // default routing table + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.0.default", "true"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.updated_at"), + + // second routing table managed via terraform + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "data.stackit_routing_tables.routing_tables", "items.1.routing_table_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.name", testutil.ConvertConfigVariable(testConfigRoutingTableMin["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.%", "0"), + resource.TestCheckNoResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.description"), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.system_routes", "true"), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.default", "false"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.updated_at"), + ), + }, + // Import + { + ConfigVariables: testConfigRoutingTableMinUpdated, + ResourceName: "stackit_routing_table.routing_table", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_routing_table.routing_table"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_routing_table.routing_table") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + routingTableId, ok := r.Primary.Attributes["routing_table_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute routing_table_id") + } + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigRoutingTableMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMinUpdated["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "0"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.Region), + resource.TestCheckNoResourceAttr("stackit_routing_table.routing_table", "description"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", "true"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) + }) + + t.Run("TestAccRoutingTableMax", func(t *testing.T) { + t.Logf("TestAccRoutingTableMax name: %s", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])) + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigRoutingTableMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Data sources + { + ConfigVariables: testConfigRoutingTableMax, + Config: fmt.Sprintf(` + %s + %s + + # single routing table + data "stackit_routing_table" "routing_table" { + organization_id = stackit_routing_table.routing_table.organization_id + network_area_id = stackit_routing_table.routing_table.network_area_id + routing_table_id = stackit_routing_table.routing_table.routing_table_id + } + + # all routing tables in network area + data "stackit_routing_tables" "routing_tables" { + organization_id = stackit_routing_table.routing_table.organization_id + network_area_id = stackit_routing_table.routing_table.network_area_id + } + `, + testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "data.stackit_routing_table.routing_table", "routing_table_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), + resource.TestCheckResourceAttr("data.stackit_routing_table.routing_table", "default", "false"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table.routing_table", "updated_at"), + + // Routing tables + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMax["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMax["region"])), + // there will be always two routing tables because of the main routing table of the network area + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.#", "2"), + + // default routing table + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.0.default", "true"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.0.updated_at"), + + // second routing table managed via terraform + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "data.stackit_routing_tables.routing_tables", "items.1.routing_table_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.name", testutil.ConvertConfigVariable(testConfigRoutingTableMax["name"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMax["label"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.description", testutil.ConvertConfigVariable(testConfigRoutingTableMax["description"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMax["system_routes"])), + resource.TestCheckResourceAttr("data.stackit_routing_tables.routing_tables", "items.1.default", "false"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_tables.routing_tables", "items.1.updated_at"), + ), + }, + // Import + { + ConfigVariables: testConfigRoutingTableMaxUpdated, + ResourceName: "stackit_routing_table.routing_table", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_routing_table.routing_table"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_routing_table.routing_table") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + routingTableId, ok := r.Primary.Attributes["routing_table_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute routing_table_id") + } + return fmt.Sprintf("%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigRoutingTableMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["name"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["label"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "region", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["region"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "description", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["description"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "system_routes", testutil.ConvertConfigVariable(testConfigRoutingTableMaxUpdated["system_routes"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "updated_at"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) + }) + + t.Run("TestAccRoutingTableRouteMin", func(t *testing.T) { + t.Logf("TestAccRoutingTableRouteMin") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigRoutingTableRouteMin, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["routing_table_name"])), + + // Routing table route + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "0"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), + ), + }, + // Data sources + { + ConfigVariables: testConfigRoutingTableRouteMin, + Config: fmt.Sprintf(` + %s + %s + + # single routing table route + data "stackit_routing_table_route" "route" { + organization_id = stackit_routing_table_route.route.organization_id + network_area_id = stackit_routing_table_route.route.network_area_id + routing_table_id = stackit_routing_table_route.route.routing_table_id + route_id = stackit_routing_table_route.route.route_id + } + + # all routing table routes in routing table + data "stackit_routing_table_routes" "routes" { + organization_id = stackit_routing_table_route.route.organization_id + network_area_id = stackit_routing_table_route.route.network_area_id + routing_table_id = stackit_routing_table_route.route.routing_table_id + } + `, + testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table route + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "routing_table_id", + "data.stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "route_id", + "data.stackit_routing_table_route.route", "route_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.%", "0"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "updated_at"), + + // Routing table routes + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "routing_table_id", + "data.stackit_routing_table_routes.routes", "routing_table_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "route_id", + "data.stackit_routing_table_routes.routes", "routes.0.route_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["destination_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMin["next_hop_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.%", "0"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.updated_at"), + ), + }, + // Import + { + ConfigVariables: testConfigRoutingTableRouteMinUpdated, + ResourceName: "stackit_routing_table_route.route", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_routing_table_route.route"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_routing_table_route.route") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + routingTableId, ok := r.Primary.Attributes["routing_table_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute routing_table_id") + } + routeId, ok := r.Primary.Attributes["route_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute route_id") + } + return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigRoutingTableRouteMinUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMinConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["routing_table_name"])), + + // Routing table route + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["destination_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["destination_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["next_hop_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMinUpdated["next_hop_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "0"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) + }) + + t.Run("TestAccRoutingTableRouteMax", func(t *testing.T) { + t.Logf("TestAccRoutingTableRouteMax") + resource.Test(t, resource.TestCase{ + ProtoV6ProviderFactories: testutil.TestAccProtoV6ProviderFactories, + CheckDestroy: testAccCheckDestroy, + Steps: []resource.TestStep{ + // Creation + { + ConfigVariables: testConfigRoutingTableRouteMax, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["routing_table_name"])), + + // Routing table route + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), + ), + }, + // Data sources + { + ConfigVariables: testConfigRoutingTableRouteMax, + Config: fmt.Sprintf(` + %s + %s + + # single routing table route + data "stackit_routing_table_route" "route" { + organization_id = stackit_routing_table_route.route.organization_id + network_area_id = stackit_routing_table_route.route.network_area_id + routing_table_id = stackit_routing_table_route.route.routing_table_id + route_id = stackit_routing_table_route.route.route_id + } + + # all routing table routes in routing table + data "stackit_routing_table_routes" "routes" { + organization_id = stackit_routing_table_route.route.organization_id + network_area_id = stackit_routing_table_route.route.network_area_id + routing_table_id = stackit_routing_table_route.route.routing_table_id + } + `, + testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig, + ), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table route + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "routing_table_id", + "data.stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "route_id", + "data.stackit_routing_table_route.route", "route_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_route.route", "updated_at"), + + // Routing table routes + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["organization_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["network_area_id"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "region", testutil.Region), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.#", "1"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "routing_table_id", + "data.stackit_routing_table_routes.routes", "routing_table_id", + ), + resource.TestCheckResourceAttrPair( + "stackit_routing_table_route.route", "route_id", + "data.stackit_routing_table_routes.routes", "routes.0.route_id", + ), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["destination_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_type"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["next_hop_value"])), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.%", "1"), + resource.TestCheckResourceAttr("data.stackit_routing_table_routes.routes", "routes.0.labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMax["label"])), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.created_at"), + resource.TestCheckResourceAttrSet("data.stackit_routing_table_routes.routes", "routes.0.updated_at"), + ), + }, + // Import + { + ConfigVariables: testConfigRoutingTableRouteMaxUpdated, + ResourceName: "stackit_routing_table_route.route", + ImportStateIdFunc: func(s *terraform.State) (string, error) { + r, ok := s.RootModule().Resources["stackit_routing_table_route.route"] + if !ok { + return "", fmt.Errorf("couldn't find resource stackit_routing_table_route.route") + } + region, ok := r.Primary.Attributes["region"] + if !ok { + return "", fmt.Errorf("couldn't find attribute region") + } + networkAreaId, ok := r.Primary.Attributes["network_area_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute network_area_id") + } + routingTableId, ok := r.Primary.Attributes["routing_table_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute routing_table_id") + } + routeId, ok := r.Primary.Attributes["route_id"] + if !ok { + return "", fmt.Errorf("couldn't find attribute route_id") + } + return fmt.Sprintf("%s,%s,%s,%s,%s", testutil.OrganizationId, region, networkAreaId, routingTableId, routeId), nil + }, + ImportState: true, + ImportStateVerify: true, + }, + // Update + { + ConfigVariables: testConfigRoutingTableRouteMaxUpdated, + Config: fmt.Sprintf("%s\n%s", testutil.IaaSProviderConfig(), resourceRoutingTableRouteMaxConfig), + Check: resource.ComposeAggregateTestCheckFunc( + // Routing table + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table.routing_table", "routing_table_id"), + resource.TestCheckResourceAttr("stackit_routing_table.routing_table", "name", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["routing_table_name"])), + + // Routing table route + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "organization_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["organization_id"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "network_area_id", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["network_area_id"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "routing_table_id"), + resource.TestCheckResourceAttrPair( + "stackit_routing_table.routing_table", "routing_table_id", + "stackit_routing_table_route.route", "routing_table_id", + ), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "region", testutil.Region), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["destination_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "destination.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["destination_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.type", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["next_hop_type"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "next_hop.value", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["next_hop_value"])), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.%", "1"), + resource.TestCheckResourceAttr("stackit_routing_table_route.route", "labels.acc-test", testutil.ConvertConfigVariable(testConfigRoutingTableRouteMaxUpdated["label"])), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "created_at"), + resource.TestCheckResourceAttrSet("stackit_routing_table_route.route", "updated_at"), + ), + }, + // Deletion is done by the framework implicitly + }, + }) + }) +} + +func testAccCheckDestroy(s *terraform.State) error { + checkFunctions := []func(s *terraform.State) error{ + testAccCheckRoutingTableDestroy, + testAccCheckRoutingTableRouteDestroy, + } + var errs []error + + wg := sync.WaitGroup{} + wg.Add(len(checkFunctions)) + + for _, f := range checkFunctions { + go func() { + err := f(s) + if err != nil { + errs = append(errs, err) + } + wg.Done() + }() + } + wg.Wait() + return errors.Join(errs...) +} + +func testAccCheckRoutingTableDestroy(s *terraform.State) error { + ctx := context.Background() + var client *iaasalpha.APIClient + var err error + if testutil.IaaSCustomEndpoint == "" { + client, err = iaasalpha.NewAPIClient() + } else { + client, err = iaasalpha.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var errs []error + // routing tables + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_routing_table" { + continue + } + routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] + region := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteRoutingTableFromAreaExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + continue + } + } + errs = append(errs, fmt.Errorf("cannot trigger routing table deletion %q: %w", routingTableId, err)) + } + } + + return errors.Join(errs...) +} + +func testAccCheckRoutingTableRouteDestroy(s *terraform.State) error { + ctx := context.Background() + var client *iaasalpha.APIClient + var err error + if testutil.IaaSCustomEndpoint == "" { + client, err = iaasalpha.NewAPIClient() + } else { + client, err = iaasalpha.NewAPIClient( + stackitSdkConfig.WithEndpoint(testutil.IaaSCustomEndpoint), + ) + } + if err != nil { + return fmt.Errorf("creating client: %w", err) + } + + var errs []error + // routes + for _, rs := range s.RootModule().Resources { + if rs.Type != "stackit_routing_table_route" { + continue + } + routingTableRouteId := strings.Split(rs.Primary.ID, core.Separator)[4] + routingTableId := strings.Split(rs.Primary.ID, core.Separator)[3] + region := strings.Split(rs.Primary.ID, core.Separator)[1] + err := client.DeleteRouteFromRoutingTableExecute(ctx, testutil.OrganizationId, testNetworkAreaId, region, routingTableId, routingTableRouteId) + if err != nil { + var oapiErr *oapierror.GenericOpenAPIError + if errors.As(err, &oapiErr) { + if oapiErr.StatusCode == http.StatusNotFound { + continue + } + } + errs = append(errs, fmt.Errorf("cannot trigger routing table route deletion %q: %w", routingTableId, err)) + } + } + + return errors.Join(errs...) +} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/datasource.go b/stackit/internal/services/iaasalpha/routingtable/route/datasource.go new file mode 100644 index 00000000..51846f69 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/route/datasource.go @@ -0,0 +1,120 @@ +package route + +import ( + "context" + "fmt" + "net/http" + + shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &routingTableRouteDataSource{} +) + +// NewRoutingTableRouteDataSource is a helper function to simplify the provider implementation. +func NewRoutingTableRouteDataSource() datasource.DataSource { + return &routingTableRouteDataSource{} +} + +// routingTableRouteDataSource is the data source implementation. +type routingTableRouteDataSource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *routingTableRouteDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_table_route" +} + +func (d *routingTableRouteDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the data source. +func (d *routingTableRouteDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Routing table route datasource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), + Attributes: shared.GetRouteDataSourceAttributes(), + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *routingTableRouteDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model shared.RouteModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + routeId := model.RouteId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "route_id", routeId) + + routeResp, err := d.client.GetRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, err.Error(), err.Error()) + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading routing table route", + fmt.Sprintf("Routing table route with ID %q, routing table with ID %q or network area with ID %q does not exist in organization %q.", routeId, routingTableId, networkAreaId, organizationId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = shared.MapRouteModel(ctx, routeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table route read") +} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource.go b/stackit/internal/services/iaasalpha/routingtable/route/resource.go new file mode 100644 index 00000000..d10e569f --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/route/resource.go @@ -0,0 +1,516 @@ +package route + +import ( + "context" + "fmt" + "strings" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + sdkUtils "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &routeResource{} + _ resource.ResourceWithConfigure = &routeResource{} + _ resource.ResourceWithImportState = &routeResource{} +) + +// NewRoutingTableRouteResource is a helper function to simplify the provider implementation. +func NewRoutingTableRouteResource() resource.Resource { + return &routeResource{} +} + +// routeResource is the resource implementation. +type routeResource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (r *routeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_table_route" +} + +// Configure adds the provider configured client to the resource. +func (r *routeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table_route", core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "IaaS alpha client configured") +} + +// Schema defines the schema for the resource. +func (r *routeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + description := "Routing table route resource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the routing table is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "routing_table_id": schema.StringAttribute{ + Description: "The routing tables ID.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "route_id": schema.StringAttribute{ + Description: "The ID of the route.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "destination": schema.SingleNestedAttribute{ + Description: "Destination of the route.", + Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported during experimental stage."), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "value": schema.StringAttribute{ + Description: "An CIDR string.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID to which the routing table is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "labels": schema.MapAttribute{ + Description: "Labels are key-value string pairs which can be attached to a resource container", + ElementType: types.StringType, + Optional: true, + }, + "next_hop": schema.SingleNestedAttribute{ + Description: "Next hop destination.", + Required: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: fmt.Sprintf("%s %s.", utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), "Only `cidrv4` is supported during experimental stage."), + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "value": schema.StringAttribute{ + Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage.", + Optional: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + }, + }, + "created_at": schema.StringAttribute{ + Description: "Date-time when the route was created.", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "Date-time when the route was updated.", + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *routeResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + var model shared.RouteModel + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + + // Create new routing table route + payload, err := toCreatePayload(ctx, &model.RouteReadModel) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + routeResp, err := r.client.AddRoutesToRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId).AddRoutesToRoutingTablePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFieldsFromList(ctx, routeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table route", fmt.Sprintf("Processing API payload: %v", err)) + return + } + ctx = tflog.SetField(ctx, "route_id", model.RouteId.ValueString()) + + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table route created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *routeResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model shared.RouteModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + routeId := model.RouteId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "route_id", routeId) + + routeResp, err := r.client.GetRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = shared.MapRouteModel(ctx, routeResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table route", fmt.Sprintf("Processing API payload: %v", err)) + return + } + + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table route read.") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *routeResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model shared.RouteModel + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + routeId := model.RouteId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "route_id", routeId) + + // Retrieve values from state + var stateModel shared.RouteModel + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + route, err := r.client.UpdateRouteOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).UpdateRouteOfRoutingTablePayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = shared.MapRouteModel(ctx, route, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table route", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table route updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *routeResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + var model shared.RouteModel + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + routeId := model.RouteId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "route_id", routeId) + ctx = tflog.SetField(ctx, "region", region) + + // Delete existing routing table route + err := r.client.DeleteRouteFromRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId, routeId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error routing table route", fmt.Sprintf("Calling API: %v", err)) + } + + tflog.Info(ctx, "Routing table route deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the routing table route resource import identifier is: organization_id,region,network_area_id,routing_table_id,route_id +func (r *routeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 5 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" || idParts[4] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing routing table", + fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id],[route_id] Got: %q", req.ID), + ) + return + } + + organizationId := idParts[0] + region := idParts[1] + networkAreaId := idParts[2] + routingTableId := idParts[3] + routeId := idParts[4] + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "route_id", routeId) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("routing_table_id"), routingTableId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("route_id"), routeId)...) + tflog.Info(ctx, "Routing table route state imported") +} + +func mapFieldsFromList(ctx context.Context, routeResp *iaasalpha.RouteListResponse, model *shared.RouteModel, region string) error { + if routeResp == nil || routeResp.Items == nil { + return fmt.Errorf("response input is nil") + } else if len(*routeResp.Items) < 1 { + return fmt.Errorf("no routes found in response") + } else if len(*routeResp.Items) > 1 { + return fmt.Errorf("more than 1 route found in response") + } + + route := (*routeResp.Items)[0] + return shared.MapRouteModel(ctx, &route, model, region) +} + +func toCreatePayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.AddRoutesToRoutingTablePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + nextHopPayload, err := toNextHopPayload(ctx, model) + if err != nil { + return nil, err + } + destinationPayload, err := toDestinationPayload(ctx, model) + if err != nil { + return nil, err + } + + return &iaasalpha.AddRoutesToRoutingTablePayload{ + Items: &[]iaasalpha.Route{ + { + Labels: &labels, + Nexthop: nextHopPayload, + Destination: destinationPayload, + }, + }, + }, nil +} + +func toUpdatePayload(ctx context.Context, model *shared.RouteModel, currentLabels types.Map) (*iaasalpha.UpdateRouteOfRoutingTablePayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &iaasalpha.UpdateRouteOfRoutingTablePayload{ + Labels: &labels, + }, nil +} + +func toNextHopPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.RouteNexthop, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if utils.IsUndefined(model.NextHop) { + return nil, nil + } + + nexthopModel := shared.RouteNextHop{} + diags := model.NextHop.As(ctx, &nexthopModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + + switch nexthopModel.Type.ValueString() { + case "blackhole": + return sdkUtils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop(iaasalpha.NewNexthopBlackhole("blackhole"))), nil + case "internet": + return sdkUtils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop(iaasalpha.NewNexthopInternet("internet"))), nil + case "ipv4": + return sdkUtils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop(iaasalpha.NewNexthopIPv4("ipv4", nexthopModel.Value.ValueString()))), nil + case "ipv6": + return sdkUtils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop(iaasalpha.NewNexthopIPv6("ipv6", nexthopModel.Value.ValueString()))), nil + } + return nil, fmt.Errorf("unknown nexthop type: %s", nexthopModel.Type.ValueString()) +} + +func toDestinationPayload(ctx context.Context, model *shared.RouteReadModel) (*iaasalpha.RouteDestination, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + if utils.IsUndefined(model.Destination) { + return nil, nil + } + + destinationModel := shared.RouteDestination{} + diags := model.Destination.As(ctx, &destinationModel, basetypes.ObjectAsOptions{}) + if diags.HasError() { + return nil, core.DiagsToError(diags) + } + + switch destinationModel.Type.ValueString() { + case "cidrv4": + return sdkUtils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination(iaasalpha.NewDestinationCIDRv4("cidrv4", destinationModel.Value.ValueString()))), nil + case "cidrv6": + return sdkUtils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination(iaasalpha.NewDestinationCIDRv6("cidrv6", destinationModel.Value.ValueString()))), nil + } + return nil, fmt.Errorf("unknown destination type: %s", destinationModel.Type.ValueString()) +} diff --git a/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go new file mode 100644 index 00000000..9d59f855 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/route/resource_test.go @@ -0,0 +1,452 @@ +package route + +import ( + "context" + "fmt" + "reflect" + "testing" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + testRegion = "eu02" +) + +var ( + organizationId = uuid.New() + networkAreaId = uuid.New() + routingTableId = uuid.New() + routeId = uuid.New() +) + +func Test_mapFieldsFromList(t *testing.T) { + type args struct { + routeResp *iaasalpha.RouteListResponse + model *shared.RouteModel + region string + } + tests := []struct { + name string + args args + wantErr bool + expectedModel *shared.RouteModel + }{ + { + name: "response is nil", + args: args{ + model: &shared.RouteModel{}, + routeResp: nil, + }, + wantErr: true, + }, + { + name: "response items is nil", + args: args{ + model: &shared.RouteModel{}, + routeResp: &iaasalpha.RouteListResponse{ + Items: nil, + }, + }, + wantErr: true, + }, + { + name: "model is nil", + args: args{ + model: nil, + routeResp: &iaasalpha.RouteListResponse{ + Items: nil, + }, + }, + wantErr: true, + }, + { + name: "response items is empty", + args: args{ + model: &shared.RouteModel{}, + routeResp: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{}, + }, + }, + wantErr: true, + }, + { + name: "response items contains more than one route", + args: args{ + model: &shared.RouteModel{}, + routeResp: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{ + { + Id: utils.Ptr(uuid.NewString()), + }, + { + Id: utils.Ptr(uuid.NewString()), + }, + }, + }, + }, + wantErr: true, + }, + { + name: "success", + args: args{ + model: &shared.RouteModel{ + RouteReadModel: shared.RouteReadModel{ + RouteId: types.StringNull(), + }, + RoutingTableId: types.StringValue(routingTableId.String()), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + }, + routeResp: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{ + { + Id: utils.Ptr(routeId.String()), + Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( + iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), + )), + Labels: &map[string]interface{}{ + "foo": "bar", + }, + CreatedAt: nil, + UpdatedAt: nil, + }, + }, + }, + region: testRegion, + }, + wantErr: false, + expectedModel: &shared.RouteModel{ + RouteReadModel: shared.RouteReadModel{ + RouteId: types.StringValue(routeId.String()), + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo": types.StringValue("bar"), + }), + CreatedAt: types.StringNull(), + UpdatedAt: types.StringNull(), + }, + Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String(), routingTableId.String(), routeId.String())), + RoutingTableId: types.StringValue(routingTableId.String()), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapFieldsFromList(ctx, tt.args.routeResp, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapFieldsFromList() error = %v, wantErr %v", err, tt.wantErr) + return + } + + diff := cmp.Diff(tt.args.model, tt.expectedModel) + if diff != "" && !tt.wantErr { + t.Fatalf("mapFieldsFromList(): %s", diff) + } + }) + } +} + +func Test_toUpdatePayload(t *testing.T) { + type args struct { + model *shared.RouteModel + currentLabels types.Map + } + tests := []struct { + name string + args args + want *iaasalpha.UpdateRouteOfRoutingTablePayload + wantErr bool + }{ + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "max", + args: args{ + model: &shared.RouteModel{ + RouteReadModel: shared.RouteReadModel{ + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo1": types.StringValue("bar1"), + "foo2": types.StringValue("bar2"), + }), + }, + }, + currentLabels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo1": types.StringValue("foobar"), + "foo3": types.StringValue("bar3"), + }), + }, + want: &iaasalpha.UpdateRouteOfRoutingTablePayload{ + Labels: &map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + "foo3": nil, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got, err := toUpdatePayload(ctx, tt.args.model, tt.args.currentLabels) + if (err != nil) != tt.wantErr { + t.Errorf("toUpdatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("toUpdatePayload(): %s", diff) + } + }) + } +} + +func Test_toNextHopPayload(t *testing.T) { + type args struct { + model *shared.RouteReadModel + } + tests := []struct { + name string + args args + want *iaasalpha.RouteNexthop + wantErr bool + }{ + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "ipv4", + args: args{ + model: &shared.RouteReadModel{ + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( + iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), + )), + }, + { + name: "ipv6", + args: args{ + model: &shared.RouteReadModel{ + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv6"), + "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( + iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), + )), + }, + { + name: "internet", + args: args{ + model: &shared.RouteReadModel{ + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("internet"), + "value": types.StringNull(), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop( + iaasalpha.NewNexthopInternet("internet"), + )), + }, + { + name: "blackhole", + args: args{ + model: &shared.RouteReadModel{ + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("blackhole"), + "value": types.StringNull(), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop( + iaasalpha.NewNexthopBlackhole("blackhole"), + )), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got, err := toNextHopPayload(ctx, tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toNextHopPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toNextHopPayload() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_toDestinationPayload(t *testing.T) { + type args struct { + model *shared.RouteReadModel + } + tests := []struct { + name string + args args + want *iaasalpha.RouteDestination + wantErr bool + }{ + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "cidrv4", + args: args{ + model: &shared.RouteReadModel{ + Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + }, + { + name: "cidrv6", + args: args{ + model: &shared.RouteReadModel{ + Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv6"), + "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), + }), + }, + }, + wantErr: false, + want: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( + iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), + )), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got, err := toDestinationPayload(ctx, tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toDestinationPayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + if !reflect.DeepEqual(got, tt.want) { + t.Errorf("toDestinationPayload() got = %v, want %v", got, tt.want) + } + }) + } +} + +func Test_toCreatePayload(t *testing.T) { + type args struct { + model *shared.RouteReadModel + } + tests := []struct { + name string + args args + want *iaasalpha.AddRoutesToRoutingTablePayload + wantErr bool + }{ + { + name: "model is nil", + args: args{ + model: nil, + }, + wantErr: true, + }, + { + name: "max", + args: args{ + model: &shared.RouteReadModel{ + NextHop: types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + Destination: types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo1": types.StringValue("bar1"), + "foo2": types.StringValue("bar2"), + }), + }, + }, + want: &iaasalpha.AddRoutesToRoutingTablePayload{ + Items: &[]iaasalpha.Route{ + { + Labels: &map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }, + Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( + iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), + )), + Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + }, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + got, err := toCreatePayload(ctx, tt.args.model) + if (err != nil) != tt.wantErr { + t.Errorf("toCreatePayload() error = %v, wantErr %v", err, tt.wantErr) + return + } + diff := cmp.Diff(got, tt.want) + if diff != "" { + t.Fatalf("toCreatePayload(): %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go new file mode 100644 index 00000000..26ea2d8a --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/routes/datasource.go @@ -0,0 +1,183 @@ +package routes + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + shared "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &routingTableRoutesDataSource{} +) + +type RoutingTableRoutesDataSourceModel struct { + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + RoutingTableId types.String `tfsdk:"routing_table_id"` + Region types.String `tfsdk:"region"` + Routes types.List `tfsdk:"routes"` +} + +// NewRoutingTableRoutesDataSource is a helper function to simplify the provider implementation. +func NewRoutingTableRoutesDataSource() datasource.DataSource { + return &routingTableRoutesDataSource{} +} + +// routingTableDataSource is the data source implementation. +type routingTableRoutesDataSource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *routingTableRoutesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_table_routes" +} + +func (d *routingTableRoutesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table_routes", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the data source. +func (d *routingTableRoutesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Routing table routes datasource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), + Attributes: shared.GetRoutesDataSourceAttributes(), + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *routingTableRoutesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model RoutingTableRoutesDataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + networkAreaId := model.NetworkAreaId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + + routesResp, err := d.client.ListRoutesOfRoutingTable(ctx, organizationId, networkAreaId, region, routingTableId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading routes of routing table", + fmt.Sprintf("Routing table with ID %q in network area with ID %q does not exist in organization %q.", routingTableId, networkAreaId, organizationId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = mapDataSourceRoutingTableRoutes(ctx, routesResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table routes", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table routes read") +} + +func mapDataSourceRoutingTableRoutes(ctx context.Context, routes *iaasalpha.RouteListResponse, model *RoutingTableRoutesDataSourceModel, region string) error { + if routes == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + if routes.Items == nil { + return fmt.Errorf("items input is nil") + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + + idParts := []string{organizationId, region, networkAreaId, routingTableId} + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + itemsList := []attr.Value{} + for i, route := range *routes.Items { + var routeModel shared.RouteReadModel + err := shared.MapRouteReadModel(ctx, &route, &routeModel) + if err != nil { + return fmt.Errorf("mapping route: %w", err) + } + + routeMap := map[string]attr.Value{ + "route_id": routeModel.RouteId, + "destination": routeModel.Destination, + "next_hop": routeModel.NextHop, + "labels": routeModel.Labels, + "created_at": routeModel.CreatedAt, + "updated_at": routeModel.UpdatedAt, + } + + routeTF, diags := types.ObjectValue(shared.RouteReadModelTypes(), routeMap) + if diags.HasError() { + return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) + } + itemsList = append(itemsList, routeTF) + } + + routesListTF, diags := types.ListValue(types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, itemsList) + if diags.HasError() { + return core.DiagsToError(diags) + } + + model.Region = types.StringValue(region) + model.Routes = routesListTF + + return nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go new file mode 100644 index 00000000..171abd65 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/routes/datasource_test.go @@ -0,0 +1,199 @@ +package routes + +import ( + "context" + "fmt" + "testing" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + testRegion = "eu02" +) + +var ( + testOrganizationId = uuid.NewString() + testNetworkAreaId = uuid.NewString() + testRoutingTableId = uuid.NewString() + testRouteId1 = uuid.NewString() + testRouteId2 = uuid.NewString() +) + +func Test_mapDataSourceRoutingTableRoutes(t *testing.T) { + type args struct { + routes *iaasalpha.RouteListResponse + model *RoutingTableRoutesDataSourceModel + region string + } + tests := []struct { + name string + args args + wantErr bool + expectedModel *RoutingTableRoutesDataSourceModel + }{ + { + name: "model is nil", + args: args{ + model: nil, + routes: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{}, + }, + }, + wantErr: true, + }, + { + name: "response is nil", + args: args{ + model: &RoutingTableRoutesDataSourceModel{}, + routes: nil, + }, + wantErr: true, + }, + { + name: "response items is nil", + args: args{ + model: nil, + routes: &iaasalpha.RouteListResponse{ + Items: nil, + }, + }, + wantErr: true, + }, + { + name: "response items is empty", + args: args{ + model: &RoutingTableRoutesDataSourceModel{ + OrganizationId: types.StringValue(testOrganizationId), + NetworkAreaId: types.StringValue(testNetworkAreaId), + RoutingTableId: types.StringValue(testRoutingTableId), + Region: types.StringValue(testRegion), + }, + routes: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{}, + }, + region: testRegion, + }, + wantErr: false, + expectedModel: &RoutingTableRoutesDataSourceModel{ + Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testOrganizationId, testRegion, testNetworkAreaId, testRoutingTableId)), + OrganizationId: types.StringValue(testOrganizationId), + NetworkAreaId: types.StringValue(testNetworkAreaId), + RoutingTableId: types.StringValue(testRoutingTableId), + Region: types.StringValue(testRegion), + Routes: types.ListValueMust( + types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, []attr.Value{}, + ), + }, + }, + { + name: "response items has items", + args: args{ + model: &RoutingTableRoutesDataSourceModel{ + OrganizationId: types.StringValue(testOrganizationId), + NetworkAreaId: types.StringValue(testNetworkAreaId), + RoutingTableId: types.StringValue(testRoutingTableId), + Region: types.StringValue(testRegion), + }, + routes: &iaasalpha.RouteListResponse{ + Items: &[]iaasalpha.Route{ + { + Id: utils.Ptr(testRouteId1), + Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( + iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), + )), + Labels: &map[string]interface{}{ + "foo": "bar", + }, + CreatedAt: nil, + UpdatedAt: nil, + }, + { + Id: utils.Ptr(testRouteId2), + Destination: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( + iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), + )), + Nexthop: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( + iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), + )), + Labels: &map[string]interface{}{ + "key": "value", + }, + CreatedAt: nil, + UpdatedAt: nil, + }, + }, + }, + region: testRegion, + }, + wantErr: false, + expectedModel: &RoutingTableRoutesDataSourceModel{ + Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s", testOrganizationId, testRegion, testNetworkAreaId, testRoutingTableId)), + OrganizationId: types.StringValue(testOrganizationId), + NetworkAreaId: types.StringValue(testNetworkAreaId), + RoutingTableId: types.StringValue(testRoutingTableId), + Region: types.StringValue(testRegion), + Routes: types.ListValueMust( + types.ObjectType{AttrTypes: shared.RouteReadModelTypes()}, []attr.Value{ + types.ObjectValueMust(shared.RouteReadModelTypes(), map[string]attr.Value{ + "route_id": types.StringValue(testRouteId1), + "created_at": types.StringNull(), + "updated_at": types.StringNull(), + "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo": types.StringValue("bar"), + }), + "destination": types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + "next_hop": types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + }), + types.ObjectValueMust(shared.RouteReadModelTypes(), map[string]attr.Value{ + "route_id": types.StringValue(testRouteId2), + "created_at": types.StringNull(), + "updated_at": types.StringNull(), + "labels": types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + "destination": types.ObjectValueMust(shared.RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv6"), + "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), + }), + "next_hop": types.ObjectValueMust(shared.RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv6"), + "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), + }), + }), + }, + ), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := mapDataSourceRoutingTableRoutes(ctx, tt.args.routes, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("mapDataSourceRoutingTableRoutes() error = %v, wantErr %v", err, tt.wantErr) + return + } + + diff := cmp.Diff(tt.args.model, tt.expectedModel) + if diff != "" && !tt.wantErr { + t.Fatalf("mapFieldsFromList(): %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route.go b/stackit/internal/services/iaasalpha/routingtable/shared/route.go new file mode 100644 index 00000000..e05cf78d --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/shared/route.go @@ -0,0 +1,213 @@ +package shared + +import ( + "context" + "fmt" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" +) + +type RouteReadModel struct { + RouteId types.String `tfsdk:"route_id"` + Destination types.Object `tfsdk:"destination"` + NextHop types.Object `tfsdk:"next_hop"` + Labels types.Map `tfsdk:"labels"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +func RouteReadModelTypes() map[string]attr.Type { + return map[string]attr.Type{ + "route_id": types.StringType, + "destination": types.ObjectType{AttrTypes: RouteDestinationTypes}, + "next_hop": types.ObjectType{AttrTypes: RouteNextHopTypes}, + "labels": types.MapType{ElemType: types.StringType}, + "created_at": types.StringType, + "updated_at": types.StringType, + } +} + +type RouteModel struct { + RouteReadModel + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + RoutingTableId types.String `tfsdk:"routing_table_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Region types.String `tfsdk:"region"` +} + +func RouteModelTypes() map[string]attr.Type { + modelTypes := RouteReadModelTypes() + modelTypes["id"] = types.StringType + modelTypes["organization_id"] = types.StringType + modelTypes["routing_table_id"] = types.StringType + modelTypes["network_area_id"] = types.StringType + modelTypes["region"] = types.StringType + return modelTypes +} + +// RouteDestination is the struct corresponding to RouteReadModel.Destination +type RouteDestination struct { + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` +} + +// RouteDestinationTypes Types corresponding to routeDestination +var RouteDestinationTypes = map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, +} + +// RouteNextHop is the struct corresponding to RouteReadModel.NextHop +type RouteNextHop struct { + Type types.String `tfsdk:"type"` + Value types.String `tfsdk:"value"` +} + +// RouteNextHopTypes Types corresponding to routeNextHop +var RouteNextHopTypes = map[string]attr.Type{ + "type": types.StringType, + "value": types.StringType, +} + +func MapRouteModel(ctx context.Context, route *iaasalpha.Route, model *RouteModel, region string) error { + if route == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + err := MapRouteReadModel(ctx, route, &model.RouteReadModel) + if err != nil { + return err + } + + idParts := []string{ + model.OrganizationId.ValueString(), + region, + model.NetworkAreaId.ValueString(), + model.RoutingTableId.ValueString(), + model.RouteId.ValueString(), + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + model.Region = types.StringValue(region) + + return nil +} + +func MapRouteReadModel(ctx context.Context, route *iaasalpha.Route, model *RouteReadModel) error { + if route == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var routeId string + if model.RouteId.ValueString() != "" { + routeId = model.RouteId.ValueString() + } else if route.Id != nil { + routeId = *route.Id + } else { + return fmt.Errorf("routing table route id not present") + } + + labels, err := iaasUtils.MapLabels(ctx, route.Labels, model.Labels) + if err != nil { + return err + } + + // created at and updated at + createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if route.CreatedAt != nil { + createdAtValue := *route.CreatedAt + createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) + } + if route.UpdatedAt != nil { + updatedAtValue := *route.UpdatedAt + updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) + } + + // destination + model.Destination, err = MapRouteDestination(route) + if err != nil { + return fmt.Errorf("error mapping route destination: %w", err) + } + + // next hop + model.NextHop, err = MapRouteNextHop(route) + if err != nil { + return fmt.Errorf("error mapping route next hop: %w", err) + } + + model.RouteId = types.StringValue(routeId) + model.CreatedAt = createdAtTF + model.UpdatedAt = updatedAtTF + model.Labels = labels + return nil +} + +func MapRouteNextHop(routeResp *iaasalpha.Route) (types.Object, error) { + if routeResp.Nexthop == nil { + return types.ObjectNull(RouteNextHopTypes), nil + } + + nextHopMap := map[string]attr.Value{} + switch i := routeResp.Nexthop.GetActualInstance().(type) { + case *iaasalpha.NexthopIPv4: + nextHopMap["type"] = types.StringValue(*i.Type) + nextHopMap["value"] = types.StringPointerValue(i.Value) + case *iaasalpha.NexthopIPv6: + nextHopMap["type"] = types.StringValue(*i.Type) + nextHopMap["value"] = types.StringPointerValue(i.Value) + case *iaasalpha.NexthopBlackhole: + nextHopMap["type"] = types.StringValue(*i.Type) + nextHopMap["value"] = types.StringNull() + case *iaasalpha.NexthopInternet: + nextHopMap["type"] = types.StringValue(*i.Type) + nextHopMap["value"] = types.StringNull() + default: + return types.ObjectNull(RouteNextHopTypes), fmt.Errorf("unexpected Nexthop type: %T", i) + } + + nextHopTF, diags := types.ObjectValue(RouteNextHopTypes, nextHopMap) + if diags.HasError() { + return types.ObjectNull(RouteNextHopTypes), core.DiagsToError(diags) + } + + return nextHopTF, nil +} + +func MapRouteDestination(routeResp *iaasalpha.Route) (types.Object, error) { + if routeResp.Destination == nil { + return types.ObjectNull(RouteDestinationTypes), nil + } + + destinationMap := map[string]attr.Value{} + switch i := routeResp.Destination.GetActualInstance().(type) { + case *iaasalpha.DestinationCIDRv4: + destinationMap["type"] = types.StringValue(*i.Type) + destinationMap["value"] = types.StringPointerValue(i.Value) + case *iaasalpha.DestinationCIDRv6: + destinationMap["type"] = types.StringValue(*i.Type) + destinationMap["value"] = types.StringPointerValue(i.Value) + default: + return types.ObjectNull(RouteDestinationTypes), fmt.Errorf("unexpected Destionation type: %T", i) + } + + destinationTF, diags := types.ObjectValue(RouteDestinationTypes, destinationMap) + if diags.HasError() { + return types.ObjectNull(RouteDestinationTypes), core.DiagsToError(diags) + } + + return destinationTF, nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go b/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go new file mode 100644 index 00000000..6997ad22 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/shared/route_test.go @@ -0,0 +1,310 @@ +package shared + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + testRegion = "eu02" +) + +var ( + testRouteId = uuid.New() + testOrganizationId = uuid.New() + testNetworkAreaId = uuid.New() + testRoutingTableId = uuid.New() +) + +func Test_MapRouteNextHop(t *testing.T) { + type args struct { + routeResp *iaasalpha.Route + } + tests := []struct { + name string + args args + wantErr bool + expected types.Object + }{ + { + name: "nexthop is nil", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: nil, + }, + }, + wantErr: false, + expected: types.ObjectNull(RouteNextHopTypes), + }, + { + name: "nexthop is empty", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: &iaasalpha.RouteNexthop{}, + }, + }, + wantErr: true, + }, + { + name: "nexthop ipv4", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: utils.Ptr(iaasalpha.NexthopIPv4AsRouteNexthop( + iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + }, + { + name: "nexthop ipv6", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: utils.Ptr(iaasalpha.NexthopIPv6AsRouteNexthop( + iaasalpha.NewNexthopIPv6("ipv6", "172b:f881:46fe:d89a:9332:90f7:3485:236d"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv6"), + "value": types.StringValue("172b:f881:46fe:d89a:9332:90f7:3485:236d"), + }), + }, + { + name: "nexthop internet", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: utils.Ptr(iaasalpha.NexthopInternetAsRouteNexthop( + iaasalpha.NewNexthopInternet("internet"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("internet"), + "value": types.StringNull(), + }), + }, + { + name: "nexthop blackhole", + args: args{ + routeResp: &iaasalpha.Route{ + Nexthop: utils.Ptr(iaasalpha.NexthopBlackholeAsRouteNexthop( + iaasalpha.NewNexthopBlackhole("blackhole"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("blackhole"), + "value": types.StringNull(), + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := MapRouteNextHop(tt.args.routeResp) + if (err != nil) != tt.wantErr { + t.Errorf("mapNextHop() error = %v, wantErr %v", err, tt.wantErr) + } + + diff := cmp.Diff(actual, tt.expected) + if !tt.wantErr && diff != "" { + t.Errorf("mapNextHop() result does not match: %s", diff) + } + }) + } +} + +func Test_MapRouteDestination(t *testing.T) { + type args struct { + routeResp *iaasalpha.Route + } + tests := []struct { + name string + args args + wantErr bool + expected types.Object + }{ + + { + name: "destination is nil", + args: args{ + routeResp: &iaasalpha.Route{ + Destination: nil, + }, + }, + wantErr: false, + expected: types.ObjectNull(RouteDestinationTypes), + }, + { + name: "destination is empty", + args: args{ + routeResp: &iaasalpha.Route{ + Destination: &iaasalpha.RouteDestination{}, + }, + }, + wantErr: true, + }, + { + name: "destination cidrv4", + args: args{ + routeResp: &iaasalpha.Route{ + Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + }, + { + name: "destination cidrv6", + args: args{ + routeResp: &iaasalpha.Route{ + Destination: utils.Ptr(iaasalpha.DestinationCIDRv6AsRouteDestination( + iaasalpha.NewDestinationCIDRv6("cidrv6", "2001:0db8:3c4d:1a2b::/64"), + )), + }, + }, + wantErr: false, + expected: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv6"), + "value": types.StringValue("2001:0db8:3c4d:1a2b::/64"), + }), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + actual, err := MapRouteDestination(tt.args.routeResp) + if (err != nil) != tt.wantErr { + t.Errorf("mapDestination() error = %v, wantErr %v", err, tt.wantErr) + } + + diff := cmp.Diff(actual, tt.expected) + if !tt.wantErr && diff != "" { + t.Errorf("mapDestination() result does not match: %s", diff) + } + }) + } +} + +func TestMapRouteModel(t *testing.T) { + createdAt := time.Now() + updatedAt := time.Now().Add(5 * time.Minute) + + type args struct { + route *iaasalpha.Route + model *RouteModel + region string + } + tests := []struct { + name string + args args + wantErr bool + expectedModel *RouteModel + }{ + { + name: "route is nil", + args: args{ + model: &RouteModel{}, + route: nil, + region: testRegion, + }, + wantErr: true, + }, + { + name: "model is nil", + args: args{ + model: nil, + route: &iaasalpha.Route{}, + region: testRegion, + }, + wantErr: true, + }, + { + name: "max", + args: args{ + model: &RouteModel{ + // state + OrganizationId: types.StringValue(testOrganizationId.String()), + NetworkAreaId: types.StringValue(testNetworkAreaId.String()), + RoutingTableId: types.StringValue(testRoutingTableId.String()), + }, + route: &iaasalpha.Route{ + Id: utils.Ptr(testRouteId.String()), + Destination: utils.Ptr(iaasalpha.DestinationCIDRv4AsRouteDestination( + iaasalpha.NewDestinationCIDRv4("cidrv4", "58.251.236.138/32"), + )), + Labels: &map[string]interface{}{ + "foo1": "bar1", + "foo2": "bar2", + }, + Nexthop: utils.Ptr( + iaasalpha.NexthopIPv4AsRouteNexthop(iaasalpha.NewNexthopIPv4("ipv4", "10.20.42.2")), + ), + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + }, + region: testRegion, + }, + wantErr: false, + expectedModel: &RouteModel{ + Id: types.StringValue(fmt.Sprintf("%s,%s,%s,%s,%s", + testOrganizationId.String(), testRegion, testNetworkAreaId.String(), testRoutingTableId.String(), testRouteId.String()), + ), + OrganizationId: types.StringValue(testOrganizationId.String()), + NetworkAreaId: types.StringValue(testNetworkAreaId.String()), + RoutingTableId: types.StringValue(testRoutingTableId.String()), + RouteReadModel: RouteReadModel{ + RouteId: types.StringValue(testRouteId.String()), + Destination: types.ObjectValueMust(RouteDestinationTypes, map[string]attr.Value{ + "type": types.StringValue("cidrv4"), + "value": types.StringValue("58.251.236.138/32"), + }), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "foo1": types.StringValue("bar1"), + "foo2": types.StringValue("bar2"), + }), + NextHop: types.ObjectValueMust(RouteNextHopTypes, map[string]attr.Value{ + "type": types.StringValue("ipv4"), + "value": types.StringValue("10.20.42.2"), + }), + CreatedAt: types.StringValue(createdAt.Format(time.RFC3339)), + UpdatedAt: types.StringValue(updatedAt.Format(time.RFC3339)), + }, + Region: types.StringValue(testRegion), + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + if err := MapRouteModel(ctx, tt.args.route, tt.args.model, tt.args.region); (err != nil) != tt.wantErr { + t.Errorf("MapRouteModel() error = %v, wantErr %v", err, tt.wantErr) + } + + diff := cmp.Diff(tt.args.model, tt.expectedModel) + if !tt.wantErr && diff != "" { + t.Errorf("MapRouteModel() model does not match: %s", diff) + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/routingtable/shared/shared.go b/stackit/internal/services/iaasalpha/routingtable/shared/shared.go new file mode 100644 index 00000000..ee212e86 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/shared/shared.go @@ -0,0 +1,263 @@ +package shared + +import ( + "context" + "fmt" + "maps" + "time" + + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +type RoutingTableReadModel struct { + RoutingTableId types.String `tfsdk:"routing_table_id"` + Name types.String `tfsdk:"name"` + Description types.String `tfsdk:"description"` + Labels types.Map `tfsdk:"labels"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` + Default types.Bool `tfsdk:"default"` + SystemRoutes types.Bool `tfsdk:"system_routes"` +} + +func RoutingTableReadModelTypes() map[string]attr.Type { + return map[string]attr.Type{ + "routing_table_id": types.StringType, + "name": types.StringType, + "description": types.StringType, + "labels": types.MapType{ElemType: types.StringType}, + "created_at": types.StringType, + "updated_at": types.StringType, + "default": types.BoolType, + "system_routes": types.BoolType, + } +} + +type RoutingTableDataSourceModel struct { + RoutingTableReadModel + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Region types.String `tfsdk:"region"` +} + +func GetDatasourceGetAttributes() map[string]schema.Attribute { + // combine the schemas + getAttributes := RoutingTableResponseAttributes() + maps.Copy(getAttributes, datasourceGetAttributes()) + getAttributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`\".", + Computed: true, + } + return getAttributes +} + +func GetRouteDataSourceAttributes() map[string]schema.Attribute { + getAttributes := datasourceGetAttributes() + maps.Copy(getAttributes, RouteResponseAttributes()) + getAttributes["route_id"] = schema.StringAttribute{ + Description: "Route ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + } + getAttributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", + Computed: true, + } + return getAttributes +} + +func GetRoutesDataSourceAttributes() map[string]schema.Attribute { + getAttributes := datasourceGetAttributes() + getAttributes["id"] = schema.StringAttribute{ + Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`,`route_id`\".", + Computed: true, + } + getAttributes["routes"] = schema.ListNestedAttribute{ + Description: "List of routes.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: RouteResponseAttributes(), + }, + } + getAttributes["region"] = schema.StringAttribute{ + Description: "The datasource region. If not defined, the provider region is used.", + Optional: true, + } + return getAttributes +} + +func datasourceGetAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the routing table is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "routing_table_id": schema.StringAttribute{ + Description: "The routing tables ID.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID to which the routing table is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + Optional: true, + }, + } +} + +func RouteResponseAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "route_id": schema.StringAttribute{ + Description: "Route ID.", + Computed: true, + }, + "destination": schema.SingleNestedAttribute{ + Description: "Destination of the route.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: fmt.Sprintf("CIDRV type. %s %s", utils.FormatPossibleValues("cidrv4", "cidrv6"), "Only `cidrv4` is supported during experimental stage."), + Computed: true, + }, + "value": schema.StringAttribute{ + Description: "An CIDR string.", + Computed: true, + }, + }, + }, + "next_hop": schema.SingleNestedAttribute{ + Description: "Next hop destination.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Description: fmt.Sprintf("%s %s.", utils.FormatPossibleValues("blackhole", "internet", "ipv4", "ipv6"), "Only `cidrv4` is supported during experimental stage."), + Computed: true, + }, + "value": schema.StringAttribute{ + Description: "Either IPv4 or IPv6 (not set for blackhole and internet). Only IPv4 supported during experimental stage.", + Computed: true, + }, + }, + }, + "labels": schema.MapAttribute{ + Description: "Labels are key-value string pairs which can be attached to a resource container", + ElementType: types.StringType, + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "Date-time when the route was created", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "Date-time when the route was updated", + Computed: true, + }, + } +} + +func RoutingTableResponseAttributes() map[string]schema.Attribute { + return map[string]schema.Attribute{ + "routing_table_id": schema.StringAttribute{ + Description: "The routing tables ID.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "The name of the routing table.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description of the routing table.", + Computed: true, + }, + "labels": schema.MapAttribute{ + Description: "Labels are key-value string pairs which can be attached to a resource container", + ElementType: types.StringType, + Computed: true, + }, + "default": schema.BoolAttribute{ + Description: "When true this is the default routing table for this network area. It can't be deleted and is used if the user does not specify it otherwise.", + Computed: true, + }, + "system_routes": schema.BoolAttribute{ + Computed: true, + }, + "created_at": schema.StringAttribute{ + Description: "Date-time when the routing table was created", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "Date-time when the routing table was updated", + Computed: true, + }, + } +} + +func MapRoutingTableReadModel(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *RoutingTableReadModel) error { + if routingTable == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var routingTableId string + if model.RoutingTableId.ValueString() != "" { + routingTableId = model.RoutingTableId.ValueString() + } else if routingTable.Id != nil { + routingTableId = *routingTable.Id + } else { + return fmt.Errorf("routing table id not present") + } + + labels, err := iaasUtils.MapLabels(ctx, routingTable.Labels, model.Labels) + if err != nil { + return err + } + + // created at and updated at + createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if routingTable.CreatedAt != nil { + createdAtValue := *routingTable.CreatedAt + createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) + } + if routingTable.UpdatedAt != nil { + updatedAtValue := *routingTable.UpdatedAt + updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) + } + + model.RoutingTableId = types.StringValue(routingTableId) + model.Name = types.StringPointerValue(routingTable.Name) + model.Description = types.StringPointerValue(routingTable.Description) + model.Default = types.BoolPointerValue(routingTable.Default) + model.SystemRoutes = types.BoolPointerValue(routingTable.SystemRoutes) + model.Labels = labels + model.CreatedAt = createdAtTF + model.UpdatedAt = updatedAtTF + return nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource.go new file mode 100644 index 00000000..61b4ddbe --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/table/datasource.go @@ -0,0 +1,156 @@ +package table + +import ( + "context" + "fmt" + "net/http" + "strings" + + "github.com/hashicorp/terraform-plugin-framework/types" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &routingTableDataSource{} +) + +// NewRoutingTableDataSource is a helper function to simplify the provider implementation. +func NewRoutingTableDataSource() datasource.DataSource { + return &routingTableDataSource{} +} + +// routingTableDataSource is the data source implementation. +type routingTableDataSource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *routingTableDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_table" +} + +func (d *routingTableDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the data source. +func (d *routingTableDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Routing table datasource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), + Attributes: shared.GetDatasourceGetAttributes(), + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *routingTableDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model shared.RoutingTableDataSourceModel + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + routingTableResp, err := d.client.GetRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading routing table", + fmt.Sprintf("Routing table with ID %q or network area with ID %q does not exist in organization %q.", routingTableId, networkAreaId, organizationId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = mapDatasourceFields(ctx, routingTableResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table read") +} + +func mapDatasourceFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *shared.RoutingTableDataSourceModel, region string) error { + if routingTable == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var routingTableId string + if model.RoutingTableId.ValueString() != "" { + routingTableId = model.RoutingTableId.ValueString() + } else if routingTable.Id != nil { + routingTableId = *routingTable.Id + } else { + return fmt.Errorf("routing table id not present") + } + + idParts := []string{ + model.OrganizationId.ValueString(), + region, + model.NetworkAreaId.ValueString(), + routingTableId, + } + model.Id = types.StringValue( + strings.Join(idParts, core.Separator), + ) + + err := shared.MapRoutingTableReadModel(ctx, routingTable, &model.RoutingTableReadModel) + if err != nil { + return err + } + + model.Region = types.StringValue(region) + return nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go new file mode 100644 index 00000000..4622e1b3 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/table/datasource_test.go @@ -0,0 +1,136 @@ +package table + +import ( + "context" + "fmt" + "testing" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + testRegion = "eu01" +) + +var ( + organizationId = uuid.New() + networkAreaId = uuid.New() + routingTableId = uuid.New() +) + +func Test_mapDatasourceFields(t *testing.T) { + id := fmt.Sprintf("%s,%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String(), routingTableId.String()) + + tests := []struct { + description string + state shared.RoutingTableDataSourceModel + input *iaasalpha.RoutingTable + expected shared.RoutingTableDataSourceModel + isValid bool + }{ + { + "default_values", + shared.RoutingTableDataSourceModel{ + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + }, + &iaasalpha.RoutingTable{ + Id: utils.Ptr(routingTableId.String()), + Name: utils.Ptr("default_values"), + }, + shared.RoutingTableDataSourceModel{ + Id: types.StringValue(id), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + RoutingTableReadModel: shared.RoutingTableReadModel{ + RoutingTableId: types.StringValue(routingTableId.String()), + Name: types.StringValue("default_values"), + Labels: types.MapNull(types.StringType), + }, + }, + true, + }, + { + "values_ok", + shared.RoutingTableDataSourceModel{ + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + RoutingTableReadModel: shared.RoutingTableReadModel{}, + }, + &iaasalpha.RoutingTable{ + Id: utils.Ptr(routingTableId.String()), + Name: utils.Ptr("values_ok"), + Description: utils.Ptr("Description"), + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + shared.RoutingTableDataSourceModel{ + Id: types.StringValue(id), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + RoutingTableReadModel: shared.RoutingTableReadModel{ + RoutingTableId: types.StringValue(routingTableId.String()), + Name: types.StringValue("values_ok"), + Description: types.StringValue("Description"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + }, + }, + true, + }, + { + "response_fields_nil_fail", + shared.RoutingTableDataSourceModel{}, + &iaasalpha.RoutingTable{ + Id: nil, + }, + shared.RoutingTableDataSourceModel{}, + false, + }, + { + "response_nil_fail", + shared.RoutingTableDataSourceModel{}, + nil, + shared.RoutingTableDataSourceModel{}, + false, + }, + { + "no_resource_id", + shared.RoutingTableDataSourceModel{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + }, + &iaasalpha.RoutingTable{}, + shared.RoutingTableDataSourceModel{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDatasourceFields(context.Background(), tt.input, &tt.state, testRegion) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource.go b/stackit/internal/services/iaasalpha/routingtable/table/resource.go new file mode 100644 index 00000000..c83c4e1d --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/table/resource.go @@ -0,0 +1,479 @@ +package table + +import ( + "context" + "fmt" + "net/http" + "strings" + "time" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/booldefault" + + "github.com/hashicorp/terraform-plugin-framework/resource/schema/boolplanmodifier" + + iaasUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/utils" + + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &routingTableResource{} + _ resource.ResourceWithConfigure = &routingTableResource{} + _ resource.ResourceWithImportState = &routingTableResource{} +) + +type Model struct { + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + RoutingTableId types.String `tfsdk:"routing_table_id"` + Name types.String `tfsdk:"name"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Description types.String `tfsdk:"description"` + Labels types.Map `tfsdk:"labels"` + Region types.String `tfsdk:"region"` + SystemRoutes types.Bool `tfsdk:"system_routes"` + CreatedAt types.String `tfsdk:"created_at"` + UpdatedAt types.String `tfsdk:"updated_at"` +} + +// NewRoutingTableResource is a helper function to simplify the provider implementation. +func NewRoutingTableResource() resource.Resource { + return &routingTableResource{} +} + +// routingTableResource is the resource implementation. +type routingTableResource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the resource type name. +func (r *routingTableResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_table" +} + +// Configure adds the provider configured client to the resource. +func (r *routingTableResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + var ok bool + r.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &r.providerData, features.RoutingTablesExperiment, "stackit_routing_table", core.Resource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &r.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + r.client = apiClient + tflog.Info(ctx, "IaaS alpha client configured") +} + +func (r *routingTableResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { + description := "Routing table resource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Resource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`region`,`network_area_id`,`routing_table_id`\".", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the routing table is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "routing_table_id": schema.StringAttribute{ + Description: "The routing tables ID.", + Computed: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "name": schema.StringAttribute{ + Description: "The name of the routing table.", + Required: true, + Validators: []validator.String{ + stringvalidator.LengthAtLeast(1), + stringvalidator.LengthAtMost(63), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID to which the routing table is associated.", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "description": schema.StringAttribute{ + Description: "Description of the routing table.", + Optional: true, + Computed: true, + Validators: []validator.String{ + stringvalidator.LengthAtMost(127), + }, + }, + "labels": schema.MapAttribute{ + Description: "Labels are key-value string pairs which can be attached to a resource container", + ElementType: types.StringType, + Optional: true, + }, + "region": schema.StringAttribute{ + Optional: true, + // must be computed to allow for storing the override value from the provider + Computed: true, + Description: "The resource region. If not defined, the provider region is used.", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "system_routes": schema.BoolAttribute{ + Optional: true, + Computed: true, + Default: booldefault.StaticBool(true), + PlanModifiers: []planmodifier.Bool{ + boolplanmodifier.RequiresReplace(), + }, + }, + "created_at": schema.StringAttribute{ + Description: "Date-time when the routing table was created", + Computed: true, + }, + "updated_at": schema.StringAttribute{ + Description: "Date-time when the routing table was updated", + Computed: true, + }, + }, + } +} + +// Create creates the resource and sets the initial Terraform state. +func (r *routingTableResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + // Generate API request body from model + payload, err := toCreatePayload(ctx, &model) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + routingTable, err := r.client.AddRoutingTableToArea(ctx, organizationId, networkAreaId, region).AddRoutingTableToAreaPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, routingTable, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating routing table.", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set state to fully populated data + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table created") +} + +// Read refreshes the Terraform state with the latest data. +func (r *routingTableResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + routingTableResp, err := r.client.GetRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading routing table", + fmt.Sprintf("routing table with ID %q does not exist in organization %q.", routingTableId, organizationId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + // Map response body to schema + err = mapFields(ctx, routingTableResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table read") +} + +// Update updates the resource and sets the updated Terraform state on success. +func (r *routingTableResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from plan + var model Model + diags := req.Plan.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + // Retrieve values from state + var stateModel Model + diags = req.State.Get(ctx, &stateModel) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + // Generate API request body from model + payload, err := toUpdatePayload(ctx, &model, stateModel.Labels) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Creating API payload: %v", err)) + return + } + + routingTable, err := r.client.UpdateRoutingTableOfArea(ctx, organizationId, networkAreaId, region, routingTableId).UpdateRoutingTableOfAreaPayload(*payload).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Calling API: %v", err)) + return + } + + // Map response body to schema + err = mapFields(ctx, routingTable, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating routing table", fmt.Sprintf("Processing API payload: %v", err)) + return + } + // Set refreshed state + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table updated") +} + +// Delete deletes the resource and removes the Terraform state on success. +func (r *routingTableResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { // nolint:gocritic // function signature required by Terraform + // Retrieve values from state + var model Model + diags := req.State.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + region := r.providerData.GetRegionWithOverride(model.Region) + routingTableId := model.RoutingTableId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + // Delete existing routing table + err := r.client.DeleteRoutingTableFromArea(ctx, organizationId, networkAreaId, region, routingTableId).Execute() + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting routing table", fmt.Sprintf("Calling API: %v", err)) + return + } + + tflog.Info(ctx, "Routing table deleted") +} + +// ImportState imports a resource into the Terraform state on success. +// The expected format of the resource import identifier is: organization_id,region,network_area_id,routing_table_id +func (r *routingTableResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + idParts := strings.Split(req.ID, core.Separator) + + if len(idParts) != 4 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" || idParts[3] == "" { + core.LogAndAddError(ctx, &resp.Diagnostics, + "Error importing routing table", + fmt.Sprintf("Expected import identifier with format: [organization_id],[region],[network_area_id],[routing_table_id] Got: %q", req.ID), + ) + return + } + + organizationId := idParts[0] + region := idParts[1] + networkAreaId := idParts[2] + routingTableId := idParts[3] + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + ctx = tflog.SetField(ctx, "routing_table_id", routingTableId) + + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("organization_id"), organizationId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("region"), region)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_area_id"), networkAreaId)...) + resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("routing_table_id"), routingTableId)...) + tflog.Info(ctx, "Routing table state imported") +} + +func mapFields(ctx context.Context, routingTable *iaasalpha.RoutingTable, model *Model, region string) error { + if routingTable == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + + var routingTableId string + if model.RoutingTableId.ValueString() != "" { + routingTableId = model.RoutingTableId.ValueString() + } else if routingTable.Id != nil { + routingTableId = *routingTable.Id + } else { + return fmt.Errorf("routing table id not present") + } + + model.Id = utils.BuildInternalTerraformId(model.OrganizationId.ValueString(), region, model.NetworkAreaId.ValueString(), routingTableId) + + labels, err := iaasUtils.MapLabels(ctx, routingTable.Labels, model.Labels) + if err != nil { + return err + } + + // created at and updated at + createdAtTF, updatedAtTF := types.StringNull(), types.StringNull() + if routingTable.CreatedAt != nil { + createdAtValue := *routingTable.CreatedAt + createdAtTF = types.StringValue(createdAtValue.Format(time.RFC3339)) + } + if routingTable.UpdatedAt != nil { + updatedAtValue := *routingTable.UpdatedAt + updatedAtTF = types.StringValue(updatedAtValue.Format(time.RFC3339)) + } + + model.RoutingTableId = types.StringValue(routingTableId) + model.Name = types.StringPointerValue(routingTable.Name) + model.Description = types.StringPointerValue(routingTable.Description) + model.Labels = labels + model.Region = types.StringValue(region) + model.SystemRoutes = types.BoolPointerValue(routingTable.SystemRoutes) + model.CreatedAt = createdAtTF + model.UpdatedAt = updatedAtTF + return nil +} + +func toCreatePayload(ctx context.Context, model *Model) (*iaasalpha.AddRoutingTableToAreaPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &iaasalpha.AddRoutingTableToAreaPayload{ + Description: conversion.StringValueToPointer(model.Description), + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + SystemRoutes: conversion.BoolValueToPointer(model.SystemRoutes), + }, nil +} + +func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaasalpha.UpdateRoutingTableOfAreaPayload, error) { + if model == nil { + return nil, fmt.Errorf("nil model") + } + + labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels) + if err != nil { + return nil, fmt.Errorf("converting to Go map: %w", err) + } + + return &iaasalpha.UpdateRoutingTableOfAreaPayload{ + Description: conversion.StringValueToPointer(model.Description), + Name: conversion.StringValueToPointer(model.Name), + Labels: &labels, + }, nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go b/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go new file mode 100644 index 00000000..24b1fef9 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/table/resource_test.go @@ -0,0 +1,212 @@ +package table + +import ( + "context" + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +func TestMapFields(t *testing.T) { + const testRegion = "eu01" + id := fmt.Sprintf("%s,%s,%s,%s", "oid", testRegion, "aid", "rtid") + tests := []struct { + description string + state Model + input *iaasalpha.RoutingTable + expected Model + isValid bool + }{ + { + "default_values", + Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("aid"), + }, + &iaasalpha.RoutingTable{ + Id: utils.Ptr("rtid"), + Name: utils.Ptr("default_values"), + }, + Model{ + Id: types.StringValue(id), + OrganizationId: types.StringValue("oid"), + RoutingTableId: types.StringValue("rtid"), + Name: types.StringValue("default_values"), + NetworkAreaId: types.StringValue("aid"), + Labels: types.MapNull(types.StringType), + Region: types.StringValue(testRegion), + }, + true, + }, + { + "values_ok", + Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("aid"), + }, + &iaasalpha.RoutingTable{ + Id: utils.Ptr("rtid"), + Name: utils.Ptr("values_ok"), + Description: utils.Ptr("Description"), + Labels: &map[string]interface{}{ + "key": "value", + }, + }, + Model{ + Id: types.StringValue(id), + OrganizationId: types.StringValue("oid"), + RoutingTableId: types.StringValue("rtid"), + Name: types.StringValue("values_ok"), + Description: types.StringValue("Description"), + NetworkAreaId: types.StringValue("aid"), + Region: types.StringValue(testRegion), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + }, + true, + }, + { + "response_fields_nil_fail", + Model{}, + &iaasalpha.RoutingTable{ + Id: nil, + }, + Model{}, + false, + }, + { + "response_nil_fail", + Model{}, + nil, + Model{}, + false, + }, + { + "no_resource_id", + Model{ + OrganizationId: types.StringValue("oid"), + NetworkAreaId: types.StringValue("naid"), + }, + &iaasalpha.RoutingTable{}, + Model{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapFields(context.Background(), tt.input, &tt.state, testRegion) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToCreatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *iaasalpha.AddRoutingTableToAreaPayload + isValid bool + }{ + { + description: "default_ok", + input: &Model{ + Description: types.StringValue("Description"), + Name: types.StringValue("default_ok"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key": types.StringValue("value"), + }), + SystemRoutes: types.BoolValue(true), + }, + expected: &iaasalpha.AddRoutingTableToAreaPayload{ + Description: utils.Ptr("Description"), + Name: utils.Ptr("default_ok"), + Labels: &map[string]interface{}{ + "key": "value", + }, + SystemRoutes: utils.Ptr(true), + }, + isValid: true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toCreatePayload(context.Background(), tt.input) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} + +func TestToUpdatePayload(t *testing.T) { + tests := []struct { + description string + input *Model + expected *iaasalpha.UpdateRoutingTableOfAreaPayload + isValid bool + }{ + { + "default_ok", + &Model{ + Description: types.StringValue("Description"), + Name: types.StringValue("default_ok"), + Labels: types.MapValueMust(types.StringType, map[string]attr.Value{ + "key1": types.StringValue("value1"), + "key2": types.StringValue("value2"), + }), + }, + &iaasalpha.UpdateRoutingTableOfAreaPayload{ + Description: utils.Ptr("Description"), + Name: utils.Ptr("default_ok"), + Labels: &map[string]interface{}{ + "key1": "value1", + "key2": "value2", + }, + }, + true, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType)) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(output, tt.expected, cmp.AllowUnexported(iaasalpha.NullableString{})) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go new file mode 100644 index 00000000..eac2257c --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/tables/datasource.go @@ -0,0 +1,212 @@ +package tables + +import ( + "context" + "fmt" + "net/http" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features" + iaasalphaUtils "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate" +) + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ datasource.DataSource = &routingTablesDataSource{} +) + +type DataSourceModelTables struct { + Id types.String `tfsdk:"id"` // needed by TF + OrganizationId types.String `tfsdk:"organization_id"` + NetworkAreaId types.String `tfsdk:"network_area_id"` + Region types.String `tfsdk:"region"` + Items types.List `tfsdk:"items"` +} + +// NewRoutingTablesDataSource is a helper function to simplify the provider implementation. +func NewRoutingTablesDataSource() datasource.DataSource { + return &routingTablesDataSource{} +} + +// routingTableDataSource is the data source implementation. +type routingTablesDataSource struct { + client *iaasalpha.APIClient + providerData core.ProviderData +} + +// Metadata returns the data source type name. +func (d *routingTablesDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_routing_tables" +} + +func (d *routingTablesDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + var ok bool + d.providerData, ok = conversion.ParseProviderData(ctx, req.ProviderData, &resp.Diagnostics) + if !ok { + return + } + + features.CheckExperimentEnabled(ctx, &d.providerData, features.RoutingTablesExperiment, "stackit_routing_tables", core.Datasource, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + + apiClient := iaasalphaUtils.ConfigureClient(ctx, &d.providerData, &resp.Diagnostics) + if resp.Diagnostics.HasError() { + return + } + d.client = apiClient + tflog.Info(ctx, "IaaS client configured") +} + +// Schema defines the schema for the data source. +func (d *routingTablesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + description := "Routing table datasource schema. Must have a `region` specified in the provider configuration." + resp.Schema = schema.Schema{ + Description: description, + MarkdownDescription: features.AddExperimentDescription(description, features.RoutingTablesExperiment, core.Datasource), + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Terraform's internal datasource ID. It is structured as \"`organization_id`,`region`,`network_area_id`\".", + Computed: true, + }, + "organization_id": schema.StringAttribute{ + Description: "STACKIT organization ID to which the routing table is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "network_area_id": schema.StringAttribute{ + Description: "The network area ID to which the routing table is associated.", + Required: true, + Validators: []validator.String{ + validate.UUID(), + validate.NoSeparator(), + }, + }, + "region": schema.StringAttribute{ + Description: "The resource region. If not defined, the provider region is used.", + // the region cannot be found, so it has to be passed + Optional: true, + }, + "items": schema.ListNestedAttribute{ + Description: "List of routing tables.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: shared.RoutingTableResponseAttributes(), + }, + }, + }, + } +} + +// Read refreshes the Terraform state with the latest data. +func (d *routingTablesDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform + var model DataSourceModelTables + diags := req.Config.Get(ctx, &model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + + organizationId := model.OrganizationId.ValueString() + region := d.providerData.GetRegionWithOverride(model.Region) + networkAreaId := model.NetworkAreaId.ValueString() + ctx = tflog.SetField(ctx, "organization_id", organizationId) + ctx = tflog.SetField(ctx, "region", region) + ctx = tflog.SetField(ctx, "network_area_id", networkAreaId) + + routingTablesResp, err := d.client.ListRoutingTablesOfArea(ctx, organizationId, networkAreaId, region).Execute() + if err != nil { + utils.LogError( + ctx, + &resp.Diagnostics, + err, + "Reading routing tables", + fmt.Sprintf("Routing tables with network area with ID %q does not exist in organization %q.", networkAreaId, organizationId), + map[int]string{ + http.StatusForbidden: fmt.Sprintf("Organization with ID %q not found or forbidden access", organizationId), + }, + ) + resp.State.RemoveResource(ctx) + return + } + + err = mapDataSourceRoutingTables(ctx, routingTablesResp, &model, region) + if err != nil { + core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading routing table", fmt.Sprintf("Processing API payload: %v", err)) + return + } + diags = resp.State.Set(ctx, model) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "Routing table read") +} + +func mapDataSourceRoutingTables(ctx context.Context, routingTables *iaasalpha.RoutingTableListResponse, model *DataSourceModelTables, region string) error { + if routingTables == nil { + return fmt.Errorf("response input is nil") + } + if model == nil { + return fmt.Errorf("model input is nil") + } + if routingTables.Items == nil { + return fmt.Errorf("items input is nil") + } + + organizationId := model.OrganizationId.ValueString() + networkAreaId := model.NetworkAreaId.ValueString() + + model.Id = utils.BuildInternalTerraformId(organizationId, region, networkAreaId) + + itemsList := []attr.Value{} + for i, routingTable := range *routingTables.Items { + var routingTableModel shared.RoutingTableReadModel + err := shared.MapRoutingTableReadModel(ctx, &routingTable, &routingTableModel) + if err != nil { + return fmt.Errorf("mapping routes: %w", err) + } + + routingTableMap := map[string]attr.Value{ + "routing_table_id": routingTableModel.RoutingTableId, + "name": routingTableModel.Name, + "description": routingTableModel.Description, + "labels": routingTableModel.Labels, + "created_at": routingTableModel.CreatedAt, + "updated_at": routingTableModel.UpdatedAt, + "default": routingTableModel.Default, + "system_routes": routingTableModel.SystemRoutes, + } + + routingTableTF, diags := types.ObjectValue(shared.RoutingTableReadModelTypes(), routingTableMap) + if diags.HasError() { + return fmt.Errorf("mapping index %d: %w", i, core.DiagsToError(diags)) + } + itemsList = append(itemsList, routingTableTF) + } + + itemsListTF, diags := types.ListValue(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, itemsList) + if diags.HasError() { + return core.DiagsToError(diags) + } + model.Items = itemsListTF + model.Region = types.StringValue(region) + + return nil +} diff --git a/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go b/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go new file mode 100644 index 00000000..2df93e79 --- /dev/null +++ b/stackit/internal/services/iaasalpha/routingtable/tables/datasource_test.go @@ -0,0 +1,175 @@ +package tables + +import ( + "context" + "fmt" + "testing" + "time" + + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/shared" + + "github.com/google/go-cmp/cmp" + "github.com/google/uuid" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stackitcloud/stackit-sdk-go/core/utils" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" +) + +const ( + testRegion = "eu01" +) + +var ( + organizationId = uuid.New() + networkAreaId = uuid.New() + routingTableId = uuid.New() + secondRoutingTableId = uuid.New() +) + +func TestMapDataFields(t *testing.T) { + terraformId := fmt.Sprintf("%s,%s,%s", organizationId.String(), testRegion, networkAreaId.String()) + createdAt := time.Now() + updatedAt := time.Now().Add(5 * time.Minute) + + tests := []struct { + description string + state DataSourceModelTables + input *iaasalpha.RoutingTableListResponse + expected DataSourceModelTables + isValid bool + }{ + { + "default_values", + DataSourceModelTables{ + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + }, + &iaasalpha.RoutingTableListResponse{ + Items: &[]iaasalpha.RoutingTable{ + { + Id: utils.Ptr(routingTableId.String()), + Name: utils.Ptr("test"), + Description: utils.Ptr("description"), + Default: utils.Ptr(true), + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + SystemRoutes: utils.Ptr(false), + }, + }, + }, + DataSourceModelTables{ + Id: types.StringValue(terraformId), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + Items: types.ListValueMust(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, []attr.Value{ + types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ + "routing_table_id": types.StringValue(routingTableId.String()), + "name": types.StringValue("test"), + "description": types.StringValue("description"), + "default": types.BoolValue(true), + "system_routes": types.BoolValue(false), + "created_at": types.StringValue(createdAt.Format(time.RFC3339)), + "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), + "labels": types.MapNull(types.StringType), + }), + }), + }, + true, + }, + { + "two routing tables", + DataSourceModelTables{ + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + }, + &iaasalpha.RoutingTableListResponse{ + Items: &[]iaasalpha.RoutingTable{ + { + Id: utils.Ptr(routingTableId.String()), + Name: utils.Ptr("test"), + Description: utils.Ptr("description"), + Default: utils.Ptr(true), + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + SystemRoutes: utils.Ptr(false), + }, + { + Id: utils.Ptr(secondRoutingTableId.String()), + Name: utils.Ptr("test2"), + Description: utils.Ptr("description2"), + Default: utils.Ptr(false), + CreatedAt: &createdAt, + UpdatedAt: &updatedAt, + SystemRoutes: utils.Ptr(false), + }, + }, + }, + DataSourceModelTables{ + Id: types.StringValue(terraformId), + OrganizationId: types.StringValue(organizationId.String()), + NetworkAreaId: types.StringValue(networkAreaId.String()), + Region: types.StringValue(testRegion), + Items: types.ListValueMust(types.ObjectType{AttrTypes: shared.RoutingTableReadModelTypes()}, []attr.Value{ + types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ + "routing_table_id": types.StringValue(routingTableId.String()), + "name": types.StringValue("test"), + "description": types.StringValue("description"), + "default": types.BoolValue(true), + "system_routes": types.BoolValue(false), + "created_at": types.StringValue(createdAt.Format(time.RFC3339)), + "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), + "labels": types.MapNull(types.StringType), + }), + types.ObjectValueMust(shared.RoutingTableReadModelTypes(), map[string]attr.Value{ + "routing_table_id": types.StringValue(secondRoutingTableId.String()), + "name": types.StringValue("test2"), + "description": types.StringValue("description2"), + "default": types.BoolValue(false), + "system_routes": types.BoolValue(false), + "created_at": types.StringValue(createdAt.Format(time.RFC3339)), + "updated_at": types.StringValue(updatedAt.Format(time.RFC3339)), + "labels": types.MapNull(types.StringType), + }), + }), + }, + true, + }, + { + "response_fields_items_nil_fail", + DataSourceModelTables{}, + &iaasalpha.RoutingTableListResponse{ + Items: nil, + }, + DataSourceModelTables{}, + false, + }, + { + "response_nil_fail", + DataSourceModelTables{}, + nil, + DataSourceModelTables{}, + false, + }, + } + for _, tt := range tests { + t.Run(tt.description, func(t *testing.T) { + err := mapDataSourceRoutingTables(context.Background(), tt.input, &tt.state, testRegion) + if !tt.isValid && err == nil { + t.Fatalf("Should have failed") + } + if tt.isValid && err != nil { + t.Fatalf("Should not have failed: %v", err) + } + if tt.isValid { + diff := cmp.Diff(tt.state, tt.expected) + if diff != "" { + t.Fatalf("Data does not match: %s", diff) + } + } + }) + } +} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf new file mode 100644 index 00000000..74c656d1 --- /dev/null +++ b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-max.tf @@ -0,0 +1,19 @@ +variable "organization_id" {} +variable "network_area_id" {} +variable "name" {} +variable "description" {} +variable "region" {} +variable "label" {} +variable "system_routes" {} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = var.network_area_id + name = var.name + description = var.description + region = var.region + labels = { + "acc-test" : var.label + } + system_routes = var.system_routes +} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf new file mode 100644 index 00000000..26921d7d --- /dev/null +++ b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-min.tf @@ -0,0 +1,9 @@ +variable "organization_id" {} +variable "network_area_id" {} +variable "name" {} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = var.network_area_id + name = var.name +} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf new file mode 100644 index 00000000..da2833c0 --- /dev/null +++ b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-max.tf @@ -0,0 +1,31 @@ +variable "organization_id" {} +variable "network_area_id" {} +variable "routing_table_name" {} +variable "destination_type" {} +variable "destination_value" {} +variable "next_hop_type" {} +variable "next_hop_value" {} +variable "label" {} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = var.network_area_id + name = var.routing_table_name +} + +resource "stackit_routing_table_route" "route" { + organization_id = var.organization_id + network_area_id = var.network_area_id + routing_table_id = stackit_routing_table.routing_table.routing_table_id + destination = { + type = var.destination_type + value = var.destination_value + } + next_hop = { + type = var.next_hop_type + value = var.next_hop_value + } + labels = { + "acc-test" = var.label + } +} diff --git a/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf new file mode 100644 index 00000000..65336be8 --- /dev/null +++ b/stackit/internal/services/iaasalpha/testdata/resource-routingtable-route-min.tf @@ -0,0 +1,27 @@ +variable "organization_id" {} +variable "network_area_id" {} +variable "routing_table_name" {} +variable "destination_type" {} +variable "destination_value" {} +variable "next_hop_type" {} +variable "next_hop_value" {} + +resource "stackit_routing_table" "routing_table" { + organization_id = var.organization_id + network_area_id = var.network_area_id + name = var.routing_table_name +} + +resource "stackit_routing_table_route" "route" { + organization_id = var.organization_id + network_area_id = var.network_area_id + routing_table_id = stackit_routing_table.routing_table.routing_table_id + destination = { + type = var.destination_type + value = var.destination_value + } + next_hop = { + type = var.next_hop_type + value = var.next_hop_value + } +} diff --git a/stackit/internal/services/iaasalpha/utils/util.go b/stackit/internal/services/iaasalpha/utils/util.go new file mode 100644 index 00000000..40216b92 --- /dev/null +++ b/stackit/internal/services/iaasalpha/utils/util.go @@ -0,0 +1,29 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-framework/diag" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +func ConfigureClient(ctx context.Context, providerData *core.ProviderData, diags *diag.Diagnostics) *iaasalpha.APIClient { + apiClientConfigOptions := []config.ConfigurationOption{ + config.WithCustomAuth(providerData.RoundTripper), + utils.UserAgentConfigOption(providerData.Version), + } + if providerData.IaaSCustomEndpoint != "" { + apiClientConfigOptions = append(apiClientConfigOptions, config.WithEndpoint(providerData.IaaSCustomEndpoint)) + } + apiClient, err := iaasalpha.NewAPIClient(apiClientConfigOptions...) + if err != nil { + core.LogAndAddError(ctx, diags, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the resource configuration", err)) + return nil + } + + return apiClient +} diff --git a/stackit/internal/services/iaasalpha/utils/util_test.go b/stackit/internal/services/iaasalpha/utils/util_test.go new file mode 100644 index 00000000..d4ba4671 --- /dev/null +++ b/stackit/internal/services/iaasalpha/utils/util_test.go @@ -0,0 +1,93 @@ +package utils + +import ( + "context" + "os" + "reflect" + "testing" + + "github.com/hashicorp/terraform-plugin-framework/diag" + sdkClients "github.com/stackitcloud/stackit-sdk-go/core/clients" + "github.com/stackitcloud/stackit-sdk-go/core/config" + "github.com/stackitcloud/stackit-sdk-go/services/iaasalpha" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core" + "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils" +) + +const ( + testVersion = "1.2.3" + testCustomEndpoint = "https://iaas-custom-endpoint.api.stackit.cloud" +) + +func TestConfigureClient(t *testing.T) { + /* mock authentication by setting service account token env variable */ + os.Clearenv() + err := os.Setenv(sdkClients.ServiceAccountToken, "mock-val") + if err != nil { + t.Errorf("error setting env variable: %v", err) + } + + type args struct { + providerData *core.ProviderData + } + tests := []struct { + name string + args args + wantErr bool + expected *iaasalpha.APIClient + }{ + { + name: "default endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + }, + }, + expected: func() *iaasalpha.APIClient { + apiClient, err := iaasalpha.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + { + name: "custom endpoint", + args: args{ + providerData: &core.ProviderData{ + Version: testVersion, + IaaSCustomEndpoint: testCustomEndpoint, + }, + }, + expected: func() *iaasalpha.APIClient { + apiClient, err := iaasalpha.NewAPIClient( + utils.UserAgentConfigOption(testVersion), + config.WithEndpoint(testCustomEndpoint), + ) + if err != nil { + t.Errorf("error configuring client: %v", err) + } + return apiClient + }(), + wantErr: false, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ctx := context.Background() + diags := diag.Diagnostics{} + + actual := ConfigureClient(ctx, tt.args.providerData, &diags) + if diags.HasError() != tt.wantErr { + t.Errorf("ConfigureClient() error = %v, want %v", diags.HasError(), tt.wantErr) + } + + if !reflect.DeepEqual(actual, tt.expected) { + t.Errorf("ConfigureClient() = %v, want %v", actual, tt.expected) + } + }) + } +} diff --git a/stackit/internal/services/serverbackup/schedule/resource.go b/stackit/internal/services/serverbackup/schedule/resource.go index 82168ef8..cc0ee969 100644 --- a/stackit/internal/services/serverbackup/schedule/resource.go +++ b/stackit/internal/services/serverbackup/schedule/resource.go @@ -129,7 +129,7 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server backup schedule resource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedule resource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server backup schedule resource schema. Must have a `region` specified in the provider configuration.", core.Resource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`backup_schedule_id`\".", diff --git a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go b/stackit/internal/services/serverbackup/schedule/schedule_datasource.go index d05fc807..2d3f82cf 100644 --- a/stackit/internal/services/serverbackup/schedule/schedule_datasource.go +++ b/stackit/internal/services/serverbackup/schedule/schedule_datasource.go @@ -77,7 +77,7 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server backup schedule datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedule datasource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server backup schedule datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`server_id`,`backup_schedule_id`\".", diff --git a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go b/stackit/internal/services/serverbackup/schedule/schedules_datasource.go index 5935ef8b..81cd5ade 100644 --- a/stackit/internal/services/serverbackup/schedule/schedules_datasource.go +++ b/stackit/internal/services/serverbackup/schedule/schedules_datasource.go @@ -76,7 +76,7 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server backup schedules datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server backup schedules datasource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server backup schedules datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`server_id`\".", diff --git a/stackit/internal/services/serverupdate/schedule/resource.go b/stackit/internal/services/serverupdate/schedule/resource.go index c81dfe4a..2ef4e705 100644 --- a/stackit/internal/services/serverupdate/schedule/resource.go +++ b/stackit/internal/services/serverupdate/schedule/resource.go @@ -122,7 +122,7 @@ func (r *scheduleResource) Configure(ctx context.Context, req resource.Configure func (r *scheduleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server update schedule resource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedule resource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server update schedule resource schema. Must have a `region` specified in the provider configuration.", core.Resource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", diff --git a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go index ef42b376..b0b17e65 100644 --- a/stackit/internal/services/serverupdate/schedule/schedule_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedule_datasource.go @@ -76,7 +76,7 @@ func (r *scheduleDataSource) Configure(ctx context.Context, req datasource.Confi func (r *scheduleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server update schedule datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedule datasource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server update schedule datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal resource identifier. It is structured as \"`project_id`,`region`,`server_id`,`update_schedule_id`\".", diff --git a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go index 85728cf9..4ea9a469 100644 --- a/stackit/internal/services/serverupdate/schedule/schedules_datasource.go +++ b/stackit/internal/services/serverupdate/schedule/schedules_datasource.go @@ -75,7 +75,7 @@ func (r *schedulesDataSource) Configure(ctx context.Context, req datasource.Conf func (r *schedulesDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { resp.Schema = schema.Schema{ Description: "Server update schedules datasource schema. Must have a `region` specified in the provider configuration.", - MarkdownDescription: features.AddBetaDescription("Server update schedules datasource schema. Must have a `region` specified in the provider configuration."), + MarkdownDescription: features.AddBetaDescription("Server update schedules datasource schema. Must have a `region` specified in the provider configuration.", core.Datasource), Attributes: map[string]schema.Attribute{ "id": schema.StringAttribute{ Description: "Terraform's internal data source identifier. It is structured as \"`project_id`,`region`,`server_id`\".", diff --git a/stackit/internal/testutil/testutil.go b/stackit/internal/testutil/testutil.go index 59ee77ac..8f48353b 100644 --- a/stackit/internal/testutil/testutil.go +++ b/stackit/internal/testutil/testutil.go @@ -122,11 +122,13 @@ func IaaSProviderConfig() string { return ` provider "stackit" { default_region = "eu01" + experiments = ["routing-tables"] }` } return fmt.Sprintf(` provider "stackit" { iaas_custom_endpoint = "%s" + experiments = ["routing-tables"] }`, IaaSCustomEndpoint, ) diff --git a/stackit/internal/utils/utils.go b/stackit/internal/utils/utils.go index 7974c44a..3368b341 100644 --- a/stackit/internal/utils/utils.go +++ b/stackit/internal/utils/utils.go @@ -146,7 +146,7 @@ func LogError(ctx context.Context, inputDiags *diag.Diagnostics, err error, summ } // FormatPossibleValues formats a slice into a comma-separated-list for usage in the provider docs -func FormatPossibleValues(values []string) string { +func FormatPossibleValues(values ...string) string { var formattedValues []string for _, value := range values { formattedValues = append(formattedValues, fmt.Sprintf("`%v`", value)) diff --git a/stackit/internal/utils/utils_test.go b/stackit/internal/utils/utils_test.go index 1edd232b..0a2af5c1 100644 --- a/stackit/internal/utils/utils_test.go +++ b/stackit/internal/utils/utils_test.go @@ -299,7 +299,7 @@ func TestFormatPossibleValues(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - if got := FormatPossibleValues(tt.args.values); got != tt.want { + if got := FormatPossibleValues(tt.args.values...); got != tt.want { t.Errorf("FormatPossibleValues() = %v, want %v", got, tt.want) } }) diff --git a/stackit/provider.go b/stackit/provider.go index a3392bdf..4ef40c3c 100644 --- a/stackit/provider.go +++ b/stackit/provider.go @@ -40,6 +40,10 @@ import ( iaasServiceAccountAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/serviceaccountattach" iaasVolume "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volume" iaasVolumeAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/volumeattach" + iaasalphaRoutingTableRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/route" + iaasalphaRoutingTableRoutes "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/routes" + iaasalphaRoutingTable "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/table" + iaasalphaRoutingTables "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaasalpha/routingtable/tables" loadBalancer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/loadbalancer" loadBalancerObservabilityCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/observability-credential" logMeCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/logme/credential" @@ -457,6 +461,10 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource iaasKeyPair.NewKeyPairDataSource, iaasServer.NewServerDataSource, iaasSecurityGroup.NewSecurityGroupDataSource, + iaasalphaRoutingTable.NewRoutingTableDataSource, + iaasalphaRoutingTableRoute.NewRoutingTableRouteDataSource, + iaasalphaRoutingTables.NewRoutingTablesDataSource, + iaasalphaRoutingTableRoutes.NewRoutingTableRoutesDataSource, iaasSecurityGroupRule.NewSecurityGroupRuleDataSource, loadBalancer.NewLoadBalancerDataSource, logMeInstance.NewInstanceDataSource, @@ -519,6 +527,8 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource { iaasServer.NewServerResource, iaasSecurityGroup.NewSecurityGroupResource, iaasSecurityGroupRule.NewSecurityGroupRuleResource, + iaasalphaRoutingTable.NewRoutingTableResource, + iaasalphaRoutingTableRoute.NewRoutingTableRouteResource, loadBalancer.NewLoadBalancerResource, loadBalancerObservabilityCredential.NewObservabilityCredentialResource, logMeInstance.NewInstanceResource,