diff --git a/docs/resources/instance.md b/docs/resources/instance.md
index 67cb5eb..17894f9 100644
--- a/docs/resources/instance.md
+++ b/docs/resources/instance.md
@@ -3,12 +3,12 @@
page_title: "gcore_instance Resource - terraform-provider-gcore"
subcategory: ""
description: |-
- Represent instance
+ Represent instance. WARNING: This resource is deprecated, please use 'gcore_instancev2' instead
---
# gcore_instance (Resource)
-Represent instance
+Represent instance. **WARNING: This resource is deprecated, please use 'gcore_instancev2' instead**
## Example Usage
diff --git a/docs/resources/instancev2.md b/docs/resources/instancev2.md
index 06aa9a3..5101bee 100644
--- a/docs/resources/instancev2.md
+++ b/docs/resources/instancev2.md
@@ -68,6 +68,12 @@ resource "gcore_keypair" "my_keypair" {
sshkey_name = "my-keypair"
public_key = "ssh-ed25519 ...your public key... gcore@gcore.com"
}
+
+data "gcore_securitygroup" "default" {
+ name = "default"
+ project_id = data.gcore_project.project.id
+ region_id = data.gcore_region.region.id
+}
```
### Basic example
@@ -75,7 +81,7 @@ resource "gcore_keypair" "my_keypair" {
#### Creating instance with one public interface
```terraform
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-one-interface" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -88,6 +94,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
@@ -100,7 +107,7 @@ resource "gcore_instancev2" "instance" {
This example demonstrates how to create an instance with two network interfaces: one public and one private.
```terraform
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-two-interface" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -113,11 +120,13 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
interface {
type = "subnet"
name = "my-private-interface"
+ security_groups = [gcore_securitygroup.default.id]
network_id = gcore_network.network.id
subnet_id = gcore_subnet.subnet.id
@@ -145,7 +154,7 @@ resource "gcore_volume" "boot_volume_windows" {
region_id = data.gcore_region.region.id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-instance" {
flavor_id = "g1w-standard-4-8"
name = "my-windows-instance"
password = "my-s3cR3tP@ssw0rd"
@@ -158,6 +167,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
@@ -167,6 +177,39 @@ resource "gcore_instancev2" "instance" {
### Advanced examples
+
+#### Creating instance with a dual-stack public interface
+
+This example demonstrates how to create an instance with a dual-stack public interface.
+The instance has both an IPv4 and an IPv6 address.
+
+```terraform
+resource "gcore_instancev2" "instance-with-dualstack" {
+ flavor_id = "g1-standard-2-4"
+ name = "my-instance"
+ keypair_name = "my-keypair"
+
+ volume {
+ volume_id = gcore_volume.boot_volume.id
+ boot_index = 0
+ }
+
+ interface {
+ type = "external"
+ ip_family = "dual"
+ name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
+ }
+
+ project_id = data.gcore_project.project.id
+ region_id = data.gcore_region.region.id
+}
+
+output "addresses" {
+ value = gcore_instancev2.instance.addresses
+}
+```
+
#### Creating instance with floating ip
```terraform
@@ -185,7 +228,7 @@ resource "gcore_floatingip" "floating_ip" {
port_id = gcore_reservedfixedip.fixed_ip.port_id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-fip" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -201,6 +244,7 @@ resource "gcore_instancev2" "instance" {
port_id = gcore_reservedfixedip.fixed_ip.port_id
existing_fip_id = gcore_floatingip.floating_ip.id
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
@@ -217,7 +261,7 @@ resource "gcore_reservedfixedip" "fixed_ip" {
type = "external"
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-reserved-address" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -231,6 +275,7 @@ resource "gcore_instancev2" "instance" {
type = "reserved_fixed_ip"
name = "my-reserved-public-interface"
port_id = gcore_reservedfixedip.fixed_ip.port_id
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
@@ -293,7 +338,7 @@ resource "gcore_securitygroup" "web_server_security_group" {
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-custom-sg" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -354,7 +399,7 @@ resource "gcore_volume" "boot_volume_windows" {
region_id = data.gcore_region.region.id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-windows-with-userdata" {
flavor_id = "g1w-standard-4-8"
name = "my-windows-instance"
password = "my-s3cR3tP@ssw0rd"
@@ -368,6 +413,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
@@ -385,6 +431,9 @@ resource "gcore_instancev2" "instance" {
- `interface` (Block Set, Min: 1) List of interfaces for the instance. You can detach the interface from the instance by removing the
interface from the instance resource and attach the interface by adding the interface resource
inside an instance resource. (see [below for nested schema](#nestedblock--interface))
+- `volume` (Block Set, Min: 1) List of volumes for the instance. You can detach the volume from the instance by removing the
+volume from the instance resource. You cannot detach the boot volume. You can attach a data volume
+by adding the volume resource inside an instance resource. (see [below for nested schema](#nestedblock--volume))
### Optional
@@ -392,7 +441,6 @@ inside an instance resource. (see [below for nested schema](#nestedblock--interf
from the marketplace application template
- `configuration` (Block List) Parameters for the application template from the marketplace (see [below for nested schema](#nestedblock--configuration))
- `keypair_name` (String) Name of the keypair to use for the instance
-- `last_updated` (String)
- `metadata_map` (Map of String) Create one or more metadata items for the instance
- `name` (String) Name of the instance.
- `name_template` (String) Instance name template. You can use forms 'ip_octets', 'two_ip_octets', 'one_ip_octet'
@@ -401,26 +449,23 @@ When only 'password' is provided, it is set as the password for the default user
when 'password' is specified. For Windows instances, 'username' cannot be specified. Use the 'password' field to set
the password for the 'Admin' user on Windows. Use the 'user_data' field to provide a script to create new users
on Windows. The password of the Admin user cannot be updated via 'user_data'
-- `project_id` (Number)
-- `project_name` (String)
-- `region_id` (Number)
-- `region_name` (String)
+- `project_id` (Number) Project ID, only one of project_id or project_name should be set
+- `project_name` (String) Project name, only one of project_id or project_name should be set
+- `region_id` (Number) Region ID, only one of region_id or region_name should be set
+- `region_name` (String) Region name, only one of region_id or region_name should be set
- `server_group` (String) ID of the server group to use for the instance
- `user_data` (String) String in base64 format. For Linux instances, 'user_data' is ignored when 'password' field is provided.
For Windows instances, Admin user password is set by 'password' field and cannot be updated via 'user_data'
- `username` (String) For Linux instances, 'username' and 'password' are used to create a new user. For Windows
instances, 'username' cannot be specified. Use 'password' field to set the password for the 'Admin' user on Windows.
- `vm_state` (String) Current vm state, use stopped to stop vm and active to start
-- `volume` (Block Set) List of volumes for the instance. You can detach the volume from the instance by removing the
-volume from the instance resource. You cannot detach the boot volume. You can attach a data volume
-by adding the volume resource inside an instance resource. (see [below for nested schema](#nestedblock--volume))
### Read-Only
- `addresses` (List of Object) List of instance addresses (see [below for nested schema](#nestedatt--addresses))
- `flavor` (Map of String) Flavor details, RAM, vCPU, etc.
- `id` (String) The ID of this resource.
-- `security_group` (List of Object) Firewalls list, they will be attached globally on all instance's interfaces (see [below for nested schema](#nestedatt--security_group))
+- `last_updated` (String)
- `status` (String) Status of the instance
@@ -429,43 +474,49 @@ by adding the volume resource inside an instance resource. (see [below for neste
Required:
- `name` (String) Name of interface, should be unique for the instance
+- `security_groups` (List of String) list of security group IDs, they will be attached to exact interface
Optional:
-- `existing_fip_id` (String)
-- `ip_address` (String)
+- `existing_fip_id` (String) The id of the existing floating IP that will be attached to the interface
+- `ip_address` (String) IP address for the interface.
- `ip_family` (String) IP family for the interface, available values are 'dual', 'ipv4' and 'ipv6'
- `network_id` (String) required if type is 'subnet' or 'any_subnet'
- `order` (Number) Order of attaching interface
- `port_id` (String) required if type is 'reserved_fixed_ip'
-- `security_groups` (List of String) list of security group IDs, they will be attached to exact interface
- `subnet_id` (String) required if type is 'subnet'
- `type` (String) Available value is 'subnet', 'any_subnet', 'external', 'reserved_fixed_ip'
-
-### Nested Schema for `configuration`
+
+### Nested Schema for `volume`
Required:
-- `key` (String)
-- `value` (String)
-
-
-
-### Nested Schema for `volume`
+- `volume_id` (String)
Optional:
-- `attachment_tag` (String)
- `boot_index` (Number) If boot_index==0 volumes can not detached
-- `delete_on_termination` (Boolean)
+
+Read-Only:
+
+- `attachment_tag` (String) Tag for the volume attachment
+- `delete_on_termination` (Boolean) Delete volume on termination
- `id` (String)
-- `image_id` (String)
-- `name` (String)
-- `size` (Number)
-- `type_name` (String)
-- `volume_id` (String)
+- `image_id` (String) Image ID for the volume
+- `name` (String) Name of the volume
+- `size` (Number) Size of the volume in GiB
+- `type_name` (String) Volume type name
+
+
+
+### Nested Schema for `configuration`
+
+Required:
+
+- `key` (String)
+- `value` (String)
@@ -485,15 +536,6 @@ Read-Only:
-
-### Nested Schema for `security_group`
-
-Read-Only:
-
-- `id` (String)
-- `name` (String)
-
-
diff --git a/examples/resources/gcore_instancev2/custom-sg.tf b/examples/resources/gcore_instancev2/custom-sg.tf
index f524e8d..344fe44 100644
--- a/examples/resources/gcore_instancev2/custom-sg.tf
+++ b/examples/resources/gcore_instancev2/custom-sg.tf
@@ -45,7 +45,7 @@ resource "gcore_securitygroup" "web_server_security_group" {
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-custom-sg" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
diff --git a/examples/resources/gcore_instancev2/dualstack-interface.tf b/examples/resources/gcore_instancev2/dualstack-interface.tf
new file mode 100644
index 0000000..b481684
--- /dev/null
+++ b/examples/resources/gcore_instancev2/dualstack-interface.tf
@@ -0,0 +1,24 @@
+resource "gcore_instancev2" "instance-with-dualstack" {
+ flavor_id = "g1-standard-2-4"
+ name = "my-instance"
+ keypair_name = "my-keypair"
+
+ volume {
+ volume_id = gcore_volume.boot_volume.id
+ boot_index = 0
+ }
+
+ interface {
+ type = "external"
+ ip_family = "dual"
+ name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
+ }
+
+ project_id = data.gcore_project.project.id
+ region_id = data.gcore_region.region.id
+}
+
+output "addresses" {
+ value = gcore_instancev2.instance.addresses
+}
\ No newline at end of file
diff --git a/examples/resources/gcore_instancev2/fip.tf b/examples/resources/gcore_instancev2/fip.tf
index f96cf8c..c39d967 100644
--- a/examples/resources/gcore_instancev2/fip.tf
+++ b/examples/resources/gcore_instancev2/fip.tf
@@ -13,7 +13,7 @@ resource "gcore_floatingip" "floating_ip" {
port_id = gcore_reservedfixedip.fixed_ip.port_id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-fip" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -29,6 +29,7 @@ resource "gcore_instancev2" "instance" {
port_id = gcore_reservedfixedip.fixed_ip.port_id
existing_fip_id = gcore_floatingip.floating_ip.id
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
diff --git a/examples/resources/gcore_instancev2/main.tf b/examples/resources/gcore_instancev2/main.tf
index 50a034b..aafc42b 100644
--- a/examples/resources/gcore_instancev2/main.tf
+++ b/examples/resources/gcore_instancev2/main.tf
@@ -47,3 +47,8 @@ resource "gcore_keypair" "my_keypair" {
public_key = "ssh-ed25519 ...your public key... gcore@gcore.com"
}
+data "gcore_securitygroup" "default" {
+ name = "default"
+ project_id = data.gcore_project.project.id
+ region_id = data.gcore_region.region.id
+}
diff --git a/examples/resources/gcore_instancev2/one-interface-windows.tf b/examples/resources/gcore_instancev2/one-interface-windows.tf
index 30d415c..d5a787b 100644
--- a/examples/resources/gcore_instancev2/one-interface-windows.tf
+++ b/examples/resources/gcore_instancev2/one-interface-windows.tf
@@ -13,7 +13,7 @@ resource "gcore_volume" "boot_volume_windows" {
region_id = data.gcore_region.region.id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-instance" {
flavor_id = "g1w-standard-4-8"
name = "my-windows-instance"
password = "my-s3cR3tP@ssw0rd"
@@ -26,6 +26,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
diff --git a/examples/resources/gcore_instancev2/one-interface.tf b/examples/resources/gcore_instancev2/one-interface.tf
index 90ff85d..dbe4219 100644
--- a/examples/resources/gcore_instancev2/one-interface.tf
+++ b/examples/resources/gcore_instancev2/one-interface.tf
@@ -1,4 +1,4 @@
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-one-interface" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -11,6 +11,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
diff --git a/examples/resources/gcore_instancev2/reserved-address.tf b/examples/resources/gcore_instancev2/reserved-address.tf
index 8007ee1..bce34ee 100644
--- a/examples/resources/gcore_instancev2/reserved-address.tf
+++ b/examples/resources/gcore_instancev2/reserved-address.tf
@@ -4,7 +4,7 @@ resource "gcore_reservedfixedip" "fixed_ip" {
type = "external"
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-reserved-address" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -18,6 +18,7 @@ resource "gcore_instancev2" "instance" {
type = "reserved_fixed_ip"
name = "my-reserved-public-interface"
port_id = gcore_reservedfixedip.fixed_ip.port_id
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
diff --git a/examples/resources/gcore_instancev2/two-interface.tf b/examples/resources/gcore_instancev2/two-interface.tf
index 21c1b16..34db398 100644
--- a/examples/resources/gcore_instancev2/two-interface.tf
+++ b/examples/resources/gcore_instancev2/two-interface.tf
@@ -1,4 +1,4 @@
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-with-two-interface" {
flavor_id = "g1-standard-2-4"
name = "my-instance"
keypair_name = "my-keypair"
@@ -11,11 +11,13 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
interface {
type = "subnet"
name = "my-private-interface"
+ security_groups = [gcore_securitygroup.default.id]
network_id = gcore_network.network.id
subnet_id = gcore_subnet.subnet.id
diff --git a/examples/resources/gcore_instancev2/windows-with-userdata.tf b/examples/resources/gcore_instancev2/windows-with-userdata.tf
index 23e361f..abd3a67 100644
--- a/examples/resources/gcore_instancev2/windows-with-userdata.tf
+++ b/examples/resources/gcore_instancev2/windows-with-userdata.tf
@@ -29,7 +29,7 @@ resource "gcore_volume" "boot_volume_windows" {
region_id = data.gcore_region.region.id
}
-resource "gcore_instancev2" "instance" {
+resource "gcore_instancev2" "instance-windows-with-userdata" {
flavor_id = "g1w-standard-4-8"
name = "my-windows-instance"
password = "my-s3cR3tP@ssw0rd"
@@ -43,6 +43,7 @@ resource "gcore_instancev2" "instance" {
interface {
type = "external"
name = "my-external-interface"
+ security_groups = [gcore_securitygroup.default.id]
}
project_id = data.gcore_project.project.id
diff --git a/gcore/resource_gcore_instance.go b/gcore/resource_gcore_instance.go
index 6d266ca..88daac9 100644
--- a/gcore/resource_gcore_instance.go
+++ b/gcore/resource_gcore_instance.go
@@ -36,7 +36,7 @@ func resourceInstance() *schema.Resource {
ReadContext: resourceInstanceRead,
UpdateContext: resourceInstanceUpdate,
DeleteContext: resourceInstanceDelete,
- Description: "Represent instance",
+ Description: "Represent instance. **WARNING: This resource is deprecated, please use 'gcore_instancev2' instead**",
DeprecationMessage: "!> **WARNING:** This resource is deprecated, please use 'gcore_instancev2' instead",
Importer: &schema.ResourceImporter{
StateContext: func(ctx context.Context, d *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) {
diff --git a/gcore/resource_gcore_instancev2.go b/gcore/resource_gcore_instancev2.go
index d8e9b6d..f996a4f 100644
--- a/gcore/resource_gcore_instancev2.go
+++ b/gcore/resource_gcore_instancev2.go
@@ -7,6 +7,7 @@ import (
"fmt"
"io"
"log"
+ "slices"
"sort"
"strconv"
"time"
@@ -15,6 +16,7 @@ import (
"github.com/G-Core/gcorelabscloud-go/gcore/floatingip/v1/floatingips"
"github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/instances"
"github.com/G-Core/gcorelabscloud-go/gcore/instance/v1/types"
+ "github.com/G-Core/gcorelabscloud-go/gcore/securitygroup/v1/securitygroups"
"github.com/G-Core/gcorelabscloud-go/gcore/task/v1/tasks"
"github.com/G-Core/gcorelabscloud-go/gcore/volume/v1/volumes"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
@@ -50,8 +52,9 @@ your applications.`,
Schema: map[string]*schema.Schema{
"project_id": &schema.Schema{
- Type: schema.TypeInt,
- Optional: true,
+ Type: schema.TypeInt,
+ Optional: true,
+ Description: "Project ID, only one of project_id or project_name should be set",
ExactlyOneOf: []string{
"project_id",
"project_name",
@@ -59,8 +62,9 @@ your applications.`,
DiffSuppressFunc: suppressDiffProjectID,
},
"region_id": &schema.Schema{
- Type: schema.TypeInt,
- Optional: true,
+ Type: schema.TypeInt,
+ Optional: true,
+ Description: "Region ID, only one of region_id or region_name should be set",
ExactlyOneOf: []string{
"region_id",
"region_name",
@@ -68,16 +72,18 @@ your applications.`,
DiffSuppressFunc: suppressDiffRegionID,
},
"project_name": &schema.Schema{
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Project name, only one of project_id or project_name should be set",
ExactlyOneOf: []string{
"project_id",
"project_name",
},
},
"region_name": &schema.Schema{
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Optional: true,
+ Description: "Region name, only one of region_id or region_name should be set",
ExactlyOneOf: []string{
"region_id",
"region_name",
@@ -101,7 +107,7 @@ your applications.`,
},
"volume": &schema.Schema{
Type: schema.TypeSet,
- Optional: true,
+ Required: true,
Description: `
List of volumes for the instance. You can detach the volume from the instance by removing the
volume from the instance resource. You cannot detach the boot volume. You can attach a data volume
@@ -110,8 +116,9 @@ by adding the volume resource inside an instance resource.`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": {
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Description: "Name of the volume",
+ Computed: true,
},
"boot_index": {
Type: schema.TypeInt,
@@ -119,35 +126,37 @@ by adding the volume resource inside an instance resource.`,
Optional: true,
},
"type_name": {
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Description: "Volume type name",
+ Computed: true,
},
"image_id": {
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Description: "Image ID for the volume",
+ Computed: true,
},
"size": {
- Type: schema.TypeInt,
- Optional: true,
- Computed: true,
+ Type: schema.TypeInt,
+ Description: "Size of the volume in GiB",
+ Computed: true,
},
"volume_id": {
Type: schema.TypeString,
- Optional: true,
+ Required: true,
},
"attachment_tag": {
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Description: "Tag for the volume attachment",
+ Computed: true,
},
"id": {
Type: schema.TypeString,
- Optional: true,
Computed: true,
},
"delete_on_termination": {
- Type: schema.TypeBool,
- Optional: true,
- Computed: true,
+ Type: schema.TypeBool,
+ Description: "Delete volume on termination",
+ Computed: true,
},
},
},
@@ -196,8 +205,9 @@ inside an instance resource.`,
},
// nested map is not supported, in this case, you do not need to use the list for the map
"existing_fip_id": {
- Type: schema.TypeString,
- Optional: true,
+ Type: schema.TypeString,
+ Description: "The id of the existing floating IP that will be attached to the interface",
+ Optional: true,
},
"port_id": {
Type: schema.TypeString,
@@ -207,14 +217,15 @@ inside an instance resource.`,
},
"security_groups": {
Type: schema.TypeList,
- Optional: true,
+ Required: true,
Description: "list of security group IDs, they will be attached to exact interface",
Elem: &schema.Schema{Type: schema.TypeString},
},
"ip_address": {
- Type: schema.TypeString,
- Computed: true,
- Optional: true,
+ Type: schema.TypeString,
+ Computed: true,
+ Optional: true,
+ Description: "IP address for the interface.",
},
},
},
@@ -229,25 +240,6 @@ inside an instance resource.`,
Optional: true,
Description: "ID of the server group to use for the instance",
},
- "security_group": &schema.Schema{
- Type: schema.TypeList,
- Computed: true,
- Description: "Firewalls list, they will be attached globally on all instance's interfaces",
- Elem: &schema.Resource{
- Schema: map[string]*schema.Schema{
- "id": {
- Type: schema.TypeString,
- Description: "Firewall unique id",
- Required: true,
- },
- "name": {
- Type: schema.TypeString,
- Required: true,
- Description: "Firewall name",
- },
- },
- },
- },
"password": &schema.Schema{
Type: schema.TypeString,
Optional: true,
@@ -337,12 +329,14 @@ For Windows instances, Admin user password is set by 'password' field and cannot
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"addr": {
- Type: schema.TypeString,
- Required: true,
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "IP address",
},
"type": {
- Type: schema.TypeString,
- Required: true,
+ Type: schema.TypeString,
+ Computed: true,
+ Description: "Type of the address",
},
},
},
@@ -352,7 +346,6 @@ For Windows instances, Admin user password is set by 'password' field and cannot
},
"last_updated": &schema.Schema{
Type: schema.TypeString,
- Optional: true,
Computed: true,
},
},
@@ -536,12 +529,6 @@ func resourceInstanceV2Read(ctx context.Context, d *schema.ResourceData, m inter
if err != nil {
return diag.FromErr(err)
}
- secGroups := prepareSecurityGroups(instancePorts)
-
- if err := d.Set("security_group", secGroups); err != nil {
- return diag.FromErr(err)
- }
-
ifs, err := instances.ListInterfacesAll(client, instanceID)
if err != nil {
return diag.FromErr(err)
@@ -593,7 +580,7 @@ func resourceInstanceV2Read(ctx context.Context, d *schema.ResourceData, m inter
i["ip_address"] = assignment.IPAddress.String()
if port, err := findInstancePort(iface.PortID, instancePorts); err == nil {
- sgs := make([]string, len(port.SecurityGroups))
+ sgs := make([]interface{}, len(port.SecurityGroups))
for i, sg := range port.SecurityGroups {
sgs[i] = sg.ID
}
@@ -652,7 +639,11 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
return diag.FromErr(err)
}
- fipClient, err := CreateClient(provider, d, floatingIPsPoint, versionPointV1)
+ clientFip, err := CreateClient(provider, d, floatingIPsPoint, versionPointV1)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+ clientSg, err := CreateClient(provider, d, securityGroupPoint, versionPointV1)
if err != nil {
return diag.FromErr(err)
}
@@ -721,6 +712,11 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
}
if d.HasChange("interface") {
+ instancePorts, err := instances.ListPortsAll(client, instanceID)
+ if err != nil {
+ return diag.FromErr(err)
+ }
+
iList, err := instances.ListInterfacesAll(client, instanceID)
if err != nil {
return diag.FromErr(err)
@@ -742,7 +738,18 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
ifsOld := ifsOldRaw.(*schema.Set)
ifsNew := ifsNewRaw.(*schema.Set)
+ ifsSetByNameOld := schema.NewSet(instanceInterfaceUniqueIDByName, ifsOld.List())
+ ifsSetByNameNew := schema.NewSet(instanceInterfaceUniqueIDByName, ifsNew.List())
+
+ ifsForUpdate := schema.NewSet(instanceInterfaceUniqueIDByName, []interface{}{})
+
for _, i := range ifsOld.Difference(ifsNew).List() {
+ // if name left the same in new set, we can skip detaching
+ if ifsSetByNameNew.Contains(i) {
+ ifsForUpdate.Add(i)
+ continue
+ }
+
iface := i.(map[string]interface{})
var opts instances.InterfaceOpts
opts.PortID = iface["port_id"].(string)
@@ -770,44 +777,75 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
ifsNewSorted := ifsNew.Difference(ifsOld).List()
sort.Sort(instanceInterfaces(ifsNewSorted))
for _, i := range ifsNewSorted {
- iface := i.(map[string]interface{})
-
- iType := types.InterfaceType(iface["type"].(string))
- ifaceName := iface["name"].(string)
- opts := instances.InterfaceInstanceCreateOpts{
- InterfaceOpts: instances.InterfaceOpts{
- Name: &ifaceName,
- Type: iType,
- IPFamily: types.IPFamilyType(iface["ip_family"].(string)),
- },
+ // if it is completely new interface we need to attach it
+ if !ifsSetByNameOld.Contains(i) {
+ if err := attachNewInterface(i, client, instanceID); err != nil {
+ return diag.FromErr(err)
+ }
+ continue
}
- switch iType {
- case types.SubnetInterfaceType:
- opts.SubnetID = iface["subnet_id"].(string)
- case types.AnySubnetInterfaceType:
- opts.NetworkID = iface["network_id"].(string)
- case types.ReservedFixedIpType:
- opts.PortID = iface["port_id"].(string)
- }
+ iface := i.(map[string]interface{})
- rawSgsID := iface["security_groups"].([]interface{})
- sgs := make([]gcorecloud.ItemID, len(rawSgsID))
- for i, sgID := range rawSgsID {
- sgs[i] = gcorecloud.ItemID{ID: sgID.(string)}
+ var portID string
+ // try to find port id from old interfaces
+ for _, iOld := range ifsForUpdate.List() {
+ interfaceOld := iOld.(map[string]interface{})
+ if interfaceOld["name"] == iface["name"] {
+ portID = interfaceOld["port_id"].(string)
+ break
+ }
}
- opts.SecurityGroups = sgs
- log.Printf("[DEBUG] attach interface: %+v", opts)
- results, err := instances.AttachInterface(client, instanceID, opts).Extract()
+ log.Println("[DEBUG] Reassign security groups")
+ port, err := findInstancePort(portID, instancePorts)
if err != nil {
- return diag.Errorf("cannot attach interface: %s. Error: %s", iType, err)
+ log.Println("[DEBUG] Port not found")
+ continue
}
- taskID := results.Tasks[0]
- log.Printf("[DEBUG] attach interface taskID: %s", taskID)
- if err = tasks.WaitForStatus(client, string(taskID), tasks.TaskStateFinished, InstanceCreatingTimeout, true); err != nil {
- return diag.FromErr(err)
+ // detach what should be detached
+ sgToDetach := make([]string, 0)
+ for _, sg := range port.SecurityGroups {
+ if !slices.ContainsFunc(iface["security_groups"].([]interface{}), func(s interface{}) bool {
+ return s.(string) == sg.ID
+ }) {
+ sgToDetach = append(sgToDetach, sg.Name)
+ }
+ }
+ detachOpts := instances.SecurityGroupOpts{
+ PortsSecurityGroupNames: []instances.PortSecurityGroupNames{{
+ PortID: &portID,
+ SecurityGroupNames: sgToDetach,
+ }},
+ }
+ if err := instances.UnAssignSecurityGroup(client, instanceID, detachOpts).ExtractErr(); err != nil {
+ log.Printf("[WARNING] Cannot detach security groups: %v", err)
+ }
+
+ // attach what should be attached
+ sgToAttach := make([]string, 0)
+ for _, sg := range iface["security_groups"].([]interface{}) {
+ if !slices.ContainsFunc(port.SecurityGroups, func(s gcorecloud.ItemIDName) bool {
+ return s.ID == sg.(string)
+ }) {
+ // get the name of the security group
+ secGroup, err := securitygroups.Get(clientSg, sg.(string)).Extract()
+ if err != nil {
+ log.Printf("[WARNING] Cannot get security group %s: %v", sg, err)
+ continue
+ }
+ sgToAttach = append(sgToAttach, secGroup.Name)
+ }
+ }
+ attachOpts := instances.SecurityGroupOpts{
+ PortsSecurityGroupNames: []instances.PortSecurityGroupNames{{
+ PortID: &portID,
+ SecurityGroupNames: sgToAttach,
+ }},
+ }
+ if err := instances.AssignSecurityGroup(client, instanceID, attachOpts).ExtractErr(); err != nil {
+ log.Printf("[WARNING] Cannot attach security groups: %v", err)
}
}
@@ -818,7 +856,7 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
mm[i.Key] = i.Value
}
- _, err := floatingips.Assign(fipClient, fip.ID, floatingips.CreateOpts{
+ _, err := floatingips.Assign(clientFip, fip.ID, floatingips.CreateOpts{
PortID: fip.PortID,
FixedIPAddress: fip.FixedIPAddress,
Metadata: mm,
@@ -904,8 +942,63 @@ func resourceInstanceV2Update(ctx context.Context, d *schema.ResourceData, m int
}
func instanceInterfaceUniqueID(i interface{}) int {
+ e := i.(map[string]interface{})
+ h := md5.New()
+ securitygroupsRaw := e["security_groups"].([]interface{})
+ var securitygroups string
+ for _, sg := range securitygroupsRaw {
+ securitygroups += sg.(string)
+ }
+ io.WriteString(h, e["name"].(string))
+ io.WriteString(h, securitygroups)
+ return int(binary.BigEndian.Uint64(h.Sum(nil)))
+}
+
+func instanceInterfaceUniqueIDByName(i interface{}) int {
e := i.(map[string]interface{})
h := md5.New()
io.WriteString(h, e["name"].(string))
return int(binary.BigEndian.Uint64(h.Sum(nil)))
}
+
+func attachNewInterface(i interface{}, client *gcorecloud.ServiceClient, instanceID string) error {
+ iface := i.(map[string]interface{})
+ iType := types.InterfaceType(iface["type"].(string))
+ ifaceName := iface["name"].(string)
+ opts := instances.InterfaceInstanceCreateOpts{
+ InterfaceOpts: instances.InterfaceOpts{
+ Name: &ifaceName,
+ Type: iType,
+ IPFamily: types.IPFamilyType(iface["ip_family"].(string)),
+ },
+ }
+
+ switch iType {
+ case types.SubnetInterfaceType:
+ opts.SubnetID = iface["subnet_id"].(string)
+ case types.AnySubnetInterfaceType:
+ opts.NetworkID = iface["network_id"].(string)
+ case types.ReservedFixedIpType:
+ opts.PortID = iface["port_id"].(string)
+ }
+
+ rawSgsID := iface["security_groups"].([]interface{})
+ sgs := make([]gcorecloud.ItemID, len(rawSgsID))
+ for i, sgID := range rawSgsID {
+ sgs[i] = gcorecloud.ItemID{ID: sgID.(string)}
+ }
+ opts.SecurityGroups = sgs
+
+ log.Printf("[DEBUG] attach interface: %+v", opts)
+ results, err := instances.AttachInterface(client, instanceID, opts).Extract()
+ if err != nil {
+ return fmt.Errorf("cannot attach interface: %s. Error: %s", iType, err)
+ }
+
+ taskID := results.Tasks[0]
+ log.Printf("[DEBUG] attach interface taskID: %s", taskID)
+ if err = tasks.WaitForStatus(client, string(taskID), tasks.TaskStateFinished, InstanceCreatingTimeout, true); err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/gcore/resource_gcore_securitygroup.go b/gcore/resource_gcore_securitygroup.go
index 726055d..2ece565 100644
--- a/gcore/resource_gcore_securitygroup.go
+++ b/gcore/resource_gcore_securitygroup.go
@@ -2,11 +2,13 @@ package gcore
import (
"context"
+ "errors"
"fmt"
"log"
"strings"
"time"
+ gcorecloud "github.com/G-Core/gcorelabscloud-go"
"github.com/G-Core/gcorelabscloud-go/gcore/securitygroup/v1/securitygrouprules"
"github.com/G-Core/gcorelabscloud-go/gcore/securitygroup/v1/securitygroups"
"github.com/G-Core/gcorelabscloud-go/gcore/securitygroup/v1/types"
@@ -268,6 +270,12 @@ func resourceSecurityGroupRead(ctx context.Context, d *schema.ResourceData, m in
sg, err := securitygroups.Get(client, d.Id()).Extract()
if err != nil {
+ var errDefault404 gcorecloud.ErrDefault404
+ if errors.As(err, &errDefault404) {
+ // removing from state because it doesn't exist anymore
+ d.SetId("")
+ return nil
+ }
return diag.FromErr(err)
}
@@ -409,7 +417,11 @@ func resourceSecurityGroupDelete(ctx context.Context, d *schema.ResourceData, m
err = securitygroups.Delete(client, sgID).Err
if err != nil {
- return diag.FromErr(err)
+ // if err is not found that's mean everything is ok
+ var errDefault404 gcorecloud.ErrDefault404
+ if !errors.As(err, &errDefault404) {
+ return diag.FromErr(err)
+ }
}
d.SetId("")
diff --git a/gcore/resource_gcore_subnet.go b/gcore/resource_gcore_subnet.go
index c739948..4e02344 100644
--- a/gcore/resource_gcore_subnet.go
+++ b/gcore/resource_gcore_subnet.go
@@ -2,6 +2,7 @@ package gcore
import (
"context"
+ "errors"
"fmt"
"log"
"net"
@@ -315,6 +316,12 @@ func resourceSubnetRead(ctx context.Context, d *schema.ResourceData, m interface
subnet, err := subnets.Get(client, subnetID).Extract()
if err != nil {
+ var errDefault404 gcorecloud.ErrDefault404
+ if errors.As(err, &errDefault404) {
+ // removing from state because it doesn't exist anymore
+ d.SetId("")
+ return nil
+ }
return diag.Errorf("cannot get subnet with ID: %s. Error: %s", subnetID, err)
}
diff --git a/gcore/resource_gcore_volume.go b/gcore/resource_gcore_volume.go
index a687ad8..e21714c 100644
--- a/gcore/resource_gcore_volume.go
+++ b/gcore/resource_gcore_volume.go
@@ -2,6 +2,7 @@ package gcore
import (
"context"
+ "errors"
"fmt"
"log"
"time"
@@ -238,6 +239,12 @@ func resourceVolumeRead(ctx context.Context, d *schema.ResourceData, m interface
volume, err := volumes.Get(client, volumeID).Extract()
if err != nil {
+ var errDefault404 gcorecloud.ErrDefault404
+ if errors.As(err, &errDefault404) {
+ // removing from state because it doesn't exist anymore
+ d.SetId("")
+ return nil
+ }
return diag.Errorf("cannot get volume with ID: %s. Error: %s", volumeID, err)
}
diff --git a/templates/resources/instancev2.md.tmpl b/templates/resources/instancev2.md.tmpl
index a019d16..65bbd48 100644
--- a/templates/resources/instancev2.md.tmpl
+++ b/templates/resources/instancev2.md.tmpl
@@ -33,6 +33,14 @@ This example demonstrates how to create an instance with two network interfaces:
### Advanced examples
+
+#### Creating instance with a dual-stack public interface
+
+This example demonstrates how to create an instance with a dual-stack public interface.
+The instance has both an IPv4 and an IPv6 address.
+
+{{tffile "examples/resources/gcore_instancev2/dualstack-interface.tf"}}
+
#### Creating instance with floating ip
{{tffile "examples/resources/gcore_instancev2/fip.tf"}}