IaaS Release (#543)
* IaaS Volume (#541) * Onboard IaaS Volume * Labels mapping * Add acceptance test * Remove source field * Fix lint * Add examples and docs * Fix lint * Fix lint * Fix lint * Volume source field (#542) * Onboard IaaS Volume * Labels mapping * Add acceptance test * Remove source field * Fix lint * Add examples and docs * Fix lint * Fix lint * Fix lint * Add source field supoort * Fix labels and source mapping * Remove unecessary source mapping * Move methods to conversion pkg * Revert change * Update stackit/internal/services/iaas/volume/datasource.go Co-authored-by: João Palet <joao.palet@outlook.com> * Update stackit/internal/services/iaas/volume/resource.go Co-authored-by: João Palet <joao.palet@outlook.com> * Update stackit/internal/services/iaas/volume/resource.go Co-authored-by: João Palet <joao.palet@outlook.com> * Update stackit/internal/services/iaas/volume/resource.go Co-authored-by: João Palet <joao.palet@outlook.com> * Changes after review * Change after revie --------- Co-authored-by: João Palet <joao.palet@outlook.com> * Onboard IaaS security groups (#545) * onboard iaas security group * add examples and generate docs * fix linter issues * fix deletion * Update stackit/internal/services/iaas/securitygroup/resource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * rename data source example file * update docs * remove field * remove field * remove plan modifier from the name field * refactor labels in mapFields * change function from utils to conversion * remove rules from the security group * update docs * add security group acceptance test * add plan modifiers to stateful field * sort imports * change stateful description --------- Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud> Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * IaaS Server baseline configuration (#546) * Server resource schema * Implemente CRUD methods and unit testsg * Bug fixes * Bug fix * Make variable private * Remove delete_on_termination and update descriptions * Add security_group field to initial networking * Add examples and acc test * Generate docs * Fix lint * Fix lint issue * Fix unit test * Update desc * Gen docs * Onboard IaaS network interface (#544) * implement network interface * handle labels * add CIDR validation * fix linter issues and generate docs * remove computed from the allowed addresses and fix the conditions * Update stackit/internal/services/iaas/networkinterface/resource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Update stackit/internal/services/iaas/networkinterface/datasource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * apply code review changes * remove status from schema * remove unnecessary GET call * Update stackit/internal/services/iaas/networkinterface/resource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Update stackit/internal/services/iaas/networkinterface/resource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * rename nic_security to security * add beta markdown description * use existing validateIP function * use utils function for the options listing * refactor labels * change function from utils to conversion * make allowed addresses a list of strings * add acceptance test for network interfaces * fix acceptance test * rename security_groups as security_group_ids * extend descriptions * fix acc test --------- Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud> Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * rename volume data source example (#552) Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud> * add requires replace to ipv4 and ipv6 fields (#549) Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud> Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Server resource improvements (#548) * Improvements to server resource * Fix example * Remove useStateForUnknown * Update SDK modules * Update iaasalpha moduel (#555) * Remove initial networking field (#556) * Server attachment resources (#557) * Server attachemnt resources * Add examples * Update volume datasource example * Fix linting issues * Fix linting * Fix examples formatting * Update go.mod * Revert iaas to v0.11 * Onboard iaas public ip (#551) * onboard public ip * onboard public ip * add public ip acceptance test * Update examples/data-sources/stackit_public_ip/data-source.tf Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * add plan modifier to IP * change type in the volume data source * add network_interface field to public ip resource * rename network_interface to network_interface_id * remove obsolete checks * extend unit tests * add network_interface_id in example * extend unit test * extend acceptance test * sort imports --------- Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Add labels to network, network are and network area route resources (#559) * Fix network_interface example * Extend network, network area and network area route with labels * Revert iaas to v0.11.0 --------- Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com> * Onboard iaas security group rule (#553) * onboard security group rule * add security group rule to acceptance test * change type in examples * fix acc test issues * extend example with objects * remove obsolete field from acceptance test * remove unnecessary plan modifier * adapt schema fields * adapt schema fields * add requires replace to all fields * extend descriptions with protocol limitations * rename subfield protocol to number * add requires replace to objects * make icmp_parameters fields required * add empty field checks for nested objects * make max and min fields required in the port_range object * make number field computed in the protocol object * add UseStateForUnknown in protocol number * remove obsolete unit test * add checks for empty protocol and adapt unit test * add atLeastOneOf validation in protocol fields * fix linter issues * Add project existence check before deleting SNA (#561) * add project list check and error in network area deletion * Update stackit/internal/services/iaas/networkarea/resource.go Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> --------- Co-authored-by: Vicente Pinto <vicente.pinto@freiheit.com> * Example server use cases and other fixes (#560) * Add example usage to server resource * Update examples * Fix beta warning * Update docs and examples * Remove size from example * Fix server description, fix security group rule error message * Other fixes * remove field from datasource --------- Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com> * Security group rule fixes (#562) * Add example usage to server resource * Update examples * Fix beta warning * Update docs and examples * Remove size from example * Fix server description, fix security group rule error message * Other fixes * Fixes to sec group rule * Fix lint * Change after review --------- Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com> * Fix server example (#565) * Fix server example * Fixes to examples, add CIDR validation to nic * Migrate iaasalpha to iaas (#568) * Migrate iaasalpha to iaas * Fix lint * Update example * Improvements to security group rule (#569) * Improvements to security group rule * Fix lint * Fix example and remove computed from description * Fix formatting * Update description --------- Co-authored-by: João Palet <joao.palet@outlook.com> Co-authored-by: GokceGK <161626272+GokceGK@users.noreply.github.com> Co-authored-by: Gökçe Gök Klingel <goekce.goek_klingel@stackit.cloud>
This commit is contained in:
parent
89dbf777fc
commit
93fe2fe89f
80 changed files with 10148 additions and 161 deletions
|
|
@ -31,6 +31,7 @@ data "stackit_network" "example" {
|
|||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`".
|
||||
- `ipv4_prefix_length` (Number) The IPv4 prefix length of the network.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `name` (String) The name of the network.
|
||||
- `nameservers` (List of String) The nameservers of the network.
|
||||
- `prefixes` (List of String) The prefixes of the network.
|
||||
|
|
|
|||
|
|
@ -35,6 +35,7 @@ data "stackit_network_area" "example" {
|
|||
- `default_nameservers` (List of String) List of DNS Servers/Nameservers.
|
||||
- `default_prefix_length` (Number) The default prefix length for networks in the network area.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`".
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `max_prefix_length` (Number) The maximal prefix length for networks in the network area.
|
||||
- `min_prefix_length` (Number) The minimal prefix length for networks in the network area.
|
||||
- `name` (String) The name of the network area.
|
||||
|
|
|
|||
|
|
@ -35,5 +35,6 @@ data "stackit_network_area_route" "example" {
|
|||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal data source ID. It is structured as "`organization_id`,`network_area_id`,`network_area_route_id`".
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `next_hop` (String) The IP address of the routing system, that will route the prefix configured. Should be a valid IPv4 address.
|
||||
- `prefix` (String) The network, that is reachable though the Next Hop. Should use CIDR notation.
|
||||
|
|
|
|||
46
docs/data-sources/network_interface.md
Normal file
46
docs/data-sources/network_interface.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_network_interface Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Network interface 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.
|
||||
---
|
||||
|
||||
# stackit_network_interface (Data Source)
|
||||
|
||||
Network interface 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "stackit_network_interface" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `network_id` (String) The network ID to which the network interface is associated.
|
||||
- `network_interface_id` (String) The network interface ID.
|
||||
- `project_id` (String) STACKIT project ID to which the network interface is associated.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `allowed_addresses` (List of String) The list of CIDR (Classless Inter-Domain Routing) notations.
|
||||
- `device` (String) The device UUID of the network interface.
|
||||
- `id` (String) Terraform's internal data source ID. It is structured as "`project_id`,`network_id`,`network_interface_id`".
|
||||
- `ipv4` (String) The IPv4 address.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a network interface.
|
||||
- `mac` (String) The MAC address of network interface.
|
||||
- `name` (String) The name of the network interface.
|
||||
- `security` (Boolean) The Network Interface Security. If set to false, then no security groups will apply to this network interface.
|
||||
- `security_group_ids` (List of String) The list of security group UUIDs. If security is set to false, setting this field will lead to an error.
|
||||
- `type` (String) Type of network interface. Some of the possible values are: Supported values are: `server`, `metadata`, `gateway`.
|
||||
38
docs/data-sources/public_ip.md
Normal file
38
docs/data-sources/public_ip.md
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_public_ip Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Volume resource 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.
|
||||
---
|
||||
|
||||
# stackit_public_ip (Data Source)
|
||||
|
||||
Volume resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "stackit_public_ip" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
public_ip_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the public IP is associated.
|
||||
- `public_ip_id` (String) The public IP ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`public_ip_id`".
|
||||
- `ip` (String) The IP address.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `network_interface_id` (String) Associates the public IP with a network interface or a virtual IP (ID).
|
||||
39
docs/data-sources/security_group.md
Normal file
39
docs/data-sources/security_group.md
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_security_group Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Security group 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.
|
||||
---
|
||||
|
||||
# stackit_security_group (Data Source)
|
||||
|
||||
Security group 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "stackit_security_group" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the security group is associated.
|
||||
- `security_group_id` (String) The security group ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `description` (String) The description of the security group.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_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 security group.
|
||||
- `stateful` (Boolean) Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.
|
||||
71
docs/data-sources/security_group_rule.md
Normal file
71
docs/data-sources/security_group_rule.md
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_security_group_rule Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Security group 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.
|
||||
---
|
||||
|
||||
# stackit_security_group_rule (Data Source)
|
||||
|
||||
Security group 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "stackit_security_group_rule" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_rule_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the security group rule is associated.
|
||||
- `security_group_id` (String) The security group ID.
|
||||
- `security_group_rule_id` (String) The security group rule ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `description` (String) The description of the security group rule.
|
||||
- `direction` (String) The direction of the traffic which the rule should match. Some of the possible values are: Supported values are: `ingress`, `egress`.
|
||||
- `ether_type` (String) The ethertype which the rule should match.
|
||||
- `icmp_parameters` (Attributes) ICMP Parameters. (see [below for nested schema](#nestedatt--icmp_parameters))
|
||||
- `id` (String) Terraform's internal datasource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`".
|
||||
- `ip_range` (String) The remote IP range which the rule should match.
|
||||
- `port_range` (Attributes) The range of ports. (see [below for nested schema](#nestedatt--port_range))
|
||||
- `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol))
|
||||
- `remote_security_group_id` (String) The remote security group which the rule should match.
|
||||
|
||||
<a id="nestedatt--icmp_parameters"></a>
|
||||
### Nested Schema for `icmp_parameters`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `code` (Number) ICMP code. Can be set if the protocol is ICMP.
|
||||
- `type` (Number) ICMP type. Can be set if the protocol is ICMP.
|
||||
|
||||
|
||||
<a id="nestedatt--port_range"></a>
|
||||
### Nested Schema for `port_range`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `max` (Number) The maximum port number. Should be greater or equal to the minimum.
|
||||
- `min` (Number) The minimum port number. Should be less or equal to the minimum.
|
||||
|
||||
|
||||
<a id="nestedatt--protocol"></a>
|
||||
### Nested Schema for `protocol`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `name` (String) The protocol name which the rule should match.
|
||||
- `number` (Number) The protocol number which the rule should match.
|
||||
50
docs/data-sources/server.md
Normal file
50
docs/data-sources/server.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_server Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Server 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.
|
||||
---
|
||||
|
||||
# stackit_server (Data Source)
|
||||
|
||||
Server 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.
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the server is associated.
|
||||
- `server_id` (String) The server ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `affinity_group` (String) The affinity group the server is assigned to.
|
||||
- `availability_zone` (String) The availability zone of the server.
|
||||
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
|
||||
- `created_at` (String) Date-time when the server was created
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`".
|
||||
- `image_id` (String) The image ID to be used for an ephemeral disk on the server.
|
||||
- `keypair_name` (String) The name of the keypair used during server creation.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `launched_at` (String) Date-time when the server was launched
|
||||
- `machine_type` (String) Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html)
|
||||
- `name` (String) The name of the server.
|
||||
- `updated_at` (String) Date-time when the server was updated
|
||||
- `user_data` (String) User data that is passed via cloud-init to the server.
|
||||
|
||||
<a id="nestedatt--boot_volume"></a>
|
||||
### Nested Schema for `boot_volume`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `id` (String) The ID of the source, either image ID or volume ID
|
||||
- `performance_class` (String) The performance class of the server.
|
||||
- `size` (Number) The size of the boot volume in GB.
|
||||
- `type` (String) The type of the source. Supported values are: `volume`, `image`.
|
||||
51
docs/data-sources/volume.md
Normal file
51
docs/data-sources/volume.md
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_volume Data Source - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Volume resource 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.
|
||||
---
|
||||
|
||||
# stackit_volume (Data Source)
|
||||
|
||||
Volume resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
data "stackit_volume" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
volume_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the volume is associated.
|
||||
- `volume_id` (String) The volume ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `availability_zone` (String) The availability zone of the volume.
|
||||
- `description` (String) The description of the volume.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`volume_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 volume.
|
||||
- `performance_class` (String) The performance class of the volume.
|
||||
- `server_id` (String) The server ID of the server to which the volume is attached to.
|
||||
- `size` (Number) The size of the volume in GB. It can only be updated to a larger value than the current size
|
||||
- `source` (Attributes) The source of the volume. It can be either a volume, an image, a snapshot or a backup (see [below for nested schema](#nestedatt--source))
|
||||
|
||||
<a id="nestedatt--source"></a>
|
||||
### Nested Schema for `source`
|
||||
|
||||
Read-Only:
|
||||
|
||||
- `id` (String) The ID of the source, e.g. image ID
|
||||
- `type` (String) The type of the source. Supported values are: `volume`, `image`, `snapshot`, `backup`.
|
||||
|
|
@ -8,23 +8,25 @@ description: |-
|
|||
To automate the creation of load balancers, OpenStack can be used to setup the supporting infrastructure.
|
||||
To set up the OpenStack provider, you can create a token through the STACKIT Portal, in your project's Infrastructure API page.
|
||||
There, the OpenStack user domain name, username, and password are generated and can be obtained. The provider can then be configured as follows:
|
||||
```terraform
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
(...)
|
||||
openstack = {
|
||||
source = "terraform-provider-openstack/openstack"
|
||||
}
|
||||
}
|
||||
required_providers {
|
||||
(...)
|
||||
openstack = {
|
||||
source = "terraform-provider-openstack/openstack"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "openstack" {
|
||||
user_domain_name = "{OpenStack user domain name}"
|
||||
user_name = "{OpenStack username}"
|
||||
password = "{OpenStack password}"
|
||||
region = "RegionOne"
|
||||
auth_url = "https://keystone.api.iaas.eu01.stackit.cloud/v3"
|
||||
user_domain_name = "{OpenStack user domain name}"
|
||||
user_name = "{OpenStack username}"
|
||||
password = "{OpenStack password}"
|
||||
region = "RegionOne"
|
||||
auth_url = "https://keystone.api.iaas.eu01.stackit.cloud/v3"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Configuring the supporting infrastructure
|
||||
The example below uses OpenStack to create the network, router, a public IP address and a compute instance.
|
||||
---
|
||||
|
|
|
|||
|
|
@ -18,6 +18,9 @@ resource "stackit_network" "example" {
|
|||
name = "example-network"
|
||||
nameservers = ["1.2.3.4", "5.6.7.8"]
|
||||
ipv4_prefix_length = 24
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -32,6 +35,7 @@ resource "stackit_network" "example" {
|
|||
### Optional
|
||||
|
||||
- `ipv4_prefix_length` (Number) The IPv4 prefix length of the network.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `nameservers` (List of String) The nameservers of the network.
|
||||
|
||||
### Read-Only
|
||||
|
|
|
|||
|
|
@ -4,12 +4,15 @@ page_title: "stackit_network_area Resource - stackit"
|
|||
subcategory: ""
|
||||
description: |-
|
||||
Network area resource 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.
|
||||
---
|
||||
|
||||
# stackit_network_area (Resource)
|
||||
|
||||
Network area resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
|
|
@ -18,10 +21,13 @@ resource "stackit_network_area" "example" {
|
|||
name = "example-network-area"
|
||||
network_ranges = [
|
||||
{
|
||||
prefix = "1.2.3.4"
|
||||
prefix = "192.168.0.0/24"
|
||||
}
|
||||
]
|
||||
transfer_network = "1.2.3.4/5"
|
||||
transfer_network = "192.168.0.0/24"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -39,14 +45,13 @@ resource "stackit_network_area" "example" {
|
|||
|
||||
- `default_nameservers` (List of String) List of DNS Servers/Nameservers.
|
||||
- `default_prefix_length` (Number) The default prefix length for networks in the network area.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `max_prefix_length` (Number) The maximal prefix length for networks in the network area.
|
||||
- `min_prefix_length` (Number) The minimal prefix length for networks in the network area.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Network area resource 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.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`".
|
||||
- `network_area_id` (String) The network area ID.
|
||||
- `project_count` (Number) The amount of projects currently referencing this area.
|
||||
|
||||
|
|
|
|||
|
|
@ -4,20 +4,26 @@ page_title: "stackit_network_area_route Resource - stackit"
|
|||
subcategory: ""
|
||||
description: |-
|
||||
Network area route resource 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.
|
||||
---
|
||||
|
||||
# stackit_network_area_route (Resource)
|
||||
|
||||
Network area route resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_network_area" "example" {
|
||||
resource "stackit_network_area_route" "example" {
|
||||
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
prefix = "1.2.3.4/5"
|
||||
next_hop = "6.7.8.9"
|
||||
prefix = "192.168.0.0/24"
|
||||
next_hop = "192.168.0.0"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -31,9 +37,11 @@ resource "stackit_network_area" "example" {
|
|||
- `organization_id` (String) STACKIT organization ID to which the network area is associated.
|
||||
- `prefix` (String) The network, that is reachable though the Next Hop. Should use CIDR notation.
|
||||
|
||||
### Optional
|
||||
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Network area route resource 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.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`organization_id`,`network_area_id`,`network_area_route_id`".
|
||||
- `network_area_route_id` (String) The network area route ID.
|
||||
|
|
|
|||
50
docs/resources/network_interface.md
Normal file
50
docs/resources/network_interface.md
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_network_interface Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Network interface resource 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.
|
||||
---
|
||||
|
||||
# stackit_network_interface (Resource)
|
||||
|
||||
Network interface resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_network_interface" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
allowed_addresses = ["192.168.0.0/24"]
|
||||
security_group_ids = ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `network_id` (String) The network ID to which the network interface is associated.
|
||||
- `project_id` (String) STACKIT project ID to which the network is associated.
|
||||
|
||||
### Optional
|
||||
|
||||
- `allowed_addresses` (List of String) The list of CIDR (Classless Inter-Domain Routing) notations.
|
||||
- `ipv4` (String) The IPv4 address.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a network interface.
|
||||
- `name` (String) The name of the network interface.
|
||||
- `security` (Boolean) The Network Interface Security. If set to false, then no security groups will apply to this network interface.
|
||||
- `security_group_ids` (List of String) The list of security group UUIDs. If security is set to false, setting this field will lead to an error.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `device` (String) The device UUID of the network interface.
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`network_id`,`network_interface_id`".
|
||||
- `mac` (String) The MAC address of network interface.
|
||||
- `network_interface_id` (String) The network interface ID.
|
||||
- `type` (String) Type of network interface. Some of the possible values are: Supported values are: `server`, `metadata`, `gateway`.
|
||||
44
docs/resources/public_ip.md
Normal file
44
docs/resources/public_ip.md
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_public_ip Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Public IP resource 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.
|
||||
---
|
||||
|
||||
# stackit_public_ip (Resource)
|
||||
|
||||
Public IP resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_public_ip" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the public IP is associated.
|
||||
|
||||
### Optional
|
||||
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `network_interface_id` (String) Associates the public IP with a network interface or a virtual IP (ID).
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`public_ip_id`".
|
||||
- `ip` (String) The IP address.
|
||||
- `public_ip_id` (String) The public IP ID.
|
||||
45
docs/resources/security_group.md
Normal file
45
docs/resources/security_group.md
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_security_group Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Security group resource 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.
|
||||
---
|
||||
|
||||
# stackit_security_group (Resource)
|
||||
|
||||
Security group resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_security_group" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "my_security_group"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `name` (String) The name of the security group.
|
||||
- `project_id` (String) STACKIT project ID to which the security group is associated.
|
||||
|
||||
### Optional
|
||||
|
||||
- `description` (String) The description of the security group.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `stateful` (Boolean) Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`".
|
||||
- `security_group_id` (String) The security group ID.
|
||||
81
docs/resources/security_group_rule.md
Normal file
81
docs/resources/security_group_rule.md
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_security_group_rule Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Security group rule resource 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.
|
||||
---
|
||||
|
||||
# stackit_security_group_rule (Resource)
|
||||
|
||||
Security group rule resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_security_group_rule" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
direction = "ingress"
|
||||
icmp_parameters = {
|
||||
code = 0
|
||||
type = 8
|
||||
}
|
||||
protocol = {
|
||||
name = "icmp"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `direction` (String) The direction of the traffic which the rule should match. Some of the possible values are: Supported values are: `ingress`, `egress`.
|
||||
- `project_id` (String) STACKIT project ID to which the security group rule is associated.
|
||||
- `security_group_id` (String) The security group ID.
|
||||
|
||||
### Optional
|
||||
|
||||
- `description` (String) The rule description.
|
||||
- `ether_type` (String) The ethertype which the rule should match.
|
||||
- `icmp_parameters` (Attributes) ICMP Parameters. These parameters should only be provided if the protocol is ICMP. (see [below for nested schema](#nestedatt--icmp_parameters))
|
||||
- `ip_range` (String) The remote IP range which the rule should match.
|
||||
- `port_range` (Attributes) The range of ports. This should only be provided if the protocol is not ICMP. (see [below for nested schema](#nestedatt--port_range))
|
||||
- `protocol` (Attributes) The internet protocol which the rule should match. (see [below for nested schema](#nestedatt--protocol))
|
||||
- `remote_security_group_id` (String) The remote security group which the rule should match.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`security_group_id`,`security_group_rule_id`".
|
||||
- `security_group_rule_id` (String) The security group rule ID.
|
||||
|
||||
<a id="nestedatt--icmp_parameters"></a>
|
||||
### Nested Schema for `icmp_parameters`
|
||||
|
||||
Required:
|
||||
|
||||
- `code` (Number) ICMP code. Can be set if the protocol is ICMP.
|
||||
- `type` (Number) ICMP type. Can be set if the protocol is ICMP.
|
||||
|
||||
|
||||
<a id="nestedatt--port_range"></a>
|
||||
### Nested Schema for `port_range`
|
||||
|
||||
Required:
|
||||
|
||||
- `max` (Number) The maximum port number. Should be greater or equal to the minimum.
|
||||
- `min` (Number) The minimum port number. Should be less or equal to the maximum.
|
||||
|
||||
|
||||
<a id="nestedatt--protocol"></a>
|
||||
### Nested Schema for `protocol`
|
||||
|
||||
Optional:
|
||||
|
||||
- `name` (String) The protocol name which the rule should match. Either `name` or `number` must be provided.
|
||||
- `number` (Number) The protocol number which the rule should match. Either `name` or `number` must be provided.
|
||||
374
docs/resources/server.md
Normal file
374
docs/resources/server.md
Normal file
|
|
@ -0,0 +1,374 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_server Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Server resource 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.
|
||||
Example Usage
|
||||
Boot from volume
|
||||
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
|
||||
Boot from existing volume
|
||||
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
source_type = "volume"
|
||||
source_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
|
||||
Network setup
|
||||
|
||||
resource "stackit_server" "server-with-network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_network" "network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-network"
|
||||
nameservers = ["192.0.2.0", "198.51.100.0", "203.0.113.0"]
|
||||
ipv4_prefix_length = 24
|
||||
}
|
||||
|
||||
resource "stackit_security_group" "sec-group" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-security-group"
|
||||
stateful = true
|
||||
}
|
||||
|
||||
resource "stackit_security_group_rule" "rule" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = stackit_security_group.sec-group.security_group_id
|
||||
direction = "ingress"
|
||||
ether_type = "IPv4"
|
||||
}
|
||||
|
||||
resource "stackit_network_interface" "nic" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = stackit_network.network.network_id
|
||||
security_group_ids = [stackit_security_group.sec-group.security_group_id]
|
||||
}
|
||||
|
||||
resource "stackit_public_ip" "public-ip" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
|
||||
resource "stackit_server_network_interface_attach" "nic-attachment" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-network.server_id
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
|
||||
|
||||
Server with attached volume
|
||||
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "server-with-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_server_volume_attach" "attach_volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-volume.server_id
|
||||
volume_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
|
||||
|
||||
Server with user data (cloud-init)
|
||||
|
||||
resource "stackit_server" "user-data" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = "#!/bin/bash\n/bin/su"
|
||||
}
|
||||
|
||||
resource "stackit_server" "user-data-from-file" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = file("${path.module}/cloud-init.yaml")
|
||||
}
|
||||
---
|
||||
|
||||
# stackit_server (Resource)
|
||||
|
||||
Server resource 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.
|
||||
## Example Usage
|
||||
|
||||
|
||||
### Boot from volume
|
||||
```terraform
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Boot from existing volume
|
||||
```terraform
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
source_type = "volume"
|
||||
source_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Network setup
|
||||
```terraform
|
||||
resource "stackit_server" "server-with-network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_network" "network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-network"
|
||||
nameservers = ["192.0.2.0", "198.51.100.0", "203.0.113.0"]
|
||||
ipv4_prefix_length = 24
|
||||
}
|
||||
|
||||
resource "stackit_security_group" "sec-group" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-security-group"
|
||||
stateful = true
|
||||
}
|
||||
|
||||
resource "stackit_security_group_rule" "rule" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = stackit_security_group.sec-group.security_group_id
|
||||
direction = "ingress"
|
||||
ether_type = "IPv4"
|
||||
}
|
||||
|
||||
resource "stackit_network_interface" "nic" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = stackit_network.network.network_id
|
||||
security_group_ids = [stackit_security_group.sec-group.security_group_id]
|
||||
}
|
||||
|
||||
resource "stackit_public_ip" "public-ip" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
|
||||
resource "stackit_server_network_interface_attach" "nic-attachment" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-network.server_id
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Server with attached volume
|
||||
```terraform
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "server-with-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_server_volume_attach" "attach_volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-volume.server_id
|
||||
volume_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
### Server with user data (cloud-init)
|
||||
```terraform
|
||||
resource "stackit_server" "user-data" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = "#!/bin/bash\n/bin/su"
|
||||
}
|
||||
|
||||
resource "stackit_server" "user-data-from-file" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = file("${path.module}/cloud-init.yaml")
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `machine_type` (String) Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html)
|
||||
- `name` (String) The name of the server.
|
||||
- `project_id` (String) STACKIT project ID to which the server is associated.
|
||||
|
||||
### Optional
|
||||
|
||||
- `affinity_group` (String) The affinity group the server is assigned to.
|
||||
- `availability_zone` (String) The availability zone of the server.
|
||||
- `boot_volume` (Attributes) The boot volume for the server (see [below for nested schema](#nestedatt--boot_volume))
|
||||
- `image_id` (String) The image ID to be used for an ephemeral disk on the server.
|
||||
- `keypair_name` (String) The name of the keypair used during server creation.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `user_data` (String) User data that is passed via cloud-init to the server.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `created_at` (String) Date-time when the server was created
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`".
|
||||
- `launched_at` (String) Date-time when the server was launched
|
||||
- `server_id` (String) The server ID.
|
||||
- `updated_at` (String) Date-time when the server was updated
|
||||
|
||||
<a id="nestedatt--boot_volume"></a>
|
||||
### Nested Schema for `boot_volume`
|
||||
|
||||
Required:
|
||||
|
||||
- `source_id` (String) The ID of the source, either image ID or volume ID
|
||||
- `source_type` (String) The type of the source. Supported values are: `volume`, `image`.
|
||||
|
||||
Optional:
|
||||
|
||||
- `performance_class` (String) The performance class of the server.
|
||||
- `size` (Number) The size of the boot volume in GB. Must be provided when `source_type` is `image`.
|
||||
37
docs/resources/server_network_interface_attach.md
Normal file
37
docs/resources/server_network_interface_attach.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_server_network_interface_attach Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Network interface attachment resource schema. Attaches a network interface to a server. 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.
|
||||
---
|
||||
|
||||
# stackit_server_network_interface_attach (Resource)
|
||||
|
||||
Network interface attachment resource schema. Attaches a network interface to a server. 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_server_network_interface_attach" "attached_network_interface" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `network_interface_id` (String) The network interface ID.
|
||||
- `project_id` (String) STACKIT project ID to which the network interface attachment is associated.
|
||||
- `server_id` (String) The server ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`network_interface_id`".
|
||||
37
docs/resources/server_service_account_attach.md
Normal file
37
docs/resources/server_service_account_attach.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_server_service_account_attach Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Service account attachment resource schema. Attaches a service account to a server. 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.
|
||||
---
|
||||
|
||||
# stackit_server_service_account_attach (Resource)
|
||||
|
||||
Service account attachment resource schema. Attaches a service account to a server. 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_server_service_account_attach" "attached_service_account" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
service_account_email = "service-account@stackit.cloud"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the service account attachment is associated.
|
||||
- `server_id` (String) The server ID.
|
||||
- `service_account_email` (String) The service account email.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`service_account_email`".
|
||||
37
docs/resources/server_volume_attach.md
Normal file
37
docs/resources/server_volume_attach.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_server_volume_attach Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Volume attachment resource schema. Attaches a volume to a server. 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.
|
||||
---
|
||||
|
||||
# stackit_server_volume_attach (Resource)
|
||||
|
||||
Volume attachment resource schema. Attaches a volume to a server. 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_server_volume_attach" "attached_volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
volume_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `project_id` (String) STACKIT project ID to which the volume attachment is associated.
|
||||
- `server_id` (String) The server ID.
|
||||
- `volume_id` (String) The volume ID.
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`server_id`,`volume_id`".
|
||||
|
|
@ -3,12 +3,12 @@
|
|||
page_title: "stackit_sqlserverflex_user Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
[Warning: BETA] SQLServer Flex user resource schema. Must have a region specified in the provider configuration.
|
||||
SQLServer Flex user resource schema. Must have a region specified in the provider configuration.
|
||||
---
|
||||
|
||||
# stackit_sqlserverflex_user (Resource)
|
||||
|
||||
[Warning: BETA] SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration.
|
||||
SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration.
|
||||
|
||||
## Example Usage
|
||||
|
||||
|
|
|
|||
59
docs/resources/volume.md
Normal file
59
docs/resources/volume.md
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
---
|
||||
# generated by https://github.com/hashicorp/terraform-plugin-docs
|
||||
page_title: "stackit_volume Resource - stackit"
|
||||
subcategory: ""
|
||||
description: |-
|
||||
Volume resource 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.
|
||||
---
|
||||
|
||||
# stackit_volume (Resource)
|
||||
|
||||
Volume resource 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.
|
||||
|
||||
## Example Usage
|
||||
|
||||
```terraform
|
||||
resource "stackit_volume" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "my_volume"
|
||||
availability_zone = "eu01-1"
|
||||
size = 64
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
<!-- schema generated by tfplugindocs -->
|
||||
## Schema
|
||||
|
||||
### Required
|
||||
|
||||
- `availability_zone` (String) The availability zone of the volume.
|
||||
- `project_id` (String) STACKIT project ID to which the volume is associated.
|
||||
|
||||
### Optional
|
||||
|
||||
- `description` (String) The description of the volume.
|
||||
- `labels` (Map of String) Labels are key-value string pairs which can be attached to a resource container
|
||||
- `name` (String) The name of the volume.
|
||||
- `performance_class` (String) The performance class of the volume.
|
||||
- `server_id` (String) The server ID of the server to which the volume is attached to.
|
||||
- `size` (Number) The size of the volume in GB. It can only be updated to a larger value than the current size. Either `size` or `source` must be provided
|
||||
- `source` (Attributes) The source of the volume. It can be either a volume, an image, a snapshot or a backup. Either `size` or `source` must be provided (see [below for nested schema](#nestedatt--source))
|
||||
|
||||
### Read-Only
|
||||
|
||||
- `id` (String) Terraform's internal resource ID. It is structured as "`project_id`,`volume_id`".
|
||||
- `volume_id` (String) The volume ID.
|
||||
|
||||
<a id="nestedatt--source"></a>
|
||||
### Nested Schema for `source`
|
||||
|
||||
Required:
|
||||
|
||||
- `id` (String) The ID of the source, e.g. image ID
|
||||
- `type` (String) The type of the source. Supported values are: `volume`, `image`, `snapshot`, `backup`.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
data "stackit_network_interface" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
4
examples/data-sources/stackit_public_ip/data-source.tf
Normal file
4
examples/data-sources/stackit_public_ip/data-source.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
data "stackit_public_ip" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
public_ip_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
data "stackit_security_group" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
data "stackit_security_group_rule" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_rule_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
4
examples/data-sources/stackit_server/datasource.tf
Normal file
4
examples/data-sources/stackit_server/datasource.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
data "stackit_server" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
4
examples/data-sources/stackit_volume/data-source.tf
Normal file
4
examples/data-sources/stackit_volume/data-source.tf
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
data "stackit_volume" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
volume_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
|
|
@ -3,4 +3,7 @@ resource "stackit_network" "example" {
|
|||
name = "example-network"
|
||||
nameservers = ["1.2.3.4", "5.6.7.8"]
|
||||
ipv4_prefix_length = 24
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,11 @@ resource "stackit_network_area" "example" {
|
|||
name = "example-network-area"
|
||||
network_ranges = [
|
||||
{
|
||||
prefix = "1.2.3.4"
|
||||
prefix = "192.168.0.0/24"
|
||||
}
|
||||
]
|
||||
transfer_network = "1.2.3.4/5"
|
||||
transfer_network = "192.168.0.0/24"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,9 @@
|
|||
resource "stackit_network_area" "example" {
|
||||
resource "stackit_network_area_route" "example" {
|
||||
organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_area_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
prefix = "1.2.3.4/5"
|
||||
next_hop = "6.7.8.9"
|
||||
prefix = "192.168.0.0/24"
|
||||
next_hop = "192.168.0.0"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
6
examples/resources/stackit_network_interface/resource.tf
Normal file
6
examples/resources/stackit_network_interface/resource.tf
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
resource "stackit_network_interface" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
allowed_addresses = ["192.168.0.0/24"]
|
||||
security_group_ids = ["xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"]
|
||||
}
|
||||
7
examples/resources/stackit_public_ip/resource.tf
Normal file
7
examples/resources/stackit_public_ip/resource.tf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
resource "stackit_public_ip" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
7
examples/resources/stackit_security_group/resource.tf
Normal file
7
examples/resources/stackit_security_group/resource.tf
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
resource "stackit_security_group" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "my_security_group"
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
12
examples/resources/stackit_security_group_rule/resource.tf
Normal file
12
examples/resources/stackit_security_group_rule/resource.tf
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
resource "stackit_security_group_rule" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
direction = "ingress"
|
||||
icmp_parameters = {
|
||||
code = 0
|
||||
type = 8
|
||||
}
|
||||
protocol = {
|
||||
name = "icmp"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
resource "stackit_server_network_interface_attach" "attached_network_interface" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
resource "stackit_server_service_account_attach" "attached_service_account" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
service_account_email = "service-account@stackit.cloud"
|
||||
}
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
resource "stackit_server_volume_attach" "attached_volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
volume_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
9
examples/resources/stackit_volume/resource.tf
Normal file
9
examples/resources/stackit_volume/resource.tf
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
resource "stackit_volume" "example" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "my_volume"
|
||||
availability_zone = "eu01-1"
|
||||
size = 64
|
||||
labels = {
|
||||
"key" = "value"
|
||||
}
|
||||
}
|
||||
2
go.mod
2
go.mod
|
|
@ -14,7 +14,7 @@ require (
|
|||
github.com/stackitcloud/stackit-sdk-go/core v0.14.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/argus v0.11.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.12.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.14.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0
|
||||
github.com/stackitcloud/stackit-sdk-go/services/mariadb v0.20.0
|
||||
|
|
|
|||
4
go.sum
4
go.sum
|
|
@ -155,8 +155,8 @@ github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0 h1:WXSIE4Kf
|
|||
github.com/stackitcloud/stackit-sdk-go/services/authorization v0.4.0/go.mod h1:8spVqlPqZrvQQ63Qodbydk3qsZx7lr963ECft+sqFhY=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0 h1:+OZ82DwFy4JIJThadVjvll5kUWjHPSLbUIF65njsNBk=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/dns v0.11.0/go.mod h1:mv8U7kuclXo+0VpDHtBCkve/3i9h1yT+RAId/MUi+C8=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.12.0 h1:tk/ztsZoFgcsyEFODaej8IGGuvprVbsrrJKGzm2PR14=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.12.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.14.0 h1:2CLlZInB7ytcnIpPUhFSbCBrNp0mMhxIuGOxlz3rhoA=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/iaas v0.14.0/go.mod h1:YfuN+eXuqr846xeRyW2Vf1JM2jU0ikeQa76dDI66RsM=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0 h1:06CGP64CEk3Zg6i9kZCMRdmCzLLiyMWQqGK1teBr9Oc=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/loadbalancer v0.17.0/go.mod h1:JL94zc8K0ebWs+DBGXR28vNCF0EFV54ZLUtrlXOvWgA=
|
||||
github.com/stackitcloud/stackit-sdk-go/services/logme v0.20.0 h1:V0UGP7JEa4Q8SsZFUJsKgLGaoPruLn2KVKnqQtaoWCU=
|
||||
|
|
|
|||
|
|
@ -133,3 +133,37 @@ func StringListToPointer(list basetypes.ListValue) (*[]string, error) {
|
|||
|
||||
return &listStr, nil
|
||||
}
|
||||
|
||||
// ToJSONMApPartialUpdatePayload returns a map[string]interface{} to be used in a PATCH request payload.
|
||||
// It takes a current map as it is in the terraform state and a desired map as it is in the user configuratiom
|
||||
// and builds a map which sets to null keys that should be removed, updates the values of existing keys and adds new keys
|
||||
// This method is needed because in partial updates, e.g. if the key is not provided it is ignored and not removed
|
||||
func ToJSONMapPartialUpdatePayload(ctx context.Context, current, desired types.Map) (map[string]interface{}, error) {
|
||||
currentMap, err := ToStringInterfaceMap(ctx, current)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
desiredMap, err := ToStringInterfaceMap(ctx, desired)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
mapPayload := map[string]interface{}{}
|
||||
// Update and remove existing keys
|
||||
for k := range currentMap {
|
||||
if desiredValue, ok := desiredMap[k]; ok {
|
||||
mapPayload[k] = desiredValue
|
||||
} else {
|
||||
mapPayload[k] = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add new keys
|
||||
for k, desiredValue := range desiredMap {
|
||||
if _, ok := mapPayload[k]; !ok {
|
||||
mapPayload[k] = desiredValue
|
||||
}
|
||||
}
|
||||
return mapPayload, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ import (
|
|||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
|
|
@ -80,3 +81,139 @@ func TestFromTerraformStringMapToInterfaceMap(t *testing.T) {
|
|||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToJSONMapUpdatePayload(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
currentLabels types.Map
|
||||
desiredLabels types.Map
|
||||
expected map[string]interface{}
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"nothing_to_update",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"update_key_value",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("updated_value"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "updated_value",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"remove_key",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "value",
|
||||
"key2": nil,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"add_new_key",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "value",
|
||||
"key2": "value2",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_desired_map",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
map[string]interface{}{
|
||||
"key": nil,
|
||||
"key2": nil,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nil_desired_map",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
types.MapNull(types.StringType),
|
||||
map[string]interface{}{
|
||||
"key": nil,
|
||||
"key2": nil,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_current_map",
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "value",
|
||||
"key2": "value2",
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nil_current_map",
|
||||
types.MapNull(types.StringType),
|
||||
types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
"key2": types.StringValue("value2"),
|
||||
}),
|
||||
map[string]interface{}{
|
||||
"key": "value",
|
||||
"key2": "value2",
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := ToJSONMapPartialUpdatePayload(context.Background(), tt.currentLabels, tt.desiredLabels)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -124,6 +124,11 @@ func (d *networkDataSource) Schema(_ context.Context, _ datasource.SchemaRequest
|
|||
Description: "The public IP of the network.",
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"github.com/hashicorp/terraform-plugin-framework/path"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema"
|
||||
|
|
@ -42,6 +43,7 @@ type Model struct {
|
|||
IPv4PrefixLength types.Int64 `tfsdk:"ipv4_prefix_length"`
|
||||
Prefixes types.List `tfsdk:"prefixes"`
|
||||
PublicIP types.String `tfsdk:"public_ip"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
}
|
||||
|
||||
// NewNetworkResource is a helper function to simplify the provider implementation.
|
||||
|
|
@ -160,6 +162,11 @@ func (r *networkResource) Schema(_ context.Context, _ resource.SchemaRequest, re
|
|||
Description: "The public IP of the network.",
|
||||
Computed: true,
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -178,7 +185,7 @@ func (r *networkResource) Create(ctx context.Context, req resource.CreateRequest
|
|||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(&model)
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
|
|
@ -269,8 +276,16 @@ func (r *networkResource) Update(ctx context.Context, req resource.UpdateRequest
|
|||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
|
||||
// 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(&model)
|
||||
payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
|
|
@ -378,6 +393,20 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model) err
|
|||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if networkResp.Labels != nil && len(*networkResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *networkResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
if networkResp.Nameservers == nil {
|
||||
model.Nameservers = types.ListNull(types.StringType)
|
||||
} else {
|
||||
|
|
@ -412,11 +441,12 @@ func mapFields(ctx context.Context, networkResp *iaas.Network, model *Model) err
|
|||
model.NetworkId = types.StringValue(networkId)
|
||||
model.Name = types.StringPointerValue(networkResp.Name)
|
||||
model.PublicIP = types.StringPointerValue(networkResp.PublicIp)
|
||||
model.Labels = labels
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(model *Model) (*iaas.CreateNetworkPayload, error) {
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
|
@ -430,6 +460,11 @@ func toCreatePayload(model *Model) (*iaas.CreateNetworkPayload, error) {
|
|||
modelNameservers = append(modelNameservers, nameserverString.ValueString())
|
||||
}
|
||||
|
||||
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
return &iaas.CreateNetworkPayload{
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
AddressFamily: &iaas.CreateNetworkAddressFamily{
|
||||
|
|
@ -438,10 +473,11 @@ func toCreatePayload(model *Model) (*iaas.CreateNetworkPayload, error) {
|
|||
Nameservers: &modelNameservers,
|
||||
},
|
||||
},
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkPayload, error) {
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
|
@ -455,6 +491,11 @@ func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkPayload, error) {
|
|||
modelNameservers = append(modelNameservers, nameserverString.ValueString())
|
||||
}
|
||||
|
||||
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
return &iaas.PartialUpdateNetworkPayload{
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
AddressFamily: &iaas.UpdateNetworkAddressFamily{
|
||||
|
|
@ -462,5 +503,6 @@ func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkPayload, error) {
|
|||
Nameservers: &modelNameservers,
|
||||
},
|
||||
},
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ func TestMapFields(t *testing.T) {
|
|||
IPv4PrefixLength: types.Int64Null(),
|
||||
Prefixes: types.ListNull(types.StringType),
|
||||
PublicIP: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -58,6 +59,9 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix2",
|
||||
},
|
||||
PublicIp: utils.Ptr("publicIp"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid"),
|
||||
|
|
@ -74,6 +78,9 @@ func TestMapFields(t *testing.T) {
|
|||
types.StringValue("prefix2"),
|
||||
}),
|
||||
PublicIP: types.StringValue("publicIp"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -104,6 +111,7 @@ func TestMapFields(t *testing.T) {
|
|||
types.StringValue("ns2"),
|
||||
types.StringValue("ns3"),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -135,6 +143,7 @@ func TestMapFields(t *testing.T) {
|
|||
types.StringValue("prefix2"),
|
||||
types.StringValue("prefix3"),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -190,6 +199,9 @@ func TestToCreatePayload(t *testing.T) {
|
|||
types.StringValue("ns2"),
|
||||
}),
|
||||
IPv4PrefixLength: types.Int64Value(24),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.CreateNetworkPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
|
|
@ -202,13 +214,16 @@ func TestToCreatePayload(t *testing.T) {
|
|||
PrefixLength: utils.Ptr(int64(24)),
|
||||
},
|
||||
},
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := toCreatePayload(tt.input)
|
||||
output, err := toCreatePayload(context.Background(), tt.input)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -240,6 +255,9 @@ func TestToUpdatePayload(t *testing.T) {
|
|||
types.StringValue("ns1"),
|
||||
types.StringValue("ns2"),
|
||||
}),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.PartialUpdateNetworkPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
|
|
@ -251,13 +269,16 @@ func TestToUpdatePayload(t *testing.T) {
|
|||
},
|
||||
},
|
||||
},
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := toUpdatePayload(tt.input)
|
||||
output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType))
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -185,6 +185,11 @@ func (d *networkAreaDataSource) Schema(_ context.Context, _ datasource.SchemaReq
|
|||
int64validator.AtMost(29),
|
||||
},
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ type Model struct {
|
|||
DefaultPrefixLength types.Int64 `tfsdk:"default_prefix_length"`
|
||||
MaxPrefixLength types.Int64 `tfsdk:"max_prefix_length"`
|
||||
MinPrefixLength types.Int64 `tfsdk:"min_prefix_length"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
}
|
||||
|
||||
// Struct corresponding to Model.NetworkRanges[i]
|
||||
|
|
@ -133,12 +134,12 @@ func (r *networkAreaResource) Configure(ctx context.Context, req resource.Config
|
|||
// Schema defines the schema for the resource.
|
||||
func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Network area resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Description: "Network area resource schema. Must have a `region` specified in the provider configuration.",
|
||||
MarkdownDescription: features.AddBetaDescription("Network area resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`\".",
|
||||
MarkdownDescription: features.AddBetaDescription("Network area resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Computed: true,
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
|
|
@ -245,6 +246,11 @@ func (r *networkAreaResource) Schema(_ context.Context, _ resource.SchemaRequest
|
|||
},
|
||||
Default: int64default.StaticInt64(24),
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -365,8 +371,16 @@ func (r *networkAreaResource) Update(ctx context.Context, req resource.UpdateReq
|
|||
}
|
||||
}
|
||||
|
||||
// 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(&model)
|
||||
payload, err := toUpdatePayload(ctx, &model, stateModel.Labels)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network area", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
|
|
@ -431,8 +445,19 @@ func (r *networkAreaResource) Delete(ctx context.Context, req resource.DeleteReq
|
|||
ctx = tflog.SetField(ctx, "organization_id", organizationId)
|
||||
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
|
||||
|
||||
projects, err := r.client.ListNetworkAreaProjects(ctx, organizationId, networkAreaId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API to get the list of projects: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if projects != nil && len(*projects.Items) > 0 {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintln("You still have projects attached to the network area. Please delete or remove them from the network area before deleting the network area."))
|
||||
return
|
||||
}
|
||||
|
||||
// Delete existing network
|
||||
err := r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute()
|
||||
err = r.client.DeleteNetworkArea(ctx, organizationId, networkAreaId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network area", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
|
|
@ -518,9 +543,24 @@ func mapFields(ctx context.Context, networkAreaResp *iaas.NetworkArea, networkAr
|
|||
return fmt.Errorf("mapping network ranges: %w", err)
|
||||
}
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if networkAreaResp.Labels != nil && len(*networkAreaResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *networkAreaResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
model.NetworkAreaId = types.StringValue(networkAreaId)
|
||||
model.Name = types.StringPointerValue(networkAreaResp.Name)
|
||||
model.ProjectCount = types.Int64PointerValue(networkAreaResp.ProjectCount)
|
||||
model.Labels = labels
|
||||
|
||||
if networkAreaResp.Ipv4 != nil {
|
||||
model.TransferNetwork = types.StringPointerValue(networkAreaResp.Ipv4.TransferNetwork)
|
||||
|
|
@ -616,6 +656,11 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
|
|||
return nil, fmt.Errorf("converting network ranges: %w", err)
|
||||
}
|
||||
|
||||
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
return &iaas.CreateNetworkAreaPayload{
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
AddressFamily: &iaas.CreateAreaAddressFamily{
|
||||
|
|
@ -628,10 +673,11 @@ func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkArea
|
|||
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
|
||||
},
|
||||
},
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkAreaPayload, error) {
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.PartialUpdateNetworkAreaPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
|
@ -645,6 +691,11 @@ func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkAreaPayload, error
|
|||
modelDefaultNameservers = append(modelDefaultNameservers, nameserverString.ValueString())
|
||||
}
|
||||
|
||||
labels, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
return &iaas.PartialUpdateNetworkAreaPayload{
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
AddressFamily: &iaas.UpdateAreaAddressFamily{
|
||||
|
|
@ -655,6 +706,7 @@ func toUpdatePayload(model *Model) (*iaas.PartialUpdateNetworkAreaPayload, error
|
|||
MinPrefixLen: conversion.Int64ValueToPointer(model.MinPrefixLength),
|
||||
},
|
||||
},
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -86,6 +86,7 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix": types.StringValue("prefix-2"),
|
||||
}),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -118,6 +119,9 @@ func TestMapFields(t *testing.T) {
|
|||
MinPrefixLen: utils.Ptr(int64(18)),
|
||||
},
|
||||
Name: utils.Ptr("name"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
&[]iaas.NetworkRange{
|
||||
{
|
||||
|
|
@ -152,6 +156,9 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix": types.StringValue("prefix-2"),
|
||||
}),
|
||||
}),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -226,6 +233,7 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix": types.StringValue("prefix-3"),
|
||||
}),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -286,6 +294,7 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix": types.StringValue("prefix-2"),
|
||||
}),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -334,6 +343,7 @@ func TestMapFields(t *testing.T) {
|
|||
"prefix": types.StringValue("prefix-3"),
|
||||
}),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -412,6 +422,9 @@ func TestToCreatePayload(t *testing.T) {
|
|||
DefaultPrefixLength: types.Int64Value(20),
|
||||
MaxPrefixLength: types.Int64Value(22),
|
||||
MinPrefixLength: types.Int64Value(18),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.CreateNetworkAreaPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
|
|
@ -435,6 +448,9 @@ func TestToCreatePayload(t *testing.T) {
|
|||
MinPrefixLen: utils.Ptr(int64(18)),
|
||||
},
|
||||
},
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -476,6 +492,9 @@ func TestToUpdatePayload(t *testing.T) {
|
|||
DefaultPrefixLength: types.Int64Value(22),
|
||||
MaxPrefixLength: types.Int64Value(24),
|
||||
MinPrefixLength: types.Int64Value(20),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.PartialUpdateNetworkAreaPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
|
|
@ -490,13 +509,16 @@ func TestToUpdatePayload(t *testing.T) {
|
|||
MinPrefixLen: utils.Ptr(int64(20)),
|
||||
},
|
||||
},
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := toUpdatePayload(tt.input)
|
||||
output, err := toUpdatePayload(context.Background(), tt.input, types.MapNull(types.StringType))
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import (
|
|||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
|
|
@ -127,6 +128,11 @@ func (d *networkAreaRouteDataSource) Schema(_ context.Context, _ datasource.Sche
|
|||
Description: "The network, that is reachable though the Next Hop. Should use CIDR notation.",
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -157,7 +163,7 @@ func (d *networkAreaRouteDataSource) Read(ctx context.Context, req datasource.Re
|
|||
return
|
||||
}
|
||||
|
||||
err = mapFields(networkAreaRouteResp, &model)
|
||||
err = mapFields(ctx, networkAreaRouteResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
|
|||
|
|
@ -6,9 +6,11 @@ import (
|
|||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/mapplanmodifier"
|
||||
"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"
|
||||
|
|
@ -42,6 +44,7 @@ type Model struct {
|
|||
NetworkAreaRouteId types.String `tfsdk:"network_area_route_id"`
|
||||
NextHop types.String `tfsdk:"next_hop"`
|
||||
Prefix types.String `tfsdk:"prefix"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
}
|
||||
|
||||
// NewNetworkAreaRouteResource is a helper function to simplify the provider implementation.
|
||||
|
|
@ -107,12 +110,12 @@ func (r *networkAreaRouteResource) Configure(ctx context.Context, req resource.C
|
|||
// Schema defines the schema for the resource.
|
||||
func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
Description: "Network area route resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Description: "Network area route resource schema. Must have a `region` specified in the provider configuration.",
|
||||
MarkdownDescription: features.AddBetaDescription("Network area route resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".",
|
||||
MarkdownDescription: features.AddBetaDescription("Network area route resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Computed: true,
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`organization_id`,`network_area_id`,`network_area_route_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
|
|
@ -172,6 +175,14 @@ func (r *networkAreaRouteResource) Schema(_ context.Context, _ resource.SchemaRe
|
|||
validate.CIDR(),
|
||||
},
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Map{
|
||||
mapplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -192,7 +203,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
|
|||
ctx = tflog.SetField(ctx, "network_area_id", networkAreaId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(&model)
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
|
|
@ -222,7 +233,7 @@ func (r *networkAreaRouteResource) Create(ctx context.Context, req resource.Crea
|
|||
ctx = tflog.SetField(ctx, "network_area_route_id", routeId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(&route, &model)
|
||||
err = mapFields(ctx, &route, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network area route.", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -263,7 +274,7 @@ func (r *networkAreaRouteResource) Read(ctx context.Context, req resource.ReadRe
|
|||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(networkAreaRouteResp, &model)
|
||||
err = mapFields(ctx, networkAreaRouteResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network area route", fmt.Sprintf("Processing API payload: %v", err))
|
||||
return
|
||||
|
|
@ -335,7 +346,7 @@ func (r *networkAreaRouteResource) ImportState(ctx context.Context, req resource
|
|||
tflog.Info(ctx, "Network area route state imported")
|
||||
}
|
||||
|
||||
func mapFields(networkAreaRoute *iaas.Route, model *Model) error {
|
||||
func mapFields(ctx context.Context, networkAreaRoute *iaas.Route, model *Model) error {
|
||||
if networkAreaRoute == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
|
|
@ -361,22 +372,43 @@ func mapFields(networkAreaRoute *iaas.Route, model *Model) error {
|
|||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if networkAreaRoute.Labels != nil && len(*networkAreaRoute.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *networkAreaRoute.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
model.NetworkAreaRouteId = types.StringValue(networkAreaRouteId)
|
||||
model.NextHop = types.StringPointerValue(networkAreaRoute.Nexthop)
|
||||
model.Prefix = types.StringPointerValue(networkAreaRoute.Prefix)
|
||||
model.Labels = labels
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(model *Model) (*iaas.CreateNetworkAreaRoutePayload, error) {
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNetworkAreaRoutePayload, 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 &iaas.CreateNetworkAreaRoutePayload{
|
||||
Ipv4: &[]iaas.Route{
|
||||
{
|
||||
Prefix: conversion.StringValueToPointer(model.Prefix),
|
||||
Nexthop: conversion.StringValueToPointer(model.NextHop),
|
||||
Labels: &labels,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package networkarearoute
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/iaas"
|
||||
|
|
@ -32,6 +34,7 @@ func TestMapFields(t *testing.T) {
|
|||
NetworkAreaRouteId: types.StringValue("narid"),
|
||||
Prefix: types.StringNull(),
|
||||
NextHop: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -45,6 +48,9 @@ func TestMapFields(t *testing.T) {
|
|||
&iaas.Route{
|
||||
Prefix: utils.Ptr("prefix"),
|
||||
Nexthop: utils.Ptr("hop"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("oid,naid,narid"),
|
||||
|
|
@ -53,6 +59,9 @@ func TestMapFields(t *testing.T) {
|
|||
NetworkAreaRouteId: types.StringValue("narid"),
|
||||
Prefix: types.StringValue("prefix"),
|
||||
NextHop: types.StringValue("hop"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
|
|
@ -86,7 +95,7 @@ func TestMapFields(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(tt.input, &tt.state)
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
@ -115,12 +124,18 @@ func TestToCreatePayload(t *testing.T) {
|
|||
input: &Model{
|
||||
Prefix: types.StringValue("prefix"),
|
||||
NextHop: types.StringValue("hop"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
expected: &iaas.CreateNetworkAreaRoutePayload{
|
||||
Ipv4: &[]iaas.Route{
|
||||
{
|
||||
Prefix: utils.Ptr("prefix"),
|
||||
Nexthop: utils.Ptr("hop"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -129,7 +144,7 @@ func TestToCreatePayload(t *testing.T) {
|
|||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := toCreatePayload(tt.input)
|
||||
output, err := toCreatePayload(context.Background(), tt.input)
|
||||
if !tt.isValid && err == nil {
|
||||
t.Fatalf("Should have failed")
|
||||
}
|
||||
|
|
|
|||
206
stackit/internal/services/iaas/networkinterface/datasource.go
Normal file
206
stackit/internal/services/iaas/networkinterface/datasource.go
Normal file
|
|
@ -0,0 +1,206 @@
|
|||
package networkinterface
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// scheduleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var networkInterfaceDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &networkInterfaceDataSource{}
|
||||
)
|
||||
|
||||
// NewNetworkDataSource is a helper function to simplify the provider implementation.
|
||||
func NewNetworkInterfaceDataSource() datasource.DataSource {
|
||||
return &networkInterfaceDataSource{}
|
||||
}
|
||||
|
||||
// networkInterfaceDataSource is the data source implementation.
|
||||
type networkInterfaceDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *networkInterfaceDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_network_interface"
|
||||
}
|
||||
|
||||
func (d *networkInterfaceDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !networkInterfaceDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_network_interface", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
networkInterfaceDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "IaaS client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the data source.
|
||||
func (d *networkInterfaceDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
typeOptions := []string{"server", "metadata", "gateway"}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Network interface datasource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Network interface datasource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal data source ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the network interface is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"network_id": schema.StringAttribute{
|
||||
Description: "The network ID to which the network interface is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"network_interface_id": schema.StringAttribute{
|
||||
Description: "The network interface ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the network interface.",
|
||||
Computed: true,
|
||||
},
|
||||
"allowed_addresses": schema.ListAttribute{
|
||||
Description: "The list of CIDR (Classless Inter-Domain Routing) notations.",
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"device": schema.StringAttribute{
|
||||
Description: "The device UUID of the network interface.",
|
||||
Computed: true,
|
||||
},
|
||||
"ipv4": schema.StringAttribute{
|
||||
Description: "The IPv4 address.",
|
||||
Computed: true,
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a network interface.",
|
||||
ElementType: types.StringType,
|
||||
Computed: true,
|
||||
},
|
||||
"mac": schema.StringAttribute{
|
||||
Description: "The MAC address of network interface.",
|
||||
Computed: true,
|
||||
},
|
||||
"security": schema.BoolAttribute{
|
||||
Description: "The Network Interface Security. If set to false, then no security groups will apply to this network interface.",
|
||||
Computed: true,
|
||||
},
|
||||
"security_group_ids": schema.ListAttribute{
|
||||
Description: "The list of security group UUIDs. If security is set to false, setting this field will lead to an error.",
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Type of network interface. Some of the possible values are: " + utils.SupportedValuesDocumentation(typeOptions),
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (d *networkInterfaceDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
networkId := model.NetworkId.ValueString()
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
networkInterfaceResp, err := d.client.GetNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, networkInterfaceResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", 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, "Network interface read")
|
||||
}
|
||||
636
stackit/internal/services/iaas/networkinterface/resource.go
Normal file
636
stackit/internal/services/iaas/networkinterface/resource.go
Normal file
|
|
@ -0,0 +1,636 @@
|
|||
package networkinterface
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/listvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &networkInterfaceResource{}
|
||||
_ resource.ResourceWithConfigure = &networkInterfaceResource{}
|
||||
_ resource.ResourceWithImportState = &networkInterfaceResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
NetworkId types.String `tfsdk:"network_id"`
|
||||
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
AllowedAddresses types.List `tfsdk:"allowed_addresses"`
|
||||
IPv4 types.String `tfsdk:"ipv4"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
Security types.Bool `tfsdk:"security"`
|
||||
SecurityGroupIds types.List `tfsdk:"security_group_ids"`
|
||||
Device types.String `tfsdk:"device"`
|
||||
Mac types.String `tfsdk:"mac"`
|
||||
Type types.String `tfsdk:"type"`
|
||||
}
|
||||
|
||||
// NewNetworkInterfaceResource is a helper function to simplify the provider implementation.
|
||||
func NewNetworkInterfaceResource() resource.Resource {
|
||||
return &networkInterfaceResource{}
|
||||
}
|
||||
|
||||
// networkResource is the resource implementation.
|
||||
type networkInterfaceResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *networkInterfaceResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_network_interface"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *networkInterfaceResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_network_interface", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *networkInterfaceResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
typeOptions := []string{"server", "metadata", "gateway"}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Network interface resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Network interface resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`network_id`,`network_interface_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the network is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"network_id": schema.StringAttribute{
|
||||
Description: "The network ID to which the network interface is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"network_interface_id": schema.StringAttribute{
|
||||
Description: "The network interface ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the network interface.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"allowed_addresses": schema.ListAttribute{
|
||||
Description: "The list of CIDR (Classless Inter-Domain Routing) notations.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
Validators: []validator.List{
|
||||
listvalidator.ValueStringsAre(
|
||||
validate.CIDR(),
|
||||
),
|
||||
},
|
||||
},
|
||||
"device": schema.StringAttribute{
|
||||
Description: "The device UUID of the network interface.",
|
||||
Computed: true,
|
||||
},
|
||||
"ipv4": schema.StringAttribute{
|
||||
Description: "The IPv4 address.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Validators: []validator.String{
|
||||
validate.IP(),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a network interface.",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
"mac": schema.StringAttribute{
|
||||
Description: "The MAC address of network interface.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"security": schema.BoolAttribute{
|
||||
Description: "The Network Interface Security. If set to false, then no security groups will apply to this network interface.",
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
},
|
||||
"security_group_ids": schema.ListAttribute{
|
||||
Description: "The list of security group UUIDs. If security is set to false, setting this field will lead to an error.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
ElementType: types.StringType,
|
||||
Validators: []validator.List{
|
||||
listvalidator.ValueStringsAre(
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
|
||||
"must match expression"),
|
||||
),
|
||||
},
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "Type of network interface. Some of the possible values are: " + utils.SupportedValuesDocumentation(typeOptions),
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *networkInterfaceResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
networkId := model.NetworkId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new network interface
|
||||
networkInterface, err := r.client.CreateNIC(ctx, projectId, networkId).CreateNICPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
networkInterfaceId := *networkInterface.Id
|
||||
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, networkInterface, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating network interface", 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, "Network interface created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *networkInterfaceResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
networkId := model.NetworkId.ValueString()
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
networkInterfaceResp, err := r.client.GetNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, networkInterfaceResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface", 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, "Network interface read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *networkInterfaceResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
networkId := model.NetworkId.ValueString()
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
// 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 network interface", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing network
|
||||
nicResp, err := r.client.UpdateNIC(ctx, projectId, networkId, networkInterfaceId).UpdateNICPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, nicResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating network interface", 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, "Network interface updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *networkInterfaceResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
networkId := model.NetworkId.ValueString()
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
// Delete existing network interface
|
||||
err := r.client.DeleteNIC(ctx, projectId, networkId, networkInterfaceId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting network interface", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "Network interface deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,network_id,network_interface_id
|
||||
func (r *networkInterfaceResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing network interface",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[network_id],[network_interface_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
networkId := idParts[1]
|
||||
networkInterfaceId := idParts[2]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "network_id", networkId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_id"), networkId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), networkInterfaceId)...)
|
||||
tflog.Info(ctx, "Network interface state imported")
|
||||
}
|
||||
|
||||
func mapFields(ctx context.Context, networkInterfaceResp *iaas.NIC, model *Model) error {
|
||||
if networkInterfaceResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var networkInterfaceId string
|
||||
if model.NetworkInterfaceId.ValueString() != "" {
|
||||
networkInterfaceId = model.NetworkInterfaceId.ValueString()
|
||||
} else if networkInterfaceResp.NetworkId != nil {
|
||||
networkInterfaceId = *networkInterfaceResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("network interface id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
model.NetworkId.ValueString(),
|
||||
networkInterfaceId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
respAllowedAddresses := []string{}
|
||||
var diags diag.Diagnostics
|
||||
if networkInterfaceResp.AllowedAddresses == nil {
|
||||
model.AllowedAddresses = types.ListNull(types.StringType)
|
||||
} else {
|
||||
for _, n := range *networkInterfaceResp.AllowedAddresses {
|
||||
respAllowedAddresses = append(respAllowedAddresses, *n.String)
|
||||
}
|
||||
|
||||
modelAllowedAddresses, err := utils.ListValuetoStringSlice(model.AllowedAddresses)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current network interface allowed addresses from model: %w", err)
|
||||
}
|
||||
|
||||
reconciledAllowedAddresses := utils.ReconcileStringSlices(modelAllowedAddresses, respAllowedAddresses)
|
||||
|
||||
allowedAddressesTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledAllowedAddresses)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("map network interface allowed addresses: %w", core.DiagsToError(diags))
|
||||
}
|
||||
|
||||
model.AllowedAddresses = allowedAddressesTF
|
||||
}
|
||||
|
||||
if networkInterfaceResp.SecurityGroups == nil {
|
||||
model.SecurityGroupIds = types.ListNull(types.StringType)
|
||||
} else {
|
||||
respSecurityGroups := *networkInterfaceResp.SecurityGroups
|
||||
modelSecurityGroups, err := utils.ListValuetoStringSlice(model.SecurityGroupIds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("get current network interface security groups from model: %w", err)
|
||||
}
|
||||
|
||||
reconciledSecurityGroups := utils.ReconcileStringSlices(modelSecurityGroups, respSecurityGroups)
|
||||
|
||||
securityGroupsTF, diags := types.ListValueFrom(ctx, types.StringType, reconciledSecurityGroups)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("map network interface security groups: %w", core.DiagsToError(diags))
|
||||
}
|
||||
|
||||
model.SecurityGroupIds = securityGroupsTF
|
||||
}
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if networkInterfaceResp.Labels != nil && len(*networkInterfaceResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *networkInterfaceResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
model.NetworkInterfaceId = types.StringValue(networkInterfaceId)
|
||||
model.Name = types.StringPointerValue(networkInterfaceResp.Name)
|
||||
model.IPv4 = types.StringPointerValue(networkInterfaceResp.Ipv4)
|
||||
model.Security = types.BoolPointerValue(networkInterfaceResp.NicSecurity)
|
||||
model.Device = types.StringPointerValue(networkInterfaceResp.Device)
|
||||
model.Mac = types.StringPointerValue(networkInterfaceResp.Mac)
|
||||
model.Type = types.StringPointerValue(networkInterfaceResp.Type)
|
||||
model.Labels = labels
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateNICPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
||||
var labelPayload *map[string]interface{}
|
||||
|
||||
modelSecurityGroups := []string{}
|
||||
if !(model.SecurityGroupIds.IsNull() || model.SecurityGroupIds.IsUnknown()) {
|
||||
for _, ns := range model.SecurityGroupIds.Elements() {
|
||||
securityGroupString, ok := ns.(types.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type assertion failed")
|
||||
}
|
||||
modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString())
|
||||
}
|
||||
}
|
||||
|
||||
allowedAddressesPayload := []iaas.AllowedAddressesInner{}
|
||||
|
||||
if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) {
|
||||
for _, allowedAddressModel := range model.AllowedAddresses.Elements() {
|
||||
allowedAddressString, ok := allowedAddressModel.(types.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type assertion failed")
|
||||
}
|
||||
|
||||
allowedAddressesPayload = append(allowedAddressesPayload, iaas.AllowedAddressesInner{
|
||||
String: conversion.StringValueToPointer(allowedAddressString),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !model.Labels.IsNull() && !model.Labels.IsUnknown() {
|
||||
labelMap, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mapping labels: %w", err)
|
||||
}
|
||||
labelPayload = &labelMap
|
||||
}
|
||||
|
||||
return &iaas.CreateNICPayload{
|
||||
AllowedAddresses: &allowedAddressesPayload,
|
||||
SecurityGroups: &modelSecurityGroups,
|
||||
Labels: labelPayload,
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
Device: conversion.StringValueToPointer(model.Device),
|
||||
Ipv4: conversion.StringValueToPointer(model.IPv4),
|
||||
Mac: conversion.StringValueToPointer(model.Mac),
|
||||
Type: conversion.StringValueToPointer(model.Type),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateNICPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
||||
var labelPayload *map[string]interface{}
|
||||
|
||||
modelSecurityGroups := []string{}
|
||||
for _, ns := range model.SecurityGroupIds.Elements() {
|
||||
securityGroupString, ok := ns.(types.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type assertion failed")
|
||||
}
|
||||
modelSecurityGroups = append(modelSecurityGroups, securityGroupString.ValueString())
|
||||
}
|
||||
|
||||
allowedAddressesPayload := []iaas.AllowedAddressesInner{}
|
||||
|
||||
if !(model.AllowedAddresses.IsNull() || model.AllowedAddresses.IsUnknown()) {
|
||||
for _, allowedAddressModel := range model.AllowedAddresses.Elements() {
|
||||
allowedAddressString, ok := allowedAddressModel.(types.String)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("type assertion failed")
|
||||
}
|
||||
|
||||
allowedAddressesPayload = append(allowedAddressesPayload, iaas.AllowedAddressesInner{
|
||||
String: conversion.StringValueToPointer(allowedAddressString),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if !model.Labels.IsNull() && !model.Labels.IsUnknown() {
|
||||
labelMap, err := conversion.ToJSONMapPartialUpdatePayload(ctx, currentLabels, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("mapping labels: %w", err)
|
||||
}
|
||||
labelPayload = &labelMap
|
||||
}
|
||||
|
||||
return &iaas.UpdateNICPayload{
|
||||
AllowedAddresses: &allowedAddressesPayload,
|
||||
SecurityGroups: &modelSecurityGroups,
|
||||
Labels: labelPayload,
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
}, nil
|
||||
}
|
||||
273
stackit/internal/services/iaas/networkinterface/resource_test.go
Normal file
273
stackit/internal/services/iaas/networkinterface/resource_test.go
Normal file
|
|
@ -0,0 +1,273 @@
|
|||
package networkinterface
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/iaas"
|
||||
)
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.NIC
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"id_ok",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
},
|
||||
&iaas.NIC{
|
||||
Id: utils.Ptr("nicid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid,nicid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
Name: types.StringNull(),
|
||||
AllowedAddresses: types.ListNull(types.StringType),
|
||||
SecurityGroupIds: types.ListNull(types.StringType),
|
||||
IPv4: types.StringNull(),
|
||||
Security: types.BoolNull(),
|
||||
Device: types.StringNull(),
|
||||
Mac: types.StringNull(),
|
||||
Type: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"values_ok",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
},
|
||||
&iaas.NIC{
|
||||
Id: utils.Ptr("nicid"),
|
||||
Name: utils.Ptr("name"),
|
||||
AllowedAddresses: &[]iaas.AllowedAddressesInner{
|
||||
{
|
||||
String: utils.Ptr("aa1"),
|
||||
},
|
||||
},
|
||||
SecurityGroups: &[]string{
|
||||
"prefix1",
|
||||
"prefix2",
|
||||
},
|
||||
Ipv4: utils.Ptr("ipv4"),
|
||||
Ipv6: utils.Ptr("ipv6"),
|
||||
NicSecurity: utils.Ptr(true),
|
||||
Device: utils.Ptr("device"),
|
||||
Mac: utils.Ptr("mac"),
|
||||
Status: utils.Ptr("status"),
|
||||
Type: utils.Ptr("type"),
|
||||
Labels: &map[string]interface{}{
|
||||
"label1": "ref1",
|
||||
},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid,nicid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
Name: types.StringValue("name"),
|
||||
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("aa1"),
|
||||
}),
|
||||
SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("prefix1"),
|
||||
types.StringValue("prefix2"),
|
||||
}),
|
||||
IPv4: types.StringValue("ipv4"),
|
||||
Security: types.BoolValue(true),
|
||||
Device: types.StringValue("device"),
|
||||
Mac: types.StringValue("mac"),
|
||||
Type: types.StringValue("type"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{"label1": types.StringValue("ref1")}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"allowed_addresses_changed_outside_tf",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("aa1"),
|
||||
}),
|
||||
},
|
||||
&iaas.NIC{
|
||||
Id: utils.Ptr("nicid"),
|
||||
AllowedAddresses: &[]iaas.AllowedAddressesInner{
|
||||
{
|
||||
String: utils.Ptr("aa2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid,nicid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
NetworkId: types.StringValue("nid"),
|
||||
NetworkInterfaceId: types.StringValue("nicid"),
|
||||
Name: types.StringNull(),
|
||||
SecurityGroupIds: types.ListNull(types.StringType),
|
||||
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("aa2"),
|
||||
}),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
},
|
||||
&iaas.NIC{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
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 *iaas.CreateNICPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("sg1"),
|
||||
types.StringValue("sg2"),
|
||||
}),
|
||||
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("aa1"),
|
||||
}),
|
||||
},
|
||||
&iaas.CreateNICPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
SecurityGroups: &[]string{
|
||||
"sg1",
|
||||
"sg2",
|
||||
},
|
||||
AllowedAddresses: &[]iaas.AllowedAddressesInner{
|
||||
{
|
||||
String: utils.Ptr("aa1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
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 *iaas.UpdateNICPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
SecurityGroupIds: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("sg1"),
|
||||
types.StringValue("sg2"),
|
||||
}),
|
||||
AllowedAddresses: types.ListValueMust(types.StringType, []attr.Value{
|
||||
types.StringValue("aa1"),
|
||||
}),
|
||||
},
|
||||
&iaas.UpdateNICPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
SecurityGroups: &[]string{
|
||||
"sg1",
|
||||
"sg2",
|
||||
},
|
||||
AllowedAddresses: &[]iaas.AllowedAddressesInner{
|
||||
{
|
||||
String: utils.Ptr("aa1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
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)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,303 @@
|
|||
package networkinterfaceattach
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &networkInterfaceAttachResource{}
|
||||
_ resource.ResourceWithConfigure = &networkInterfaceAttachResource{}
|
||||
_ resource.ResourceWithImportState = &networkInterfaceAttachResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
ServerId types.String `tfsdk:"server_id"`
|
||||
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
|
||||
}
|
||||
|
||||
// NewNetworkInterfaceAttachResource is a helper function to simplify the provider implementation.
|
||||
func NewNetworkInterfaceAttachResource() resource.Resource {
|
||||
return &networkInterfaceAttachResource{}
|
||||
}
|
||||
|
||||
// networkInterfaceAttachResource is the resource implementation.
|
||||
type networkInterfaceAttachResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_server_network_interface_attach"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_network_interface_attach", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Network interface attachment resource schema. Attaches a network interface to a server. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`network_interface_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the network interface attachment is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"network_interface_id": schema.StringAttribute{
|
||||
Description: "The network interface ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
// Create new network interface attachment
|
||||
err := r.client.AddNICToServer(ctx, projectId, serverId, networkInterfaceId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching network interface to server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
projectId,
|
||||
serverId,
|
||||
networkInterfaceId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
// Set state to fully populated data
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Network interface attachment created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
networkInterfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", networkInterfaceId)
|
||||
|
||||
nics, err := r.client.ListServerNICs(ctx, projectId, serverId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface attachment", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if nics == nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading network interface attachment", "List of network interfaces attached to the server is nil")
|
||||
return
|
||||
}
|
||||
|
||||
if nics.Items != nil {
|
||||
for _, nic := range *nics.Items {
|
||||
if nic.Id == nil || (nic.Id != nil && *nic.Id != networkInterfaceId) {
|
||||
continue
|
||||
}
|
||||
// Set refreshed state
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Network interface attachment read")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no matching network interface was found, the attachment no longer exists
|
||||
resp.State.RemoveResource(ctx)
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Update is not supported, all fields require replace
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
network_interfaceId := model.NetworkInterfaceId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId)
|
||||
|
||||
// Remove network_interface from server
|
||||
err := r.client.RemoveNICFromServer(ctx, projectId, serverId, network_interfaceId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing network interface from server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "Network interface attachment deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,server_id
|
||||
func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing network_interface attachment",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[network_interface_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
serverId := idParts[1]
|
||||
network_interfaceId := idParts[2]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
ctx = tflog.SetField(ctx, "network_interface_id", network_interfaceId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("network_interface_id"), network_interfaceId)...)
|
||||
tflog.Info(ctx, "Network interface attachment state imported")
|
||||
}
|
||||
171
stackit/internal/services/iaas/publicip/datasource.go
Normal file
171
stackit/internal/services/iaas/publicip/datasource.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// publicIpDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var publicIpDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &publicIpDataSource{}
|
||||
)
|
||||
|
||||
// NewVolumeDataSource is a helper function to simplify the provider implementation.
|
||||
func NewPublicIpDataSource() datasource.DataSource {
|
||||
return &publicIpDataSource{}
|
||||
}
|
||||
|
||||
// publicIpDataSource is the data source implementation.
|
||||
type publicIpDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *publicIpDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_public_ip"
|
||||
}
|
||||
|
||||
func (d *publicIpDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !publicIpDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_public_ip", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
publicIpDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *publicIpDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Volume resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Volume resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`public_ip_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the public IP is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"public_ip_id": schema.StringAttribute{
|
||||
Description: "The public IP ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"ip": schema.StringAttribute{
|
||||
Description: "The IP address.",
|
||||
Computed: true,
|
||||
},
|
||||
"network_interface_id": schema.StringAttribute{
|
||||
Description: "Associates the public IP with a network interface or a virtual IP (ID).",
|
||||
Computed: true,
|
||||
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,
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (d *publicIpDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
publicIpId := model.PublicIpId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
|
||||
|
||||
publicIpResp, err := d.client.GetPublicIP(ctx, projectId, publicIpId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, publicIpResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", 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, "public IP read")
|
||||
}
|
||||
431
stackit/internal/services/iaas/publicip/resource.go
Normal file
431
stackit/internal/services/iaas/publicip/resource.go
Normal file
|
|
@ -0,0 +1,431 @@
|
|||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &publicIpResource{}
|
||||
_ resource.ResourceWithConfigure = &publicIpResource{}
|
||||
_ resource.ResourceWithImportState = &publicIpResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
PublicIpId types.String `tfsdk:"public_ip_id"`
|
||||
Ip types.String `tfsdk:"ip"`
|
||||
NetworkInterfaceId types.String `tfsdk:"network_interface_id"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
}
|
||||
|
||||
// NewPublicIpResource is a helper function to simplify the provider implementation.
|
||||
func NewPublicIpResource() resource.Resource {
|
||||
return &publicIpResource{}
|
||||
}
|
||||
|
||||
// publicIpResource is the resource implementation.
|
||||
type publicIpResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *publicIpResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_public_ip"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *publicIpResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_public_ip", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *publicIpResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Public IP resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Public IP resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`public_ip_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the public IP is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"public_ip_id": schema.StringAttribute{
|
||||
Description: "The public IP ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"ip": schema.StringAttribute{
|
||||
Description: "The IP address.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.IP(),
|
||||
},
|
||||
},
|
||||
"network_interface_id": schema.StringAttribute{
|
||||
Description: "Associates the public IP with a network interface or a virtual IP (ID).",
|
||||
Optional: true,
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *publicIpResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new public IP
|
||||
|
||||
publicIp, err := r.client.CreatePublicIP(ctx, projectId).CreatePublicIPPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", *publicIp.Id)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, publicIp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating public IP", 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, "Public IP created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *publicIpResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
publicIpId := model.PublicIpId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
|
||||
|
||||
publicIpResp, err := r.client.GetPublicIP(ctx, projectId, publicIpId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, publicIpResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading public IP", 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, "public IP read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *publicIpResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
publicIpId := model.PublicIpId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
|
||||
|
||||
// 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 public IP", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing public IP
|
||||
updatedPublicIp, err := r.client.UpdatePublicIP(ctx, projectId, publicIpId).UpdatePublicIPPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, updatedPublicIp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating public IP", 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, "public IP updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *publicIpResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
publicIpId := model.PublicIpId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
|
||||
|
||||
// Delete existing publicIp
|
||||
err := r.client.DeletePublicIP(ctx, projectId, publicIpId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting public IP", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "public IP deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,public_ip_id
|
||||
func (r *publicIpResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing public IP",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[public_ip_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
publicIpId := idParts[1]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "public_ip_id", publicIpId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("public_ip_id"), publicIpId)...)
|
||||
tflog.Info(ctx, "public IP state imported")
|
||||
}
|
||||
|
||||
func mapFields(ctx context.Context, publicIpResp *iaas.PublicIp, model *Model) error {
|
||||
if publicIpResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var publicIpId string
|
||||
if model.PublicIpId.ValueString() != "" {
|
||||
publicIpId = model.PublicIpId.ValueString()
|
||||
} else if publicIpResp.Id != nil {
|
||||
publicIpId = *publicIpResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("public IP id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
publicIpId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if publicIpResp.Labels != nil && len(*publicIpResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *publicIpResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
model.PublicIpId = types.StringValue(publicIpId)
|
||||
model.Ip = types.StringPointerValue(publicIpResp.Ip)
|
||||
if publicIpResp.NetworkInterface != nil {
|
||||
model.NetworkInterfaceId = types.StringPointerValue(publicIpResp.GetNetworkInterface())
|
||||
} else {
|
||||
model.NetworkInterfaceId = types.StringNull()
|
||||
}
|
||||
model.Labels = labels
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreatePublicIPPayload, 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 &iaas.CreatePublicIPPayload{
|
||||
Labels: &labels,
|
||||
Ip: conversion.StringValueToPointer(model.Ip),
|
||||
NetworkInterface: iaas.NewNullableString(conversion.StringValueToPointer(model.NetworkInterfaceId)),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdatePublicIPPayload, 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 &iaas.UpdatePublicIPPayload{
|
||||
Labels: &labels,
|
||||
NetworkInterface: iaas.NewNullableString(conversion.StringValueToPointer(model.NetworkInterfaceId)),
|
||||
}, nil
|
||||
}
|
||||
264
stackit/internal/services/iaas/publicip/resource_test.go
Normal file
264
stackit/internal/services/iaas/publicip/resource_test.go
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
package publicip
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/iaas"
|
||||
)
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.PublicIp
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
},
|
||||
&iaas.PublicIp{
|
||||
Id: utils.Ptr("pipid"),
|
||||
NetworkInterface: iaas.NewNullableString(nil),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,pipid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
Ip: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
NetworkInterfaceId: types.StringNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"simple_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
},
|
||||
&iaas.PublicIp{
|
||||
Id: utils.Ptr("pipid"),
|
||||
Ip: utils.Ptr("ip"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,pipid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
Ip: types.StringValue("ip"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
NetworkInterfaceId: types.StringValue("interface"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_labels",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
},
|
||||
&iaas.PublicIp{
|
||||
Id: utils.Ptr("pipid"),
|
||||
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,pipid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
Ip: types.StringNull(),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
NetworkInterfaceId: types.StringValue("interface"),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"network_interface_id_nil",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
},
|
||||
&iaas.PublicIp{
|
||||
Id: utils.Ptr("pipid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,pipid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
PublicIpId: types.StringValue("pipid"),
|
||||
Ip: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
NetworkInterfaceId: types.StringNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
},
|
||||
&iaas.PublicIp{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
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 *iaas.CreatePublicIPPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Ip: types.StringValue("ip"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
NetworkInterfaceId: types.StringValue("interface"),
|
||||
},
|
||||
&iaas.CreatePublicIPPayload{
|
||||
Ip: utils.Ptr("ip"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"network_interface_nil",
|
||||
&Model{
|
||||
Ip: types.StringValue("ip"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.CreatePublicIPPayload{
|
||||
Ip: utils.Ptr("ip"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
NetworkInterface: iaas.NewNullableString(nil),
|
||||
},
|
||||
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, cmp.AllowUnexported(iaas.NullableString{}))
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToUpdatePayload(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
input *Model
|
||||
expected *iaas.UpdatePublicIPPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Ip: types.StringValue("ip"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
NetworkInterfaceId: types.StringValue("interface"),
|
||||
},
|
||||
&iaas.UpdatePublicIPPayload{
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
NetworkInterface: iaas.NewNullableString(utils.Ptr("interface")),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"network_interface_nil",
|
||||
&Model{
|
||||
Ip: types.StringValue("ip"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.UpdatePublicIPPayload{
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
NetworkInterface: iaas.NewNullableString(nil),
|
||||
},
|
||||
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(iaas.NullableString{}))
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
171
stackit/internal/services/iaas/securitygroup/datasource.go
Normal file
171
stackit/internal/services/iaas/securitygroup/datasource.go
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
package securitygroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// securityGroupDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var securityGroupDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &securityGroupDataSource{}
|
||||
)
|
||||
|
||||
// NewSecurityGroupDataSource is a helper function to simplify the provider implementation.
|
||||
func NewSecurityGroupDataSource() datasource.DataSource {
|
||||
return &securityGroupDataSource{}
|
||||
}
|
||||
|
||||
// securityGroupDataSource is the data source implementation.
|
||||
type securityGroupDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *securityGroupDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_security_group"
|
||||
}
|
||||
|
||||
func (d *securityGroupDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !securityGroupDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
securityGroupDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *securityGroupDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Security group datasource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Security group datasource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the security group is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_id": schema.StringAttribute{
|
||||
Description: "The security group ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the security group.",
|
||||
Computed: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the security group.",
|
||||
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,
|
||||
},
|
||||
"stateful": schema.BoolAttribute{
|
||||
Description: "Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (d *securityGroupDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
securityGroupResp, err := d.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, securityGroupResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", 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, "security group read")
|
||||
}
|
||||
451
stackit/internal/services/iaas/securitygroup/resource.go
Normal file
451
stackit/internal/services/iaas/securitygroup/resource.go
Normal file
|
|
@ -0,0 +1,451 @@
|
|||
package securitygroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/boolplanmodifier"
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &securityGroupResource{}
|
||||
_ resource.ResourceWithConfigure = &securityGroupResource{}
|
||||
_ resource.ResourceWithImportState = &securityGroupResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
SecurityGroupId types.String `tfsdk:"security_group_id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
Stateful types.Bool `tfsdk:"stateful"`
|
||||
}
|
||||
|
||||
// NewSecurityGroupResource is a helper function to simplify the provider implementation.
|
||||
func NewSecurityGroupResource() resource.Resource {
|
||||
return &securityGroupResource{}
|
||||
}
|
||||
|
||||
// securityGroupResource is the resource implementation.
|
||||
type securityGroupResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *securityGroupResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_security_group"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *securityGroupResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *securityGroupResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Security group resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Security group resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the security group is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_id": schema.StringAttribute{
|
||||
Description: "The security group ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the security group.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the security group.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
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,
|
||||
},
|
||||
"stateful": schema.BoolAttribute{
|
||||
Description: "Configures if a security group is stateful or stateless. There can only be one type of security groups per network interface/server.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Bool{
|
||||
boolplanmodifier.RequiresReplace(),
|
||||
boolplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *securityGroupResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new security group
|
||||
|
||||
securityGroup, err := r.client.CreateSecurityGroup(ctx, projectId).CreateSecurityGroupPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
securityGroupId := *securityGroup.Id
|
||||
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, securityGroup, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group", 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, "Security group created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *securityGroupResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_id", securityGroupId)
|
||||
|
||||
securityGroupResp, err := r.client.GetSecurityGroup(ctx, projectId, securityGroupId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, securityGroupResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group", 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, "security group read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *securityGroupResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
// 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 security group", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing security group
|
||||
updatedSecurityGroup, err := r.client.UpdateSecurityGroup(ctx, projectId, securityGroupId).UpdateSecurityGroupPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, updatedSecurityGroup, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group", 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, "security group updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *securityGroupResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
// Delete existing security group
|
||||
err := r.client.DeleteSecurityGroup(ctx, projectId, securityGroupId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "security group deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,security_group_id
|
||||
func (r *securityGroupResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing security group",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
securityGroupId := idParts[1]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...)
|
||||
tflog.Info(ctx, "security group state imported")
|
||||
}
|
||||
|
||||
func mapFields(ctx context.Context, securityGroupResp *iaas.SecurityGroup, model *Model) error {
|
||||
if securityGroupResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var securityGroupId string
|
||||
if model.SecurityGroupId.ValueString() != "" {
|
||||
securityGroupId = model.SecurityGroupId.ValueString()
|
||||
} else if securityGroupResp.Id != nil {
|
||||
securityGroupId = *securityGroupResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("security group id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
securityGroupId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if securityGroupResp.Labels != nil && len(*securityGroupResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *securityGroupResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
model.SecurityGroupId = types.StringValue(securityGroupId)
|
||||
model.Name = types.StringPointerValue(securityGroupResp.Name)
|
||||
model.Description = types.StringPointerValue(securityGroupResp.Description)
|
||||
model.Stateful = types.BoolPointerValue(securityGroupResp.Stateful)
|
||||
model.Labels = labels
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateSecurityGroupPayload, 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 &iaas.CreateSecurityGroupPayload{
|
||||
Stateful: conversion.BoolValueToPointer(model.Stateful),
|
||||
Description: conversion.StringValueToPointer(model.Description),
|
||||
Labels: &labels,
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateSecurityGroupPayload, 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 &iaas.UpdateSecurityGroupPayload{
|
||||
Description: conversion.StringValueToPointer(model.Description),
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
218
stackit/internal/services/iaas/securitygroup/resource_test.go
Normal file
218
stackit/internal/services/iaas/securitygroup/resource_test.go
Normal file
|
|
@ -0,0 +1,218 @@
|
|||
package securitygroup
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/iaas"
|
||||
)
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.SecurityGroup
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
},
|
||||
&iaas.SecurityGroup{
|
||||
Id: utils.Ptr("sgid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
Name: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
Description: types.StringNull(),
|
||||
Stateful: types.BoolNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"simple_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
},
|
||||
// &sourceModel{},
|
||||
&iaas.SecurityGroup{
|
||||
Id: utils.Ptr("sgid"),
|
||||
Name: utils.Ptr("name"),
|
||||
Stateful: utils.Ptr(true),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
Name: types.StringValue("name"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
Stateful: types.BoolValue(true),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_labels",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
},
|
||||
&iaas.SecurityGroup{
|
||||
Id: utils.Ptr("sgid"),
|
||||
Labels: &map[string]interface{}{},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
Name: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
Description: types.StringNull(),
|
||||
Stateful: types.BoolNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
},
|
||||
&iaas.SecurityGroup{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
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 *iaas.CreateSecurityGroupPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
Stateful: types.BoolValue(true),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
},
|
||||
&iaas.CreateSecurityGroupPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
Stateful: utils.Ptr(true),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
},
|
||||
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 *iaas.UpdateSecurityGroupPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
},
|
||||
&iaas.UpdateSecurityGroupPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
},
|
||||
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)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
228
stackit/internal/services/iaas/securitygrouprule/datasource.go
Normal file
228
stackit/internal/services/iaas/securitygrouprule/datasource.go
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
package securitygrouprule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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-log/tflog"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// securityGroupRuleDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var securityGroupRuleDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &securityGroupRuleDataSource{}
|
||||
)
|
||||
|
||||
// NewSecurityGroupRuleDataSource is a helper function to simplify the provider implementation.
|
||||
func NewSecurityGroupRuleDataSource() datasource.DataSource {
|
||||
return &securityGroupRuleDataSource{}
|
||||
}
|
||||
|
||||
// securityGroupRuleDataSource is the data source implementation.
|
||||
type securityGroupRuleDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *securityGroupRuleDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_security_group_rule"
|
||||
}
|
||||
|
||||
func (d *securityGroupRuleDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !securityGroupRuleDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group_rule", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
securityGroupRuleDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *securityGroupRuleDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
directionOptions := []string{"ingress", "egress"}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Security group datasource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Security group datasource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal datasource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the security group rule is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_id": schema.StringAttribute{
|
||||
Description: "The security group ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_rule_id": schema.StringAttribute{
|
||||
Description: "The security group rule ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"direction": schema.StringAttribute{
|
||||
Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.SupportedValuesDocumentation(directionOptions),
|
||||
Computed: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the security group rule.",
|
||||
Computed: true,
|
||||
},
|
||||
"ether_type": schema.StringAttribute{
|
||||
Description: "The ethertype which the rule should match.",
|
||||
Computed: true,
|
||||
},
|
||||
"icmp_parameters": schema.SingleNestedAttribute{
|
||||
Description: "ICMP Parameters.",
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"code": schema.Int64Attribute{
|
||||
Description: "ICMP code. Can be set if the protocol is ICMP.",
|
||||
Computed: true,
|
||||
},
|
||||
"type": schema.Int64Attribute{
|
||||
Description: "ICMP type. Can be set if the protocol is ICMP.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"ip_range": schema.StringAttribute{
|
||||
Description: "The remote IP range which the rule should match.",
|
||||
Computed: true,
|
||||
},
|
||||
"port_range": schema.SingleNestedAttribute{
|
||||
Description: "The range of ports.",
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"max": schema.Int64Attribute{
|
||||
Description: "The maximum port number. Should be greater or equal to the minimum.",
|
||||
Computed: true,
|
||||
},
|
||||
"min": schema.Int64Attribute{
|
||||
Description: "The minimum port number. Should be less or equal to the minimum.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"protocol": schema.SingleNestedAttribute{
|
||||
Description: "The internet protocol which the rule should match.",
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The protocol name which the rule should match.",
|
||||
Computed: true,
|
||||
},
|
||||
"number": schema.Int64Attribute{
|
||||
Description: "The protocol number which the rule should match.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote_security_group_id": schema.StringAttribute{
|
||||
Description: "The remote security group which the rule should match.",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (d *securityGroupRuleDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
|
||||
|
||||
securityGroupRuleResp, err := d.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(securityGroupRuleResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", 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, "security group rule read")
|
||||
}
|
||||
|
|
@ -0,0 +1,93 @@
|
|||
package securitygrouprule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/conversion"
|
||||
)
|
||||
|
||||
// UseNullForUnknownBasedOnProtocolModifier returns a plan modifier that sets a null
|
||||
// value into the planned value, based on the value of the protocol.name attribute.
|
||||
//
|
||||
// To prevent Terraform errors, the framework automatically sets unconfigured
|
||||
// and Computed attributes to an unknown value "(known after apply)" on update.
|
||||
// To prevent always showing "(known after apply)" on update for an attribute, e.g. port_range, which never changes in case the protocol is a specific one,
|
||||
// we set the value to null.
|
||||
// Examples: port_range is only computed if protocol is not icmp and icmp_parameters is only computed if protocol is icmp
|
||||
func UseNullForUnknownBasedOnProtocolModifier() planmodifier.Object {
|
||||
return useNullForUnknownBasedOnProtocolModifier{}
|
||||
}
|
||||
|
||||
// useNullForUnknownBasedOnProtocolModifier implements the plan modifier.
|
||||
type useNullForUnknownBasedOnProtocolModifier struct{}
|
||||
|
||||
func (m useNullForUnknownBasedOnProtocolModifier) Description(_ context.Context) string {
|
||||
return "If protocol.name attribute is set and the value corresponds to an icmp protocol, the value of this attribute in state will be set to null."
|
||||
}
|
||||
|
||||
// MarkdownDescription returns a markdown description of the plan modifier.
|
||||
func (m useNullForUnknownBasedOnProtocolModifier) MarkdownDescription(_ context.Context) string {
|
||||
return "Once set, the value of this attribute in state will be set to null if protocol.name attribute is set and the value corresponds to an icmp protocol."
|
||||
}
|
||||
|
||||
// PlanModifyBool implements the plan modification logic.
|
||||
func (m useNullForUnknownBasedOnProtocolModifier) PlanModifyObject(ctx context.Context, req planmodifier.ObjectRequest, resp *planmodifier.ObjectResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Check if the resource is being created.
|
||||
if req.State.Raw.IsNull() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is a known planned value.
|
||||
if !req.PlanValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// Do nothing if there is an unknown configuration value, otherwise interpolation gets messed up.
|
||||
if req.ConfigValue.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
// If there is an unknown configuration value, check if the value of protocol.name attribute corresponds to an icmp protocol. If it does, set the attribute value to null
|
||||
var model Model
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// If protocol is not configured, return without error.
|
||||
if model.Protocol.IsNull() || model.Protocol.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
protocol := &protocolModel{}
|
||||
diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
protocolName := conversion.StringValueToPointer(protocol.Name)
|
||||
|
||||
if protocolName == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(icmpProtocols, *protocolName) {
|
||||
if model.PortRange.IsUnknown() {
|
||||
resp.PlanValue = types.ObjectNull(portRangeTypes)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if model.IcmpParameters.IsUnknown() {
|
||||
resp.PlanValue = types.ObjectNull(icmpParametersTypes)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// use state for unknown if the value was not set to null
|
||||
resp.PlanValue = req.StateValue
|
||||
}
|
||||
777
stackit/internal/services/iaas/securitygrouprule/resource.go
Normal file
777
stackit/internal/services/iaas/securitygrouprule/resource.go
Normal file
|
|
@ -0,0 +1,777 @@
|
|||
package securitygrouprule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/int64validator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"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/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
|
||||
"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"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &securityGroupRuleResource{}
|
||||
_ resource.ResourceWithConfigure = &securityGroupRuleResource{}
|
||||
_ resource.ResourceWithImportState = &securityGroupRuleResource{}
|
||||
icmpProtocols = []string{"icmp", "ipv6-icmp"}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
SecurityGroupId types.String `tfsdk:"security_group_id"`
|
||||
SecurityGroupRuleId types.String `tfsdk:"security_group_rule_id"`
|
||||
Direction types.String `tfsdk:"direction"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
EtherType types.String `tfsdk:"ether_type"`
|
||||
IcmpParameters types.Object `tfsdk:"icmp_parameters"`
|
||||
IpRange types.String `tfsdk:"ip_range"`
|
||||
PortRange types.Object `tfsdk:"port_range"`
|
||||
Protocol types.Object `tfsdk:"protocol"`
|
||||
RemoteSecurityGroupId types.String `tfsdk:"remote_security_group_id"`
|
||||
}
|
||||
|
||||
type icmpParametersModel struct {
|
||||
Code types.Int64 `tfsdk:"code"`
|
||||
Type types.Int64 `tfsdk:"type"`
|
||||
}
|
||||
|
||||
// Types corresponding to icmpParameters
|
||||
var icmpParametersTypes = map[string]attr.Type{
|
||||
"code": basetypes.Int64Type{},
|
||||
"type": basetypes.Int64Type{},
|
||||
}
|
||||
|
||||
type portRangeModel struct {
|
||||
Max types.Int64 `tfsdk:"max"`
|
||||
Min types.Int64 `tfsdk:"min"`
|
||||
}
|
||||
|
||||
// Types corresponding to portRange
|
||||
var portRangeTypes = map[string]attr.Type{
|
||||
"max": basetypes.Int64Type{},
|
||||
"min": basetypes.Int64Type{},
|
||||
}
|
||||
|
||||
type protocolModel struct {
|
||||
Name types.String `tfsdk:"name"`
|
||||
Number types.Int64 `tfsdk:"number"`
|
||||
}
|
||||
|
||||
// Types corresponding to protocol
|
||||
var protocolTypes = map[string]attr.Type{
|
||||
"name": basetypes.StringType{},
|
||||
"number": basetypes.Int64Type{},
|
||||
}
|
||||
|
||||
// NewSecurityGroupRuleResource is a helper function to simplify the provider implementation.
|
||||
func NewSecurityGroupRuleResource() resource.Resource {
|
||||
return &securityGroupRuleResource{}
|
||||
}
|
||||
|
||||
// securityGroupRuleResource is the resource implementation.
|
||||
type securityGroupRuleResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *securityGroupRuleResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_security_group_rule"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *securityGroupRuleResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_security_group_rule", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
func (r securityGroupRuleResource) ValidateConfig(ctx context.Context, req resource.ValidateConfigRequest, resp *resource.ValidateConfigResponse) {
|
||||
var model Model
|
||||
|
||||
resp.Diagnostics.Append(req.Config.Get(ctx, &model)...)
|
||||
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
// If protocol is not configured, return without error.
|
||||
if model.Protocol.IsNull() || model.Protocol.IsUnknown() {
|
||||
return
|
||||
}
|
||||
|
||||
protocol := &protocolModel{}
|
||||
diags := model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
|
||||
protocolName := conversion.StringValueToPointer(protocol.Name)
|
||||
|
||||
if protocolName == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if slices.Contains(icmpProtocols, *protocolName) {
|
||||
if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("port_range"),
|
||||
"Conflicting attribute configuration",
|
||||
"`port_range` attribute can't be provided if `protocol.name` is set to `icmp` or `ipv6-icmp`",
|
||||
)
|
||||
}
|
||||
} else {
|
||||
if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) {
|
||||
resp.Diagnostics.AddAttributeError(
|
||||
path.Root("icmp_parameters"),
|
||||
"Conflicting attribute configuration",
|
||||
"`icmp_parameters` attribute can't be provided if `protocol.name` is not `icmp` or `ipv6-icmp`",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *securityGroupRuleResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
directionOptions := []string{"ingress", "egress"}
|
||||
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Security group rule resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Security group rule resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`security_group_id`,`security_group_rule_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the security group rule is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_id": schema.StringAttribute{
|
||||
Description: "The security group ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"security_group_rule_id": schema.StringAttribute{
|
||||
Description: "The security group rule ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The rule description.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplaceIfConfigured(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtMost(127),
|
||||
},
|
||||
},
|
||||
"direction": schema.StringAttribute{
|
||||
Description: "The direction of the traffic which the rule should match. Some of the possible values are: " + utils.SupportedValuesDocumentation(directionOptions),
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"ether_type": schema.StringAttribute{
|
||||
Description: "The ethertype which the rule should match.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
stringplanmodifier.RequiresReplaceIfConfigured(),
|
||||
},
|
||||
},
|
||||
"icmp_parameters": schema.SingleNestedAttribute{
|
||||
Description: "ICMP Parameters. These parameters should only be provided if the protocol is ICMP.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Object{
|
||||
UseNullForUnknownBasedOnProtocolModifier(),
|
||||
objectplanmodifier.RequiresReplaceIfConfigured(),
|
||||
},
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"code": schema.Int64Attribute{
|
||||
Description: "ICMP code. Can be set if the protocol is ICMP.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(0),
|
||||
int64validator.AtMost(255),
|
||||
},
|
||||
},
|
||||
"type": schema.Int64Attribute{
|
||||
Description: "ICMP type. Can be set if the protocol is ICMP.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(0),
|
||||
int64validator.AtMost(255),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"ip_range": schema.StringAttribute{
|
||||
Description: "The remote IP range which the rule should match.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.IP(),
|
||||
},
|
||||
},
|
||||
"port_range": schema.SingleNestedAttribute{
|
||||
Description: "The range of ports. This should only be provided if the protocol is not ICMP.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Object{
|
||||
objectplanmodifier.RequiresReplaceIfConfigured(),
|
||||
UseNullForUnknownBasedOnProtocolModifier(),
|
||||
},
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"max": schema.Int64Attribute{
|
||||
Description: "The maximum port number. Should be greater or equal to the minimum.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(0),
|
||||
int64validator.AtMost(65535),
|
||||
},
|
||||
},
|
||||
"min": schema.Int64Attribute{
|
||||
Description: "The minimum port number. Should be less or equal to the maximum.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(0),
|
||||
int64validator.AtMost(65535),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"protocol": schema.SingleNestedAttribute{
|
||||
Description: "The internet protocol which the rule should match.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Object{
|
||||
objectplanmodifier.RequiresReplaceIfConfigured(),
|
||||
objectplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The protocol name which the rule should match. Either `name` or `number` must be provided.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.AtLeastOneOf(
|
||||
path.MatchRoot("protocol").AtName("number"),
|
||||
),
|
||||
stringvalidator.ConflictsWith(
|
||||
path.MatchRoot("protocol").AtName("number"),
|
||||
),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
stringplanmodifier.RequiresReplaceIfConfigured(),
|
||||
},
|
||||
},
|
||||
"number": schema.Int64Attribute{
|
||||
Description: "The protocol number which the rule should match. Either `name` or `number` must be provided.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.UseStateForUnknown(),
|
||||
int64planmodifier.RequiresReplaceIfConfigured(),
|
||||
},
|
||||
Validators: []validator.Int64{
|
||||
int64validator.AtLeast(0),
|
||||
int64validator.AtMost(255),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"remote_security_group_id": schema.StringAttribute{
|
||||
Description: "The remote security group which the rule should match.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *securityGroupRuleResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
|
||||
var icmpParameters *icmpParametersModel
|
||||
if !(model.IcmpParameters.IsNull() || model.IcmpParameters.IsUnknown()) {
|
||||
icmpParameters = &icmpParametersModel{}
|
||||
diags = model.IcmpParameters.As(ctx, icmpParameters, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var portRange *portRangeModel
|
||||
if !(model.PortRange.IsNull() || model.PortRange.IsUnknown()) {
|
||||
portRange = &portRangeModel{}
|
||||
diags = model.PortRange.As(ctx, portRange, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
var protocol *protocolModel
|
||||
if !(model.Protocol.IsNull() || model.Protocol.IsUnknown()) {
|
||||
protocol = &protocolModel{}
|
||||
diags = model.Protocol.As(ctx, protocol, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(&model, icmpParameters, portRange, protocol)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new security group rule
|
||||
securityGroupRule, err := r.client.CreateSecurityGroupRule(ctx, projectId, securityGroupId).CreateSecurityGroupRulePayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = tflog.SetField(ctx, "security_group_rule_id", *securityGroupRule.Id)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(securityGroupRule, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating security group rule", 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, "Security group rule created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *securityGroupRuleResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
|
||||
|
||||
securityGroupRuleResp, err := r.client.GetSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(securityGroupRuleResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading security group rule", 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, "security group rule read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *securityGroupRuleResource) Update(ctx context.Context, _ resource.UpdateRequest, resp *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Update shouldn't be called
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating security group rule", "Security group rule can't be updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *securityGroupRuleResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
securityGroupId := model.SecurityGroupId.ValueString()
|
||||
securityGroupRuleId := model.SecurityGroupRuleId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
|
||||
|
||||
// Delete existing security group rule
|
||||
err := r.client.DeleteSecurityGroupRule(ctx, projectId, securityGroupId, securityGroupRuleId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting security group rule", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "security group rule deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,security_group_id, security_group_rule_id
|
||||
func (r *securityGroupRuleResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing security group rule",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[security_group_id],[security_group_rule_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
securityGroupId := idParts[1]
|
||||
securityGroupRuleId := idParts[2]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "security_group_id", securityGroupId)
|
||||
ctx = tflog.SetField(ctx, "security_group_rule_id", securityGroupRuleId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_id"), securityGroupId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("security_group_rule_id"), securityGroupRuleId)...)
|
||||
tflog.Info(ctx, "security group rule state imported")
|
||||
}
|
||||
|
||||
func mapFields(securityGroupRuleResp *iaas.SecurityGroupRule, model *Model) error {
|
||||
if securityGroupRuleResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var securityGroupRuleId string
|
||||
if model.SecurityGroupRuleId.ValueString() != "" {
|
||||
securityGroupRuleId = model.SecurityGroupRuleId.ValueString()
|
||||
} else if securityGroupRuleResp.Id != nil {
|
||||
securityGroupRuleId = *securityGroupRuleResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("security group rule id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
model.SecurityGroupId.ValueString(),
|
||||
securityGroupRuleId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
model.SecurityGroupRuleId = types.StringValue(securityGroupRuleId)
|
||||
model.Direction = types.StringPointerValue(securityGroupRuleResp.Direction)
|
||||
model.Description = types.StringPointerValue(securityGroupRuleResp.Description)
|
||||
model.EtherType = types.StringPointerValue(securityGroupRuleResp.Ethertype)
|
||||
model.IpRange = types.StringPointerValue(securityGroupRuleResp.IpRange)
|
||||
model.RemoteSecurityGroupId = types.StringPointerValue(securityGroupRuleResp.RemoteSecurityGroupId)
|
||||
|
||||
err := mapIcmpParameters(securityGroupRuleResp, model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("map icmp_parameters: %w", err)
|
||||
}
|
||||
err = mapPortRange(securityGroupRuleResp, model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("map port_range: %w", err)
|
||||
}
|
||||
err = mapProtocol(securityGroupRuleResp, model)
|
||||
if err != nil {
|
||||
return fmt.Errorf("map protocol: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapIcmpParameters(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
|
||||
if securityGroupRuleResp.IcmpParameters == nil {
|
||||
m.IcmpParameters = types.ObjectNull(icmpParametersTypes)
|
||||
return nil
|
||||
}
|
||||
|
||||
icmpParametersValues := map[string]attr.Value{
|
||||
"type": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Type),
|
||||
"code": types.Int64Value(*securityGroupRuleResp.IcmpParameters.Code),
|
||||
}
|
||||
|
||||
icmpParametersObject, diags := types.ObjectValue(icmpParametersTypes, icmpParametersValues)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("create icmpParameters object: %w", core.DiagsToError(diags))
|
||||
}
|
||||
m.IcmpParameters = icmpParametersObject
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapPortRange(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
|
||||
if securityGroupRuleResp.PortRange == nil {
|
||||
m.PortRange = types.ObjectNull(portRangeTypes)
|
||||
return nil
|
||||
}
|
||||
|
||||
portRangeMax := types.Int64Null()
|
||||
portRangeMin := types.Int64Null()
|
||||
|
||||
if securityGroupRuleResp.PortRange.Max != nil {
|
||||
portRangeMax = types.Int64Value(*securityGroupRuleResp.PortRange.Max)
|
||||
}
|
||||
|
||||
if securityGroupRuleResp.PortRange.Min != nil {
|
||||
portRangeMin = types.Int64Value(*securityGroupRuleResp.PortRange.Min)
|
||||
}
|
||||
|
||||
portRangeValues := map[string]attr.Value{
|
||||
"max": portRangeMax,
|
||||
"min": portRangeMin,
|
||||
}
|
||||
|
||||
portRangeObject, diags := types.ObjectValue(portRangeTypes, portRangeValues)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("create portRange object: %w", core.DiagsToError(diags))
|
||||
}
|
||||
m.PortRange = portRangeObject
|
||||
return nil
|
||||
}
|
||||
|
||||
func mapProtocol(securityGroupRuleResp *iaas.SecurityGroupRule, m *Model) error {
|
||||
if securityGroupRuleResp.Protocol == nil {
|
||||
m.Protocol = types.ObjectNull(protocolTypes)
|
||||
return nil
|
||||
}
|
||||
|
||||
protocolNumberValue := types.Int64Null()
|
||||
if securityGroupRuleResp.Protocol.Number != nil {
|
||||
protocolNumberValue = types.Int64Value(*securityGroupRuleResp.Protocol.Number)
|
||||
}
|
||||
|
||||
protocolNameValue := types.StringNull()
|
||||
if securityGroupRuleResp.Protocol.Name != nil {
|
||||
protocolNameValue = types.StringValue(*securityGroupRuleResp.Protocol.Name)
|
||||
}
|
||||
|
||||
protocolValues := map[string]attr.Value{
|
||||
"name": protocolNameValue,
|
||||
"number": protocolNumberValue,
|
||||
}
|
||||
protocolObject, diags := types.ObjectValue(protocolTypes, protocolValues)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("create protocol object: %w", core.DiagsToError(diags))
|
||||
}
|
||||
m.Protocol = protocolObject
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(model *Model, icmpParameters *icmpParametersModel, portRange *portRangeModel, protocol *protocolModel) (*iaas.CreateSecurityGroupRulePayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
||||
payloadIcmpParameters, err := toIcmpParametersPayload(icmpParameters)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting icmp parameters: %w", err)
|
||||
}
|
||||
|
||||
payloadPortRange, err := toPortRangePayload(portRange)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting port range: %w", err)
|
||||
}
|
||||
|
||||
payloadProtocol, err := toProtocolPayload(protocol)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting protocol: %w", err)
|
||||
}
|
||||
|
||||
return &iaas.CreateSecurityGroupRulePayload{
|
||||
Description: conversion.StringValueToPointer(model.Description),
|
||||
Direction: conversion.StringValueToPointer(model.Direction),
|
||||
Ethertype: conversion.StringValueToPointer(model.EtherType),
|
||||
IpRange: conversion.StringValueToPointer(model.IpRange),
|
||||
RemoteSecurityGroupId: conversion.StringValueToPointer(model.RemoteSecurityGroupId),
|
||||
IcmpParameters: payloadIcmpParameters,
|
||||
PortRange: payloadPortRange,
|
||||
Protocol: payloadProtocol,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toIcmpParametersPayload(icmpParameters *icmpParametersModel) (*iaas.ICMPParameters, error) {
|
||||
if icmpParameters == nil {
|
||||
return nil, nil
|
||||
}
|
||||
payloadParams := &iaas.ICMPParameters{}
|
||||
|
||||
payloadParams.Code = conversion.Int64ValueToPointer(icmpParameters.Code)
|
||||
payloadParams.Type = conversion.Int64ValueToPointer(icmpParameters.Type)
|
||||
|
||||
return payloadParams, nil
|
||||
}
|
||||
|
||||
func toPortRangePayload(portRange *portRangeModel) (*iaas.PortRange, error) {
|
||||
if portRange == nil {
|
||||
return nil, nil
|
||||
}
|
||||
payloadPortRange := &iaas.PortRange{}
|
||||
|
||||
payloadPortRange.Max = conversion.Int64ValueToPointer(portRange.Max)
|
||||
payloadPortRange.Min = conversion.Int64ValueToPointer(portRange.Min)
|
||||
|
||||
return payloadPortRange, nil
|
||||
}
|
||||
|
||||
func toProtocolPayload(protocol *protocolModel) (*iaas.CreateProtocol, error) {
|
||||
if protocol == nil {
|
||||
return nil, nil
|
||||
}
|
||||
payloadProtocol := &iaas.CreateProtocol{}
|
||||
|
||||
payloadProtocol.String = conversion.StringValueToPointer(protocol.Name)
|
||||
payloadProtocol.Int64 = conversion.Int64ValueToPointer(protocol.Number)
|
||||
|
||||
return payloadProtocol, nil
|
||||
}
|
||||
|
|
@ -0,0 +1,305 @@
|
|||
package securitygrouprule
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types"
|
||||
"github.com/hashicorp/terraform-plugin-framework/types/basetypes"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
)
|
||||
|
||||
var fixtureModelIcmpParameters = types.ObjectValueMust(icmpParametersTypes, map[string]attr.Value{
|
||||
"code": types.Int64Value(1),
|
||||
"type": types.Int64Value(2),
|
||||
})
|
||||
|
||||
var fixtureIcmpParameters = iaas.ICMPParameters{
|
||||
Code: utils.Ptr(int64(1)),
|
||||
Type: utils.Ptr(int64(2)),
|
||||
}
|
||||
|
||||
var fixtureModelPortRange = types.ObjectValueMust(portRangeTypes, map[string]attr.Value{
|
||||
"max": types.Int64Value(2),
|
||||
"min": types.Int64Value(1),
|
||||
})
|
||||
|
||||
var fixturePortRange = iaas.PortRange{
|
||||
Max: utils.Ptr(int64(2)),
|
||||
Min: utils.Ptr(int64(1)),
|
||||
}
|
||||
|
||||
var fixtureModelProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{
|
||||
"name": types.StringValue("name"),
|
||||
"number": types.Int64Value(1),
|
||||
})
|
||||
|
||||
var fixtureProtocol = iaas.Protocol{
|
||||
Name: utils.Ptr("name"),
|
||||
Number: utils.Ptr(int64(1)),
|
||||
}
|
||||
|
||||
var fixtureModelCreateProtocol = types.ObjectValueMust(protocolTypes, map[string]attr.Value{
|
||||
"name": types.StringValue("name"),
|
||||
"number": types.Int64Null(),
|
||||
})
|
||||
|
||||
var fixtureCreateProtocol = iaas.CreateProtocol{
|
||||
String: utils.Ptr("name"),
|
||||
}
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.SecurityGroupRule
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
},
|
||||
&iaas.SecurityGroupRule{
|
||||
Id: utils.Ptr("sgrid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid,sgrid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Direction: types.StringNull(),
|
||||
Description: types.StringNull(),
|
||||
EtherType: types.StringNull(),
|
||||
IpRange: types.StringNull(),
|
||||
RemoteSecurityGroupId: types.StringNull(),
|
||||
IcmpParameters: types.ObjectNull(icmpParametersTypes),
|
||||
PortRange: types.ObjectNull(portRangeTypes),
|
||||
Protocol: types.ObjectNull(protocolTypes),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"simple_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
},
|
||||
&iaas.SecurityGroupRule{
|
||||
Id: utils.Ptr("sgrid"),
|
||||
Description: utils.Ptr("desc"),
|
||||
Direction: utils.Ptr("ingress"),
|
||||
Ethertype: utils.Ptr("ether"),
|
||||
IpRange: utils.Ptr("iprange"),
|
||||
RemoteSecurityGroupId: utils.Ptr("remote"),
|
||||
IcmpParameters: &fixtureIcmpParameters,
|
||||
PortRange: &fixturePortRange,
|
||||
Protocol: &fixtureProtocol,
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid,sgrid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Direction: types.StringValue("ingress"),
|
||||
Description: types.StringValue("desc"),
|
||||
EtherType: types.StringValue("ether"),
|
||||
IpRange: types.StringValue("iprange"),
|
||||
RemoteSecurityGroupId: types.StringValue("remote"),
|
||||
IcmpParameters: fixtureModelIcmpParameters,
|
||||
PortRange: fixtureModelPortRange,
|
||||
Protocol: fixtureModelProtocol,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"protocol_only_with_name",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
|
||||
"name": types.StringValue("name"),
|
||||
"number": types.Int64Null(),
|
||||
}),
|
||||
},
|
||||
&iaas.SecurityGroupRule{
|
||||
Id: utils.Ptr("sgrid"),
|
||||
Protocol: &fixtureProtocol,
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid,sgrid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Direction: types.StringNull(),
|
||||
Description: types.StringNull(),
|
||||
EtherType: types.StringNull(),
|
||||
IpRange: types.StringNull(),
|
||||
RemoteSecurityGroupId: types.StringNull(),
|
||||
IcmpParameters: types.ObjectNull(icmpParametersTypes),
|
||||
PortRange: types.ObjectNull(portRangeTypes),
|
||||
Protocol: fixtureModelProtocol,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"protocol_only_with_number",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Protocol: types.ObjectValueMust(protocolTypes, map[string]attr.Value{
|
||||
"name": types.StringNull(),
|
||||
"number": types.Int64Value(1),
|
||||
}),
|
||||
},
|
||||
&iaas.SecurityGroupRule{
|
||||
Id: utils.Ptr("sgrid"),
|
||||
Protocol: &fixtureProtocol,
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sgid,sgrid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
SecurityGroupRuleId: types.StringValue("sgrid"),
|
||||
Direction: types.StringNull(),
|
||||
Description: types.StringNull(),
|
||||
EtherType: types.StringNull(),
|
||||
IpRange: types.StringNull(),
|
||||
RemoteSecurityGroupId: types.StringNull(),
|
||||
IcmpParameters: types.ObjectNull(icmpParametersTypes),
|
||||
PortRange: types.ObjectNull(portRangeTypes),
|
||||
Protocol: fixtureModelProtocol,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
SecurityGroupId: types.StringValue("sgid"),
|
||||
},
|
||||
&iaas.SecurityGroupRule{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(tt.input, &tt.state)
|
||||
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 *iaas.CreateSecurityGroupRulePayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
&Model{},
|
||||
&iaas.CreateSecurityGroupRulePayload{},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Description: types.StringValue("desc"),
|
||||
Direction: types.StringValue("ingress"),
|
||||
IcmpParameters: fixtureModelIcmpParameters,
|
||||
PortRange: fixtureModelPortRange,
|
||||
Protocol: fixtureModelCreateProtocol,
|
||||
},
|
||||
&iaas.CreateSecurityGroupRulePayload{
|
||||
Description: utils.Ptr("desc"),
|
||||
Direction: utils.Ptr("ingress"),
|
||||
IcmpParameters: &fixtureIcmpParameters,
|
||||
PortRange: &fixturePortRange,
|
||||
Protocol: &fixtureCreateProtocol,
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"nil_model",
|
||||
nil,
|
||||
nil,
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
var icmpParameters *icmpParametersModel
|
||||
var portRange *portRangeModel
|
||||
var protocol *protocolModel
|
||||
if tt.input != nil {
|
||||
if !(tt.input.IcmpParameters.IsNull() || tt.input.IcmpParameters.IsUnknown()) {
|
||||
icmpParameters = &icmpParametersModel{}
|
||||
diags := tt.input.IcmpParameters.As(context.Background(), icmpParameters, basetypes.ObjectAsOptions{})
|
||||
if diags.HasError() {
|
||||
t.Fatalf("Error converting icmp parameters: %v", diags.Errors())
|
||||
}
|
||||
}
|
||||
|
||||
if !(tt.input.PortRange.IsNull() || tt.input.PortRange.IsUnknown()) {
|
||||
portRange = &portRangeModel{}
|
||||
diags := tt.input.PortRange.As(context.Background(), portRange, basetypes.ObjectAsOptions{})
|
||||
if diags.HasError() {
|
||||
t.Fatalf("Error converting port range: %v", diags.Errors())
|
||||
}
|
||||
}
|
||||
|
||||
if !(tt.input.Protocol.IsNull() || tt.input.Protocol.IsUnknown()) {
|
||||
protocol = &protocolModel{}
|
||||
diags := tt.input.Protocol.As(context.Background(), protocol, basetypes.ObjectAsOptions{})
|
||||
if diags.HasError() {
|
||||
t.Fatalf("Error converting protocol: %v", diags.Errors())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
output, err := toCreatePayload(tt.input, icmpParameters, portRange, protocol)
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
167
stackit/internal/services/iaas/server/const.go
Normal file
167
stackit/internal/services/iaas/server/const.go
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
package server
|
||||
|
||||
const markdownDescription = `
|
||||
Server resource schema. Must have a region specified in the provider configuration.` + "\n" + `
|
||||
~> 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.
|
||||
## Example Usage` + "\n" + `
|
||||
|
||||
### Boot from volume` + "\n" +
|
||||
|
||||
"```terraform" + `
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
` + "\n```" + `
|
||||
|
||||
### Boot from existing volume` + "\n" +
|
||||
|
||||
"```terraform" + `
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "boot-from-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
source_type = "volume"
|
||||
source_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
` + "\n```" + `
|
||||
|
||||
### Network setup` + "\n" +
|
||||
|
||||
"```terraform" + `
|
||||
resource "stackit_server" "server-with-network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_network" "network" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-network"
|
||||
nameservers = ["192.0.2.0", "198.51.100.0", "203.0.113.0"]
|
||||
ipv4_prefix_length = 24
|
||||
}
|
||||
|
||||
resource "stackit_security_group" "sec-group" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-security-group"
|
||||
stateful = true
|
||||
}
|
||||
|
||||
resource "stackit_security_group_rule" "rule" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
security_group_id = stackit_security_group.sec-group.security_group_id
|
||||
direction = "ingress"
|
||||
ether_type = "IPv4"
|
||||
}
|
||||
|
||||
resource "stackit_network_interface" "nic" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_id = stackit_network.network.network_id
|
||||
security_group_ids = [stackit_security_group.sec-group.security_group_id]
|
||||
}
|
||||
|
||||
resource "stackit_public_ip" "public-ip" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
|
||||
resource "stackit_server_network_interface_attach" "nic-attachment" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-network.server_id
|
||||
network_interface_id = stackit_network_interface.nic.network_interface_id
|
||||
}
|
||||
` + "\n```" + `
|
||||
|
||||
### Server with attached volume` + "\n" +
|
||||
|
||||
"```terraform" + `
|
||||
resource "stackit_volume" "example-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
size = 12
|
||||
source = {
|
||||
type = "image"
|
||||
id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-volume"
|
||||
availability_zone = "eu01-1"
|
||||
}
|
||||
|
||||
resource "stackit_server" "server-with-volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
name = "example-server"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
availability_zone = "eu01-1"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
}
|
||||
|
||||
resource "stackit_server_volume_attach" "attach_volume" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
server_id = stackit_server.server-with-volume.server_id
|
||||
volume_id = stackit_volume.example-volume.volume_id
|
||||
}
|
||||
` + "\n```" + `
|
||||
|
||||
### Server with user data (cloud-init)` + "\n" +
|
||||
|
||||
"```terraform" + `
|
||||
resource "stackit_server" "user-data" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = "#!/bin/bash\n/bin/su"
|
||||
}
|
||||
|
||||
resource "stackit_server" "user-data-from-file" {
|
||||
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
boot_volume = {
|
||||
size = 64
|
||||
source_type = "image"
|
||||
source_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
|
||||
}
|
||||
name = "example-server"
|
||||
machine_type = "g1.1"
|
||||
keypair_name = "example-keypair"
|
||||
user_data = file("${path.module}/cloud-init.yaml")
|
||||
}
|
||||
` + "\n```"
|
||||
224
stackit/internal/services/iaas/server/datasource.go
Normal file
224
stackit/internal/services/iaas/server/datasource.go
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// serverDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var serverDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &serverDataSource{}
|
||||
)
|
||||
|
||||
// NewServerDataSource is a helper function to simplify the provider implementation.
|
||||
func NewServerDataSource() datasource.DataSource {
|
||||
return &serverDataSource{}
|
||||
}
|
||||
|
||||
// serverDataSource is the data source implementation.
|
||||
type serverDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *serverDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_server"
|
||||
}
|
||||
|
||||
func (d *serverDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !serverDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
serverDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the datasource.
|
||||
func (r *serverDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Server datasource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Server datasource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the server is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the server.",
|
||||
Computed: true,
|
||||
},
|
||||
"machine_type": schema.StringAttribute{
|
||||
MarkdownDescription: "Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html)",
|
||||
Computed: true,
|
||||
},
|
||||
"availability_zone": schema.StringAttribute{
|
||||
Description: "The availability zone of the server.",
|
||||
Computed: true,
|
||||
},
|
||||
"boot_volume": schema.SingleNestedAttribute{
|
||||
Description: "The boot volume for the server",
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"performance_class": schema.StringAttribute{
|
||||
Description: "The performance class of the server.",
|
||||
Computed: true,
|
||||
},
|
||||
"size": schema.Int64Attribute{
|
||||
Description: "The size of the boot volume in GB.",
|
||||
Computed: true,
|
||||
},
|
||||
"type": schema.StringAttribute{
|
||||
Description: "The type of the source. " + utils.SupportedValuesDocumentation(supportedSourceTypes),
|
||||
Computed: true,
|
||||
},
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The ID of the source, either image ID or volume ID",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"image_id": schema.StringAttribute{
|
||||
Description: "The image ID to be used for an ephemeral disk on the server.",
|
||||
Computed: true,
|
||||
},
|
||||
"keypair_name": schema.StringAttribute{
|
||||
Description: "The name of the keypair used during server creation.",
|
||||
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,
|
||||
},
|
||||
"affinity_group": schema.StringAttribute{
|
||||
Description: "The affinity group the server is assigned to.",
|
||||
Computed: true,
|
||||
},
|
||||
"user_data": schema.StringAttribute{
|
||||
Description: "User data that is passed via cloud-init to the server.",
|
||||
Computed: true,
|
||||
},
|
||||
"created_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was created",
|
||||
Computed: true,
|
||||
},
|
||||
"launched_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was launched",
|
||||
Computed: true,
|
||||
},
|
||||
"updated_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was updated",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// // Read refreshes the Terraform state with the latest data.
|
||||
func (r *serverDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
serverResp, err := r.client.GetServer(ctx, projectId, serverId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, serverResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", 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, "server read")
|
||||
}
|
||||
682
stackit/internal/services/iaas/server/resource.go
Normal file
682
stackit/internal/services/iaas/server/resource.go
Normal file
|
|
@ -0,0 +1,682 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/int64planmodifier"
|
||||
"github.com/hashicorp/terraform-plugin-framework/resource/schema/objectplanmodifier"
|
||||
"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"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &serverResource{}
|
||||
_ resource.ResourceWithConfigure = &serverResource{}
|
||||
_ resource.ResourceWithImportState = &serverResource{}
|
||||
|
||||
supportedSourceTypes = []string{"volume", "image"}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
ServerId types.String `tfsdk:"server_id"`
|
||||
MachineType types.String `tfsdk:"machine_type"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
AvailabilityZone types.String `tfsdk:"availability_zone"`
|
||||
BootVolume types.Object `tfsdk:"boot_volume"`
|
||||
ImageId types.String `tfsdk:"image_id"`
|
||||
KeypairName types.String `tfsdk:"keypair_name"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
AffinityGroup types.String `tfsdk:"affinity_group"`
|
||||
UserData types.String `tfsdk:"user_data"`
|
||||
CreatedAt types.String `tfsdk:"created_at"`
|
||||
LaunchedAt types.String `tfsdk:"launched_at"`
|
||||
UpdatedAt types.String `tfsdk:"updated_at"`
|
||||
}
|
||||
|
||||
// Struct corresponding to Model.BootVolume
|
||||
type bootVolumeModel struct {
|
||||
PerformanceClass types.String `tfsdk:"performance_class"`
|
||||
Size types.Int64 `tfsdk:"size"`
|
||||
SourceType types.String `tfsdk:"source_type"`
|
||||
SourceId types.String `tfsdk:"source_id"`
|
||||
}
|
||||
|
||||
// Types corresponding to bootVolumeModel
|
||||
var bootVolumeTypes = map[string]attr.Type{
|
||||
"performance_class": basetypes.StringType{},
|
||||
"size": basetypes.Int64Type{},
|
||||
"source_type": basetypes.StringType{},
|
||||
"source_id": basetypes.StringType{},
|
||||
}
|
||||
|
||||
// NewServerResource is a helper function to simplify the provider implementation.
|
||||
func NewServerResource() resource.Resource {
|
||||
return &serverResource{}
|
||||
}
|
||||
|
||||
// serverResource is the resource implementation.
|
||||
type serverResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *serverResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_server"
|
||||
}
|
||||
|
||||
// ConfigValidators validates the resource configuration
|
||||
func (r *serverResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
|
||||
return []resource.ConfigValidator{
|
||||
resourcevalidator.AtLeastOneOf(
|
||||
path.MatchRoot("image_id"),
|
||||
path.MatchRoot("boot_volume"),
|
||||
),
|
||||
resourcevalidator.Conflicting(
|
||||
path.MatchRoot("image_id"),
|
||||
path.MatchRoot("boot_volume"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *serverResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *serverResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: markdownDescription,
|
||||
Description: "Server resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the server is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the server.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"machine_type": schema.StringAttribute{
|
||||
MarkdownDescription: "Name of the type of the machine for the server. Possible values are documented in [Virtual machine flavors](https://docs.stackit.cloud/stackit/en/virtual-machine-flavors-75137231.html)",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"availability_zone": schema.StringAttribute{
|
||||
Description: "The availability zone of the server.",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
},
|
||||
"boot_volume": schema.SingleNestedAttribute{
|
||||
Description: "The boot volume for the server",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Object{
|
||||
objectplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"performance_class": schema.StringAttribute{
|
||||
Description: "The performance class of the server.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"size": schema.Int64Attribute{
|
||||
Description: "The size of the boot volume in GB. Must be provided when `source_type` is `image`.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Int64{
|
||||
int64planmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"source_type": schema.StringAttribute{
|
||||
Description: "The type of the source. " + utils.SupportedValuesDocumentation(supportedSourceTypes),
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"source_id": schema.StringAttribute{
|
||||
Description: "The ID of the source, either image ID or volume ID",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"image_id": schema.StringAttribute{
|
||||
Description: "The image ID to be used for an ephemeral disk on the server.",
|
||||
Optional: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"keypair_name": schema.StringAttribute{
|
||||
Description: "The name of the keypair used during server creation.",
|
||||
Optional: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
"affinity_group": schema.StringAttribute{
|
||||
Description: "The affinity group the server is assigned to.",
|
||||
Optional: true,
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(36),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`),
|
||||
"must match expression"),
|
||||
},
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"user_data": schema.StringAttribute{
|
||||
Description: "User data that is passed via cloud-init to the server.",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"created_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was created",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"launched_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was launched",
|
||||
Computed: true,
|
||||
},
|
||||
"updated_at": schema.StringAttribute{
|
||||
Description: "Date-time when the server was updated",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *serverResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new server
|
||||
|
||||
server, err := r.client.CreateServer(ctx, projectId).CreateServerPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
serverId := *server.Id
|
||||
server, err = wait.CreateServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", fmt.Sprintf("server creation waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, server, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating server", 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, "Server created")
|
||||
}
|
||||
|
||||
// // Read refreshes the Terraform state with the latest data.
|
||||
func (r *serverResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
serverResp, err := r.client.GetServer(ctx, projectId, serverId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, serverResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading server", 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, "server read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *serverResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
// 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 server", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing server
|
||||
updatedServer, err := r.client.UpdateServer(ctx, projectId, serverId).UpdateServerPayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Update machine type
|
||||
modelMachineType := conversion.StringValueToPointer(model.MachineType)
|
||||
if modelMachineType != nil && updatedServer.MachineType != nil && *modelMachineType != *updatedServer.MachineType {
|
||||
payload := iaas.ResizeServerPayload{
|
||||
MachineType: modelMachineType,
|
||||
}
|
||||
err := r.client.ResizeServer(ctx, projectId, serverId).ResizeServerPayload(payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("Resizing the server, calling API: %v", err))
|
||||
}
|
||||
|
||||
_, err = wait.ResizeServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", fmt.Sprintf("server resize waiting: %v", err))
|
||||
return
|
||||
}
|
||||
// Update server model because the API doesn't return a server object as response
|
||||
updatedServer.MachineType = modelMachineType
|
||||
}
|
||||
|
||||
err = mapFields(ctx, updatedServer, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating server", 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, "server updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *serverResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
// Delete existing server
|
||||
err := r.client.DeleteServer(ctx, projectId, serverId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
_, err = wait.DeleteServerWaitHandler(ctx, r.client, projectId, serverId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting server", fmt.Sprintf("server deletion waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "server deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,server_id
|
||||
func (r *serverResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing server",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
serverId := idParts[1]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
|
||||
tflog.Info(ctx, "server state imported")
|
||||
}
|
||||
|
||||
func mapFields(ctx context.Context, serverResp *iaas.Server, model *Model) error {
|
||||
if serverResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var serverId string
|
||||
if model.ServerId.ValueString() != "" {
|
||||
serverId = model.ServerId.ValueString()
|
||||
} else if serverResp.Id != nil {
|
||||
serverId = *serverResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("Server id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
serverId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if serverResp.Labels != nil && len(*serverResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *serverResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("convert labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
var createdAt basetypes.StringValue
|
||||
if serverResp.CreatedAt != nil {
|
||||
createdAtValue := *serverResp.CreatedAt
|
||||
createdAt = types.StringValue(createdAtValue.Format(time.RFC3339))
|
||||
}
|
||||
var updatedAt basetypes.StringValue
|
||||
if serverResp.UpdatedAt != nil {
|
||||
updatedAtValue := *serverResp.UpdatedAt
|
||||
updatedAt = types.StringValue(updatedAtValue.Format(time.RFC3339))
|
||||
}
|
||||
var launchedAt basetypes.StringValue
|
||||
if serverResp.LaunchedAt != nil {
|
||||
launchedAtValue := *serverResp.LaunchedAt
|
||||
launchedAt = types.StringValue(launchedAtValue.Format(time.RFC3339))
|
||||
}
|
||||
|
||||
model.ServerId = types.StringValue(serverId)
|
||||
model.MachineType = types.StringPointerValue(serverResp.MachineType)
|
||||
model.AvailabilityZone = types.StringPointerValue(serverResp.AvailabilityZone)
|
||||
model.Name = types.StringPointerValue(serverResp.Name)
|
||||
model.Labels = labels
|
||||
model.ImageId = types.StringPointerValue(serverResp.ImageId)
|
||||
model.KeypairName = types.StringPointerValue(serverResp.KeypairName)
|
||||
model.AffinityGroup = types.StringPointerValue(serverResp.AffinityGroup)
|
||||
model.CreatedAt = createdAt
|
||||
model.UpdatedAt = updatedAt
|
||||
model.LaunchedAt = launchedAt
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(ctx context.Context, model *Model) (*iaas.CreateServerPayload, error) {
|
||||
if model == nil {
|
||||
return nil, fmt.Errorf("nil model")
|
||||
}
|
||||
|
||||
var bootVolume = &bootVolumeModel{}
|
||||
if !(model.BootVolume.IsNull() || model.BootVolume.IsUnknown()) {
|
||||
diags := model.BootVolume.As(ctx, bootVolume, basetypes.ObjectAsOptions{})
|
||||
if diags.HasError() {
|
||||
return nil, fmt.Errorf("convert boot volume object to struct: %w", core.DiagsToError(diags))
|
||||
}
|
||||
}
|
||||
|
||||
labels, err := conversion.ToStringInterfaceMap(ctx, model.Labels)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("converting to Go map: %w", err)
|
||||
}
|
||||
|
||||
var bootVolumePayload *iaas.CreateServerPayloadBootVolume
|
||||
if !bootVolume.SourceId.IsNull() && !bootVolume.SourceType.IsNull() {
|
||||
bootVolumePayload = &iaas.CreateServerPayloadBootVolume{
|
||||
PerformanceClass: conversion.StringValueToPointer(bootVolume.PerformanceClass),
|
||||
Size: conversion.Int64ValueToPointer(bootVolume.Size),
|
||||
Source: &iaas.BootVolumeSource{
|
||||
Id: conversion.StringValueToPointer(bootVolume.SourceId),
|
||||
Type: conversion.StringValueToPointer(bootVolume.SourceType),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
var userData *string
|
||||
if !model.UserData.IsNull() && !model.UserData.IsUnknown() {
|
||||
encodedUserData := base64.StdEncoding.EncodeToString([]byte(model.UserData.ValueString()))
|
||||
userData = &encodedUserData
|
||||
}
|
||||
|
||||
return &iaas.CreateServerPayload{
|
||||
AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone),
|
||||
BootVolume: bootVolumePayload,
|
||||
ImageId: conversion.StringValueToPointer(model.ImageId),
|
||||
KeypairName: conversion.StringValueToPointer(model.KeypairName),
|
||||
Labels: &labels,
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
MachineType: conversion.StringValueToPointer(model.MachineType),
|
||||
UserData: userData,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateServerPayload, 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 &iaas.UpdateServerPayload{
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
269
stackit/internal/services/iaas/server/resource_test.go
Normal file
269
stackit/internal/services/iaas/server/resource_test.go
Normal file
|
|
@ -0,0 +1,269 @@
|
|||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"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/iaas"
|
||||
)
|
||||
|
||||
const (
|
||||
userData = "user_data"
|
||||
base64EncodedUserData = "dXNlcl9kYXRh"
|
||||
testTimestampValue = "2006-01-02T15:04:05Z"
|
||||
)
|
||||
|
||||
func testTimestamp() time.Time {
|
||||
timestamp, _ := time.Parse(time.RFC3339, testTimestampValue)
|
||||
return timestamp
|
||||
}
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.Server
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
},
|
||||
&iaas.Server{
|
||||
Id: utils.Ptr("sid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
Name: types.StringNull(),
|
||||
AvailabilityZone: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
ImageId: types.StringNull(),
|
||||
KeypairName: types.StringNull(),
|
||||
AffinityGroup: types.StringNull(),
|
||||
UserData: types.StringNull(),
|
||||
CreatedAt: types.StringNull(),
|
||||
UpdatedAt: types.StringNull(),
|
||||
LaunchedAt: types.StringNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"simple_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
},
|
||||
&iaas.Server{
|
||||
Id: utils.Ptr("sid"),
|
||||
Name: utils.Ptr("name"),
|
||||
AvailabilityZone: utils.Ptr("zone"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
ImageId: utils.Ptr("image_id"),
|
||||
KeypairName: utils.Ptr("keypair_name"),
|
||||
AffinityGroup: utils.Ptr("group_id"),
|
||||
CreatedAt: utils.Ptr(testTimestamp()),
|
||||
UpdatedAt: utils.Ptr(testTimestamp()),
|
||||
LaunchedAt: utils.Ptr(testTimestamp()),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
Name: types.StringValue("name"),
|
||||
AvailabilityZone: types.StringValue("zone"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
ImageId: types.StringValue("image_id"),
|
||||
KeypairName: types.StringValue("keypair_name"),
|
||||
AffinityGroup: types.StringValue("group_id"),
|
||||
CreatedAt: types.StringValue(testTimestampValue),
|
||||
UpdatedAt: types.StringValue(testTimestampValue),
|
||||
LaunchedAt: types.StringValue(testTimestampValue),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_labels",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
},
|
||||
&iaas.Server{
|
||||
Id: utils.Ptr("sid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,sid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
Name: types.StringNull(),
|
||||
AvailabilityZone: types.StringNull(),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
ImageId: types.StringNull(),
|
||||
KeypairName: types.StringNull(),
|
||||
AffinityGroup: types.StringNull(),
|
||||
UserData: types.StringNull(),
|
||||
CreatedAt: types.StringNull(),
|
||||
UpdatedAt: types.StringNull(),
|
||||
LaunchedAt: types.StringNull(),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
},
|
||||
&iaas.Server{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
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 *iaas.CreateServerPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
AvailabilityZone: types.StringValue("zone"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
BootVolume: types.ObjectValueMust(bootVolumeTypes, map[string]attr.Value{
|
||||
"performance_class": types.StringValue("class"),
|
||||
"size": types.Int64Value(1),
|
||||
"source_type": types.StringValue("type"),
|
||||
"source_id": types.StringValue("id"),
|
||||
}),
|
||||
ImageId: types.StringValue("image"),
|
||||
KeypairName: types.StringValue("keypair"),
|
||||
MachineType: types.StringValue("machine_type"),
|
||||
UserData: types.StringValue(userData),
|
||||
},
|
||||
&iaas.CreateServerPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
AvailabilityZone: utils.Ptr("zone"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
BootVolume: &iaas.CreateServerPayloadBootVolume{
|
||||
PerformanceClass: utils.Ptr("class"),
|
||||
Size: utils.Ptr(int64(1)),
|
||||
Source: &iaas.BootVolumeSource{
|
||||
Type: utils.Ptr("type"),
|
||||
Id: utils.Ptr("id"),
|
||||
},
|
||||
},
|
||||
ImageId: utils.Ptr("image"),
|
||||
KeypairName: utils.Ptr("keypair"),
|
||||
MachineType: utils.Ptr("machine_type"),
|
||||
UserData: utils.Ptr(base64EncodedUserData),
|
||||
},
|
||||
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 *iaas.UpdateServerPayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
},
|
||||
&iaas.UpdateServerPayload{
|
||||
Name: utils.Ptr("name"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
},
|
||||
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)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
299
stackit/internal/services/iaas/serviceaccountattach/resource.go
Normal file
299
stackit/internal/services/iaas/serviceaccountattach/resource.go
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
package serviceaccountattach
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &networkInterfaceAttachResource{}
|
||||
_ resource.ResourceWithConfigure = &networkInterfaceAttachResource{}
|
||||
_ resource.ResourceWithImportState = &networkInterfaceAttachResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
ServerId types.String `tfsdk:"server_id"`
|
||||
ServiceAccountEmail types.String `tfsdk:"service_account_email"`
|
||||
}
|
||||
|
||||
// NewServiceAccountAttachResource is a helper function to simplify the provider implementation.
|
||||
func NewServiceAccountAttachResource() resource.Resource {
|
||||
return &networkInterfaceAttachResource{}
|
||||
}
|
||||
|
||||
// networkInterfaceAttachResource is the resource implementation.
|
||||
type networkInterfaceAttachResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *networkInterfaceAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_server_service_account_attach"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *networkInterfaceAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_service_account_attach", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *networkInterfaceAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Service account attachment resource schema. Attaches a service account to a server. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`service_account_email`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the service account attachment is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"service_account_email": schema.StringAttribute{
|
||||
Description: "The service account email.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
serviceAccountEmail := model.ServiceAccountEmail.ValueString()
|
||||
ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail)
|
||||
|
||||
// Create new service account attachment
|
||||
_, err := r.client.AddServiceAccountToServer(ctx, projectId, serverId, serviceAccountEmail).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching service account to server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
projectId,
|
||||
serverId,
|
||||
serviceAccountEmail,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
// Set state to fully populated data
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Service account attachment created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
serviceAccountEmail := model.ServiceAccountEmail.ValueString()
|
||||
ctx = tflog.SetField(ctx, "service_account_email", serviceAccountEmail)
|
||||
|
||||
serviceAccounts, err := r.client.ListServerServiceAccounts(ctx, projectId, serverId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account attachment", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
if serviceAccounts == nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading service account attachment", "List of service accounts attached to the server is nil")
|
||||
return
|
||||
}
|
||||
|
||||
if serviceAccounts.Items != nil {
|
||||
for _, mail := range *serviceAccounts.Items {
|
||||
if mail != serviceAccountEmail {
|
||||
continue
|
||||
}
|
||||
// Set refreshed state
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Service account attachment read")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// no matching service account was found, the attachment no longer exists
|
||||
resp.State.RemoveResource(ctx)
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *networkInterfaceAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Update is not supported, all fields require replace
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *networkInterfaceAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
service_accountId := model.ServiceAccountEmail.ValueString()
|
||||
ctx = tflog.SetField(ctx, "service_account_email", service_accountId)
|
||||
|
||||
// Remove service_account from server
|
||||
_, err := r.client.RemoveServiceAccountFromServer(ctx, projectId, serverId, service_accountId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing service account from server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "Service account attachment deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,server_id
|
||||
func (r *networkInterfaceAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing service_account attachment",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[service_account_email] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
serverId := idParts[1]
|
||||
service_accountId := idParts[2]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
ctx = tflog.SetField(ctx, "service_account_email", service_accountId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("service_account_email"), service_accountId)...)
|
||||
tflog.Info(ctx, "Service account attachment state imported")
|
||||
}
|
||||
202
stackit/internal/services/iaas/volume/datasource.go
Normal file
202
stackit/internal/services/iaas/volume/datasource.go
Normal file
|
|
@ -0,0 +1,202 @@
|
|||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// volumeDataSourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var volumeDataSourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ datasource.DataSource = &volumeDataSource{}
|
||||
)
|
||||
|
||||
// NewVolumeDataSource is a helper function to simplify the provider implementation.
|
||||
func NewVolumeDataSource() datasource.DataSource {
|
||||
return &volumeDataSource{}
|
||||
}
|
||||
|
||||
// volumeDataSource is the data source implementation.
|
||||
type volumeDataSource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the data source type name.
|
||||
func (d *volumeDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_volume"
|
||||
}
|
||||
|
||||
func (d *volumeDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !volumeDataSourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_volume", "data source")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
volumeDataSourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Configuring client: %v. This is an error related to the provider configuration, not to the data source configuration", err))
|
||||
return
|
||||
}
|
||||
|
||||
d.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *volumeDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Volume resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Volume resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".",
|
||||
Computed: true,
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the volume is associated.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"volume_id": schema.StringAttribute{
|
||||
Description: "The volume ID.",
|
||||
Required: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID of the server to which the volume is attached to.",
|
||||
Computed: true,
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the volume.",
|
||||
Computed: true,
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the volume.",
|
||||
Computed: true,
|
||||
},
|
||||
"availability_zone": schema.StringAttribute{
|
||||
Description: "The availability zone of the volume.",
|
||||
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,
|
||||
},
|
||||
"performance_class": schema.StringAttribute{
|
||||
Description: "The performance class of the volume.",
|
||||
Computed: true,
|
||||
},
|
||||
"size": schema.Int64Attribute{
|
||||
Description: "The size of the volume in GB. It can only be updated to a larger value than the current size",
|
||||
Computed: true,
|
||||
},
|
||||
"source": schema.SingleNestedAttribute{
|
||||
Description: "The source of the volume. It can be either a volume, an image, a snapshot or a backup",
|
||||
Computed: true,
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"type": schema.StringAttribute{
|
||||
Description: "The type of the source. " + utils.SupportedValuesDocumentation(SupportedSourceTypes),
|
||||
Computed: true,
|
||||
},
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The ID of the source, e.g. image ID",
|
||||
Computed: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (d *volumeDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
var model Model
|
||||
diags := req.Config.Get(ctx, &model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
volumeResp, err := d.client.GetVolume(ctx, projectId, volumeId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
err = mapFields(ctx, volumeResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", 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, "volume read")
|
||||
}
|
||||
608
stackit/internal/services/iaas/volume/resource.go
Normal file
608
stackit/internal/services/iaas/volume/resource.go
Normal file
|
|
@ -0,0 +1,608 @@
|
|||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/resourcevalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator"
|
||||
"github.com/hashicorp/terraform-plugin-framework/attr"
|
||||
"github.com/hashicorp/terraform-plugin-framework/diag"
|
||||
"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/objectplanmodifier"
|
||||
"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"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
|
||||
"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"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/utils"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &volumeResource{}
|
||||
_ resource.ResourceWithConfigure = &volumeResource{}
|
||||
_ resource.ResourceWithImportState = &volumeResource{}
|
||||
|
||||
SupportedSourceTypes = []string{"volume", "image", "snapshot", "backup"}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
VolumeId types.String `tfsdk:"volume_id"`
|
||||
Name types.String `tfsdk:"name"`
|
||||
AvailabilityZone types.String `tfsdk:"availability_zone"`
|
||||
Labels types.Map `tfsdk:"labels"`
|
||||
Description types.String `tfsdk:"description"`
|
||||
PerformanceClass types.String `tfsdk:"performance_class"`
|
||||
Size types.Int64 `tfsdk:"size"`
|
||||
ServerId types.String `tfsdk:"server_id"`
|
||||
Source types.Object `tfsdk:"source"`
|
||||
}
|
||||
|
||||
// Struct corresponding to Model.Source
|
||||
type sourceModel struct {
|
||||
Type types.String `tfsdk:"type"`
|
||||
Id types.String `tfsdk:"id"`
|
||||
}
|
||||
|
||||
// Types corresponding to sourceModel
|
||||
var sourceTypes = map[string]attr.Type{
|
||||
"type": basetypes.StringType{},
|
||||
"id": basetypes.StringType{},
|
||||
}
|
||||
|
||||
// NewVolumeResource is a helper function to simplify the provider implementation.
|
||||
func NewVolumeResource() resource.Resource {
|
||||
return &volumeResource{}
|
||||
}
|
||||
|
||||
// volumeResource is the resource implementation.
|
||||
type volumeResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *volumeResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_volume"
|
||||
}
|
||||
|
||||
// ConfigValidators validates the resource configuration
|
||||
func (r *volumeResource) ConfigValidators(_ context.Context) []resource.ConfigValidator {
|
||||
return []resource.ConfigValidator{
|
||||
resourcevalidator.AtLeastOneOf(
|
||||
path.MatchRoot("source"),
|
||||
path.MatchRoot("size"),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *volumeResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_volume", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *volumeResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Volume resource schema. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Volume resource schema. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`volume_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the volume is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"volume_id": schema.StringAttribute{
|
||||
Description: "The volume ID.",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID of the server to which the volume is attached to.",
|
||||
Computed: true,
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"name": schema.StringAttribute{
|
||||
Description: "The name of the volume.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"description": schema.StringAttribute{
|
||||
Description: "The description of the volume.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(127),
|
||||
},
|
||||
},
|
||||
"availability_zone": schema.StringAttribute{
|
||||
Description: "The availability zone of the volume.",
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Required: true,
|
||||
},
|
||||
"labels": schema.MapAttribute{
|
||||
Description: "Labels are key-value string pairs which can be attached to a resource container",
|
||||
ElementType: types.StringType,
|
||||
Optional: true,
|
||||
},
|
||||
"performance_class": schema.StringAttribute{
|
||||
Description: "The performance class of the volume.",
|
||||
Optional: true,
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
stringvalidator.LengthAtLeast(1),
|
||||
stringvalidator.LengthAtMost(63),
|
||||
stringvalidator.RegexMatches(
|
||||
regexp.MustCompile(`^[A-Za-z0-9]+((-|_|\s|\.)[A-Za-z0-9]+)*$`),
|
||||
"must match expression"),
|
||||
},
|
||||
},
|
||||
"size": schema.Int64Attribute{
|
||||
Description: "The size of the volume in GB. It can only be updated to a larger value than the current size. Either `size` or `source` must be provided",
|
||||
Optional: true,
|
||||
},
|
||||
"source": schema.SingleNestedAttribute{
|
||||
Description: "The source of the volume. It can be either a volume, an image, a snapshot or a backup. Either `size` or `source` must be provided",
|
||||
Optional: true,
|
||||
PlanModifiers: []planmodifier.Object{
|
||||
objectplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"type": schema.StringAttribute{
|
||||
Description: "The type of the source. " + utils.SupportedValuesDocumentation(SupportedSourceTypes),
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
"id": schema.StringAttribute{
|
||||
Description: "The ID of the source, e.g. image ID",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *volumeResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
|
||||
var source = &sourceModel{}
|
||||
if !(model.Source.IsNull() || model.Source.IsUnknown()) {
|
||||
diags = model.Source.As(ctx, source, basetypes.ObjectAsOptions{})
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Generate API request body from model
|
||||
payload, err := toCreatePayload(ctx, &model, source)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Create new volume
|
||||
|
||||
volume, err := r.client.CreateVolume(ctx, projectId).CreateVolumePayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
volumeId := *volume.Id
|
||||
volume, err = wait.CreateVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", fmt.Sprintf("volume creation waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, volume, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error creating volume", 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, "Volume created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *volumeResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
volumeResp, err := r.client.GetVolume(ctx, projectId, volumeId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Map response body to schema
|
||||
err = mapFields(ctx, volumeResp, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume", 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, "volume read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *volumeResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
// 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 volume", fmt.Sprintf("Creating API payload: %v", err))
|
||||
return
|
||||
}
|
||||
// Update existing volume
|
||||
updatedVolume, err := r.client.UpdateVolume(ctx, projectId, volumeId).UpdateVolumePayload(*payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Resize existing volume
|
||||
modelSize := conversion.Int64ValueToPointer(model.Size)
|
||||
if modelSize != nil && updatedVolume.Size != nil {
|
||||
// A volume can only be resized to larger values, otherwise an error occurs
|
||||
if *modelSize < *updatedVolume.Size {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("The new volume size must be larger than the current size (%d GB)", *updatedVolume.Size))
|
||||
} else if *modelSize > *updatedVolume.Size {
|
||||
payload := iaas.ResizeVolumePayload{
|
||||
Size: modelSize,
|
||||
}
|
||||
err := r.client.ResizeVolume(ctx, projectId, volumeId).ResizeVolumePayload(payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", fmt.Sprintf("Resizing the volume, calling API: %v", err))
|
||||
}
|
||||
// Update volume model because the API doesn't return a volume object as response
|
||||
updatedVolume.Size = modelSize
|
||||
}
|
||||
}
|
||||
err = mapFields(ctx, updatedVolume, &model)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error updating volume", 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, "volume updated")
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *volumeResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
// Delete existing volume
|
||||
err := r.client.DeleteVolume(ctx, projectId, volumeId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
_, err = wait.DeleteVolumeWaitHandler(ctx, r.client, projectId, volumeId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error deleting volume", fmt.Sprintf("volume deletion waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "volume deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,volume_id
|
||||
func (r *volumeResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 2 || idParts[0] == "" || idParts[1] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing volume",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[volume_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
volumeId := idParts[1]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...)
|
||||
tflog.Info(ctx, "volume state imported")
|
||||
}
|
||||
|
||||
func mapFields(ctx context.Context, volumeResp *iaas.Volume, model *Model) error {
|
||||
if volumeResp == nil {
|
||||
return fmt.Errorf("response input is nil")
|
||||
}
|
||||
if model == nil {
|
||||
return fmt.Errorf("model input is nil")
|
||||
}
|
||||
|
||||
var volumeId string
|
||||
if model.VolumeId.ValueString() != "" {
|
||||
volumeId = model.VolumeId.ValueString()
|
||||
} else if volumeResp.Id != nil {
|
||||
volumeId = *volumeResp.Id
|
||||
} else {
|
||||
return fmt.Errorf("Volume id not present")
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
model.ProjectId.ValueString(),
|
||||
volumeId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
labels, diags := types.MapValueFrom(ctx, types.StringType, map[string]interface{}{})
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
if volumeResp.Labels != nil && len(*volumeResp.Labels) != 0 {
|
||||
var diags diag.Diagnostics
|
||||
labels, diags = types.MapValueFrom(ctx, types.StringType, *volumeResp.Labels)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("converting labels to StringValue map: %w", core.DiagsToError(diags))
|
||||
}
|
||||
} else if model.Labels.IsNull() {
|
||||
labels = types.MapNull(types.StringType)
|
||||
}
|
||||
|
||||
var sourceValues map[string]attr.Value
|
||||
var sourceObject basetypes.ObjectValue
|
||||
if volumeResp.Source == nil {
|
||||
sourceObject = types.ObjectNull(sourceTypes)
|
||||
} else {
|
||||
sourceValues = map[string]attr.Value{
|
||||
"type": types.StringPointerValue(volumeResp.Source.Type),
|
||||
"id": types.StringPointerValue(volumeResp.Source.Id),
|
||||
}
|
||||
sourceObject, diags = types.ObjectValue(sourceTypes, sourceValues)
|
||||
if diags.HasError() {
|
||||
return fmt.Errorf("creating source: %w", core.DiagsToError(diags))
|
||||
}
|
||||
}
|
||||
|
||||
model.VolumeId = types.StringValue(volumeId)
|
||||
model.AvailabilityZone = types.StringPointerValue(volumeResp.AvailabilityZone)
|
||||
model.Description = types.StringPointerValue(volumeResp.Description)
|
||||
model.Name = types.StringPointerValue(volumeResp.Name)
|
||||
model.Labels = labels
|
||||
model.PerformanceClass = types.StringPointerValue(volumeResp.PerformanceClass)
|
||||
model.ServerId = types.StringPointerValue(volumeResp.ServerId)
|
||||
model.Size = types.Int64PointerValue(volumeResp.Size)
|
||||
model.Source = sourceObject
|
||||
return nil
|
||||
}
|
||||
|
||||
func toCreatePayload(ctx context.Context, model *Model, source *sourceModel) (*iaas.CreateVolumePayload, 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)
|
||||
}
|
||||
|
||||
var sourcePayload *iaas.VolumeSource
|
||||
|
||||
if !source.Id.IsNull() && !source.Type.IsNull() {
|
||||
sourcePayload = &iaas.VolumeSource{
|
||||
Id: conversion.StringValueToPointer(source.Id),
|
||||
Type: conversion.StringValueToPointer(source.Type),
|
||||
}
|
||||
}
|
||||
|
||||
return &iaas.CreateVolumePayload{
|
||||
AvailabilityZone: conversion.StringValueToPointer(model.AvailabilityZone),
|
||||
Description: conversion.StringValueToPointer(model.Description),
|
||||
Labels: &labels,
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
PerformanceClass: conversion.StringValueToPointer(model.PerformanceClass),
|
||||
Size: conversion.Int64ValueToPointer(model.Size),
|
||||
Source: sourcePayload,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func toUpdatePayload(ctx context.Context, model *Model, currentLabels types.Map) (*iaas.UpdateVolumePayload, 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 &iaas.UpdateVolumePayload{
|
||||
Description: conversion.StringValueToPointer(model.Description),
|
||||
Name: conversion.StringValueToPointer(model.Name),
|
||||
Labels: &labels,
|
||||
}, nil
|
||||
}
|
||||
253
stackit/internal/services/iaas/volume/resource_test.go
Normal file
253
stackit/internal/services/iaas/volume/resource_test.go
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
package volume
|
||||
|
||||
import (
|
||||
"context"
|
||||
"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/iaas"
|
||||
)
|
||||
|
||||
func TestMapFields(t *testing.T) {
|
||||
tests := []struct {
|
||||
description string
|
||||
state Model
|
||||
input *iaas.Volume
|
||||
expected Model
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
},
|
||||
&iaas.Volume{
|
||||
Id: utils.Ptr("nid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
Name: types.StringNull(),
|
||||
AvailabilityZone: types.StringNull(),
|
||||
Labels: types.MapNull(types.StringType),
|
||||
Description: types.StringNull(),
|
||||
PerformanceClass: types.StringNull(),
|
||||
ServerId: types.StringNull(),
|
||||
Size: types.Int64Null(),
|
||||
Source: types.ObjectNull(sourceTypes),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"simple_values",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
},
|
||||
&iaas.Volume{
|
||||
Id: utils.Ptr("nid"),
|
||||
Name: utils.Ptr("name"),
|
||||
AvailabilityZone: utils.Ptr("zone"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
PerformanceClass: utils.Ptr("class"),
|
||||
ServerId: utils.Ptr("sid"),
|
||||
Size: utils.Ptr(int64(1)),
|
||||
Source: &iaas.VolumeSource{},
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
Name: types.StringValue("name"),
|
||||
AvailabilityZone: types.StringValue("zone"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
PerformanceClass: types.StringValue("class"),
|
||||
ServerId: types.StringValue("sid"),
|
||||
Size: types.Int64Value(1),
|
||||
Source: types.ObjectValueMust(sourceTypes, map[string]attr.Value{
|
||||
"type": types.StringNull(),
|
||||
"id": types.StringNull(),
|
||||
}),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"empty_labels",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
},
|
||||
&iaas.Volume{
|
||||
Id: utils.Ptr("nid"),
|
||||
},
|
||||
Model{
|
||||
Id: types.StringValue("pid,nid"),
|
||||
ProjectId: types.StringValue("pid"),
|
||||
VolumeId: types.StringValue("nid"),
|
||||
Name: types.StringNull(),
|
||||
AvailabilityZone: types.StringNull(),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{}),
|
||||
Description: types.StringNull(),
|
||||
PerformanceClass: types.StringNull(),
|
||||
ServerId: types.StringNull(),
|
||||
Size: types.Int64Null(),
|
||||
Source: types.ObjectNull(sourceTypes),
|
||||
},
|
||||
true,
|
||||
},
|
||||
{
|
||||
"response_nil_fail",
|
||||
Model{},
|
||||
nil,
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
{
|
||||
"no_resource_id",
|
||||
Model{
|
||||
ProjectId: types.StringValue("pid"),
|
||||
},
|
||||
&iaas.Volume{},
|
||||
Model{},
|
||||
false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
err := mapFields(context.Background(), tt.input, &tt.state)
|
||||
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
|
||||
source *sourceModel
|
||||
expected *iaas.CreateVolumePayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
AvailabilityZone: types.StringValue("zone"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
PerformanceClass: types.StringValue("class"),
|
||||
Size: types.Int64Value(1),
|
||||
Source: types.ObjectValueMust(sourceTypes, map[string]attr.Value{
|
||||
"type": types.StringNull(),
|
||||
"id": types.StringNull(),
|
||||
}),
|
||||
},
|
||||
&sourceModel{
|
||||
Type: types.StringValue("volume"),
|
||||
Id: types.StringValue("id"),
|
||||
},
|
||||
&iaas.CreateVolumePayload{
|
||||
Name: utils.Ptr("name"),
|
||||
AvailabilityZone: utils.Ptr("zone"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
PerformanceClass: utils.Ptr("class"),
|
||||
Size: utils.Ptr(int64(1)),
|
||||
Source: &iaas.VolumeSource{
|
||||
Type: utils.Ptr("volume"),
|
||||
Id: utils.Ptr("id"),
|
||||
},
|
||||
},
|
||||
true,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.description, func(t *testing.T) {
|
||||
output, err := toCreatePayload(context.Background(), tt.input, tt.source)
|
||||
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 *iaas.UpdateVolumePayload
|
||||
isValid bool
|
||||
}{
|
||||
{
|
||||
"default_ok",
|
||||
&Model{
|
||||
Name: types.StringValue("name"),
|
||||
Labels: types.MapValueMust(types.StringType, map[string]attr.Value{
|
||||
"key": types.StringValue("value"),
|
||||
}),
|
||||
Description: types.StringValue("desc"),
|
||||
},
|
||||
&iaas.UpdateVolumePayload{
|
||||
Name: utils.Ptr("name"),
|
||||
Labels: &map[string]interface{}{
|
||||
"key": "value",
|
||||
},
|
||||
Description: utils.Ptr("desc"),
|
||||
},
|
||||
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)
|
||||
if diff != "" {
|
||||
t.Fatalf("Data does not match: %s", diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
305
stackit/internal/services/iaas/volumeattach/resource.go
Normal file
305
stackit/internal/services/iaas/volumeattach/resource.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
package volumeattach
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"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/core/config"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/oapierror"
|
||||
"github.com/stackitcloud/stackit-sdk-go/core/utils"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas"
|
||||
"github.com/stackitcloud/stackit-sdk-go/services/iaas/wait"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/core"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/features"
|
||||
"github.com/stackitcloud/terraform-provider-stackit/stackit/internal/validate"
|
||||
)
|
||||
|
||||
// resourceBetaCheckDone is used to prevent multiple checks for beta resources.
|
||||
// This is a workaround for the lack of a global state in the provider and
|
||||
// needs to exist because the Configure method is called twice.
|
||||
var resourceBetaCheckDone bool
|
||||
|
||||
// Ensure the implementation satisfies the expected interfaces.
|
||||
var (
|
||||
_ resource.Resource = &volumeAttachResource{}
|
||||
_ resource.ResourceWithConfigure = &volumeAttachResource{}
|
||||
_ resource.ResourceWithImportState = &volumeAttachResource{}
|
||||
)
|
||||
|
||||
type Model struct {
|
||||
Id types.String `tfsdk:"id"` // needed by TF
|
||||
ProjectId types.String `tfsdk:"project_id"`
|
||||
ServerId types.String `tfsdk:"server_id"`
|
||||
VolumeId types.String `tfsdk:"volume_id"`
|
||||
}
|
||||
|
||||
// NewVolumeAttachResource is a helper function to simplify the provider implementation.
|
||||
func NewVolumeAttachResource() resource.Resource {
|
||||
return &volumeAttachResource{}
|
||||
}
|
||||
|
||||
// volumeAttachResource is the resource implementation.
|
||||
type volumeAttachResource struct {
|
||||
client *iaas.APIClient
|
||||
}
|
||||
|
||||
// Metadata returns the resource type name.
|
||||
func (r *volumeAttachResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) {
|
||||
resp.TypeName = req.ProviderTypeName + "_server_volume_attach"
|
||||
}
|
||||
|
||||
// Configure adds the provider configured client to the resource.
|
||||
func (r *volumeAttachResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) {
|
||||
// Prevent panic if the provider has not been configured.
|
||||
if req.ProviderData == nil {
|
||||
return
|
||||
}
|
||||
|
||||
providerData, ok := req.ProviderData.(core.ProviderData)
|
||||
if !ok {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error configuring API client", fmt.Sprintf("Expected configure type stackit.ProviderData, got %T", req.ProviderData))
|
||||
return
|
||||
}
|
||||
|
||||
if !resourceBetaCheckDone {
|
||||
features.CheckBetaResourcesEnabled(ctx, &providerData, &resp.Diagnostics, "stackit_server_volume_attach", "resource")
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
resourceBetaCheckDone = true
|
||||
}
|
||||
|
||||
var apiClient *iaas.APIClient
|
||||
var err error
|
||||
if providerData.IaaSCustomEndpoint != "" {
|
||||
ctx = tflog.SetField(ctx, "iaas_custom_endpoint", providerData.IaaSCustomEndpoint)
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithEndpoint(providerData.IaaSCustomEndpoint),
|
||||
)
|
||||
} else {
|
||||
apiClient, err = iaas.NewAPIClient(
|
||||
config.WithCustomAuth(providerData.RoundTripper),
|
||||
config.WithRegion(providerData.Region),
|
||||
)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "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
|
||||
}
|
||||
|
||||
r.client = apiClient
|
||||
tflog.Info(ctx, "iaas client configured")
|
||||
}
|
||||
|
||||
// Schema defines the schema for the resource.
|
||||
func (r *volumeAttachResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
resp.Schema = schema.Schema{
|
||||
MarkdownDescription: features.AddBetaDescription("Volume attachment resource schema. Attaches a volume to a server. Must have a `region` specified in the provider configuration."),
|
||||
Description: "Volume attachment resource schema. Attaches a volume to a server. Must have a `region` specified in the provider configuration.",
|
||||
Attributes: map[string]schema.Attribute{
|
||||
"id": schema.StringAttribute{
|
||||
Description: "Terraform's internal resource ID. It is structured as \"`project_id`,`server_id`,`volume_id`\".",
|
||||
Computed: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.UseStateForUnknown(),
|
||||
},
|
||||
},
|
||||
"project_id": schema.StringAttribute{
|
||||
Description: "STACKIT project ID to which the volume attachment is associated.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"server_id": schema.StringAttribute{
|
||||
Description: "The server ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
"volume_id": schema.StringAttribute{
|
||||
Description: "The volume ID.",
|
||||
Required: true,
|
||||
PlanModifiers: []planmodifier.String{
|
||||
stringplanmodifier.RequiresReplace(),
|
||||
},
|
||||
Validators: []validator.String{
|
||||
validate.UUID(),
|
||||
validate.NoSeparator(),
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Create creates the resource and sets the initial Terraform state.
|
||||
func (r *volumeAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
// Create new Volume attachment
|
||||
|
||||
payload := iaas.AddVolumeToServerPayload{
|
||||
DeleteOnTermination: utils.Ptr(false),
|
||||
}
|
||||
_, err := r.client.AddVolumeToServer(ctx, projectId, serverId, volumeId).AddVolumeToServerPayload(payload).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = wait.AddVolumeToServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error attaching volume to server", fmt.Sprintf("volume attachment waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
idParts := []string{
|
||||
projectId,
|
||||
serverId,
|
||||
volumeId,
|
||||
}
|
||||
model.Id = types.StringValue(
|
||||
strings.Join(idParts, core.Separator),
|
||||
)
|
||||
|
||||
// Set state to fully populated data
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Volume attachment created")
|
||||
}
|
||||
|
||||
// Read refreshes the Terraform state with the latest data.
|
||||
func (r *volumeAttachResource) 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
|
||||
}
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
_, err := r.client.GetAttachedVolume(ctx, projectId, serverId, volumeId).Execute()
|
||||
if err != nil {
|
||||
oapiErr, ok := err.(*oapierror.GenericOpenAPIError) //nolint:errorlint //complaining that error.As should be used to catch wrapped errors, but this error should not be wrapped
|
||||
if ok && oapiErr.StatusCode == http.StatusNotFound {
|
||||
resp.State.RemoveResource(ctx)
|
||||
return
|
||||
}
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error reading volume attachment", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Set refreshed state
|
||||
diags = resp.State.Set(ctx, model)
|
||||
resp.Diagnostics.Append(diags...)
|
||||
if resp.Diagnostics.HasError() {
|
||||
return
|
||||
}
|
||||
tflog.Info(ctx, "Volume attachment read")
|
||||
}
|
||||
|
||||
// Update updates the resource and sets the updated Terraform state on success.
|
||||
func (r *volumeAttachResource) Update(_ context.Context, _ resource.UpdateRequest, _ *resource.UpdateResponse) { // nolint:gocritic // function signature required by Terraform
|
||||
// Update is not supported, all fields require replace
|
||||
}
|
||||
|
||||
// Delete deletes the resource and removes the Terraform state on success.
|
||||
func (r *volumeAttachResource) 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
|
||||
}
|
||||
|
||||
projectId := model.ProjectId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
serverId := model.ServerId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
volumeId := model.VolumeId.ValueString()
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
// Remove volume from server
|
||||
err := r.client.RemoveVolumeFromServer(ctx, projectId, serverId, volumeId).Execute()
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("Calling API: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
_, err = wait.RemoveVolumeFromServerWaitHandler(ctx, r.client, projectId, serverId, volumeId).WaitWithContext(ctx)
|
||||
if err != nil {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics, "Error removing volume from server", fmt.Sprintf("volume removal waiting: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
tflog.Info(ctx, "Volume attachment deleted")
|
||||
}
|
||||
|
||||
// ImportState imports a resource into the Terraform state on success.
|
||||
// The expected format of the resource import identifier is: project_id,server_id
|
||||
func (r *volumeAttachResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) {
|
||||
idParts := strings.Split(req.ID, core.Separator)
|
||||
|
||||
if len(idParts) != 3 || idParts[0] == "" || idParts[1] == "" || idParts[2] == "" {
|
||||
core.LogAndAddError(ctx, &resp.Diagnostics,
|
||||
"Error importing volume attachment",
|
||||
fmt.Sprintf("Expected import identifier with format: [project_id],[server_id],[volume_id] Got: %q", req.ID),
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
projectId := idParts[0]
|
||||
serverId := idParts[1]
|
||||
volumeId := idParts[2]
|
||||
ctx = tflog.SetField(ctx, "project_id", projectId)
|
||||
ctx = tflog.SetField(ctx, "server_id", serverId)
|
||||
ctx = tflog.SetField(ctx, "volume_id", volumeId)
|
||||
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("project_id"), projectId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("server_id"), serverId)...)
|
||||
resp.Diagnostics.Append(resp.State.SetAttribute(ctx, path.Root("volume_id"), volumeId)...)
|
||||
tflog.Info(ctx, "Volume attachment state imported")
|
||||
}
|
||||
|
|
@ -100,7 +100,7 @@ func (r *userResource) Configure(ctx context.Context, req resource.ConfigureRequ
|
|||
// Schema defines the schema for the resource.
|
||||
func (r *userResource) Schema(_ context.Context, _ resource.SchemaRequest, resp *resource.SchemaResponse) {
|
||||
descriptions := map[string]string{
|
||||
"main": "[Warning: BETA] SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration.",
|
||||
"main": "SQLServer Flex user resource schema. Must have a `region` specified in the provider configuration.",
|
||||
"id": "Terraform's internal resource ID. It is structured as \"`project_id`,`instance_id`,`user_id`\".",
|
||||
"user_id": "User ID.",
|
||||
"instance_id": "ID of the SQLServer Flex instance.",
|
||||
|
|
|
|||
|
|
@ -34,6 +34,10 @@ var (
|
|||
ProjectId = os.Getenv("TF_ACC_PROJECT_ID")
|
||||
// ServerId is the id of a server used for some tests
|
||||
ServerId = getenv("TF_ACC_SERVER_ID", "")
|
||||
// IaaSImageId is the id of an image used for IaaS acceptance tests. Once the stackit_image resource is implemented, we can remove this
|
||||
IaaSImageId = getenv("TF_ACC_IMAGE_ID", "")
|
||||
// IaaSNetworkInterfaceId is the id of a network interface used for IaaS acceptance tests. Once acceptance tests are merged, we can remove this
|
||||
IaaSNetworkInterfaceId = getenv("TF_ACC_NETWORK_INTERFACE_ID", "")
|
||||
// TestProjectParentContainerID is the container id of the parent resource under which projects are created as part of the resource-manager acceptance tests
|
||||
TestProjectParentContainerID = os.Getenv("TF_ACC_TEST_PROJECT_PARENT_CONTAINER_ID")
|
||||
// TestProjectParentContainerID is the uuid of the parent resource under which projects are created as part of the resource-manager acceptance tests
|
||||
|
|
@ -120,6 +124,7 @@ func IaaSProviderConfig() string {
|
|||
return `
|
||||
provider "stackit" {
|
||||
region = "eu01"
|
||||
enable_beta_resources = true
|
||||
}`
|
||||
}
|
||||
return fmt.Sprintf(`
|
||||
|
|
|
|||
|
|
@ -17,6 +17,15 @@ import (
|
|||
iaasNetwork "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/network"
|
||||
iaasNetworkArea "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarea"
|
||||
iaasNetworkAreaRoute "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkarearoute"
|
||||
iaasNetworkInterface "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterface"
|
||||
iaasNetworkInterfaceAttach "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/networkinterfaceattach"
|
||||
iaasPublicIp "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/publicip"
|
||||
iaasSecurityGroup "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygroup"
|
||||
iaasSecurityGroupRule "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/securitygrouprule"
|
||||
iaasServer "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/iaas/server"
|
||||
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"
|
||||
loadBalancerCredential "github.com/stackitcloud/terraform-provider-stackit/stackit/internal/services/loadbalancer/credential"
|
||||
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"
|
||||
|
|
@ -400,6 +409,12 @@ func (p *Provider) DataSources(_ context.Context) []func() datasource.DataSource
|
|||
iaasNetwork.NewNetworkDataSource,
|
||||
iaasNetworkArea.NewNetworkAreaDataSource,
|
||||
iaasNetworkAreaRoute.NewNetworkAreaRouteDataSource,
|
||||
iaasNetworkInterface.NewNetworkInterfaceDataSource,
|
||||
iaasVolume.NewVolumeDataSource,
|
||||
iaasPublicIp.NewPublicIpDataSource,
|
||||
iaasServer.NewServerDataSource,
|
||||
iaasSecurityGroup.NewSecurityGroupDataSource,
|
||||
iaasSecurityGroupRule.NewSecurityGroupRuleDataSource,
|
||||
loadBalancer.NewLoadBalancerDataSource,
|
||||
logMeInstance.NewInstanceDataSource,
|
||||
logMeCredential.NewCredentialDataSource,
|
||||
|
|
@ -444,6 +459,15 @@ func (p *Provider) Resources(_ context.Context) []func() resource.Resource {
|
|||
iaasNetwork.NewNetworkResource,
|
||||
iaasNetworkArea.NewNetworkAreaResource,
|
||||
iaasNetworkAreaRoute.NewNetworkAreaRouteResource,
|
||||
iaasNetworkInterface.NewNetworkInterfaceResource,
|
||||
iaasVolume.NewVolumeResource,
|
||||
iaasPublicIp.NewPublicIpResource,
|
||||
iaasVolumeAttach.NewVolumeAttachResource,
|
||||
iaasNetworkInterfaceAttach.NewNetworkInterfaceAttachResource,
|
||||
iaasServiceAccountAttach.NewServiceAccountAttachResource,
|
||||
iaasServer.NewServerResource,
|
||||
iaasSecurityGroup.NewSecurityGroupResource,
|
||||
iaasSecurityGroupRule.NewSecurityGroupRuleResource,
|
||||
loadBalancer.NewLoadBalancerResource,
|
||||
loadBalancerCredential.NewCredentialResource,
|
||||
loadBalancerObservabilityCredential.NewObservabilityCredentialResource,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue