Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add external ipv6 support #5241

Merged
merged 7 commits into from
Sep 29, 2021
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions mmv1/products/compute/api.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14674,6 +14674,36 @@ objects:
https://cloud.google.com/vpc/docs/flow-logs#filtering for details on how to format this field.
The default value is 'true', which evaluates to include everything.
default_value: "true"
- !ruby/object:Api::Type::Enum
name: 'stackType'
update_verb: :PATCH
update_url: projects/{{project}}/regions/{{region}}/subnetworks/{{name}}
fingerprint_name: 'fingerprint'
values:
- :IPV4_ONLY
- :IPV4_IPV6
default_value: :IPV4_ONLY
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think we can apply a default here! The API doesn't return the value for older resources, so it wouldn't be safe to apply.

Here's an example from a couple nights ago:

{
  "id": "9043695605119007144",
  "creationTimestamp": "2021-09-23T06:55:19.039-07:00",
  "name": "tf-test-xyvapdt3w1",
  "selfLink": "https://www.googleapis.com/compute/beta/projects/ci-test-project-188019/global/networks/tf-test-xyvapdt3w1",
  "autoCreateSubnetworks": false,
  "routingConfig": {
    "routingMode": "REGIONAL"
  },
  "kind": "compute#network"
}

There's a few mitigation options:

  1. Remove the default, do nothing else
    • This is probably what I'd do- it has a mix of risk and safety.
  2. Remove the default, make the field O+C
    • This is technically the safest option, but has the downside of making the field O+C. It's safest because if the API started returning it out of the blue, Terraform wouldn't have a diff. This is probably not worth applying.
  3. Add default_if_empty: true in terraform.yaml
    • This gives us a Terraform-side default, which is useful information at plan time, but means that if the default ever changes we may handle it very badly. default_if_empty rewrites all empty responses into the default, so if empty means something else Terraform'll confuse users.

description: |
The stack type for this subnet to identify whether the IPv6 feature is enabled or not.
If not specified IPV4_ONLY will be used.
- !ruby/object:Api::Type::Enum
name: 'ipv6AccessType'
values:
- :EXTERNAL
description: |
The access type of IPv6 address this subnet holds. It's immutable and can only be specified during creation
or the first time the subnet is updated into IPV4_IPV6 dual stack. If the ipv6_type is EXTERNAL then this subnet
cannot enable direct path.
- !ruby/object:Api::Type::String
name: 'ipv6CidrRange'
output: true
description: |
The range of internal IPv6 addresses that are owned by this subnetwork.
- !ruby/object:Api::Type::String
name: 'externalIpv6Prefix'
output: true
description: |
The range of external IPv6 addresses that are owned by this subnetwork.
references: !ruby/object:Api::Resource::ReferenceLinks
guides:
'Private Google Access':
Expand Down
6 changes: 6 additions & 0 deletions mmv1/products/compute/terraform.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2711,6 +2711,12 @@ overrides: !ruby/object:Overrides::ResourceOverrides
vars:
subnetwork_name: "l7lb-test-subnetwork"
network_name: "l7lb-test-network"
- !ruby/object:Provider::Terraform::Examples
name: "subnetwork_ipv6"
primary_resource_id: "subnetwork-ipv6"
vars:
subnetwork_name: "ipv6-test-subnetwork"
network_name: "ipv6-test-network"
TargetHttpProxy: !ruby/object:Overrides::Terraform::ResourceOverride
examples:
- !ruby/object:Provider::Terraform::Examples
Expand Down
16 changes: 16 additions & 0 deletions mmv1/templates/terraform/examples/subnetwork_ipv6.tf.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
resource "google_compute_subnetwork" "subnetwork-ipv6" {
name = "<%= ctx[:vars]['subnetwork_name'] %>"

ip_cidr_range = "10.0.0.0/22"
region = "us-west2"

stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL"

network = google_compute_network.custom-test.id
}

resource "google_compute_network" "custom-test" {
name = "<%= ctx[:vars]['network_name'] %>"
auto_create_subnetworks = false
}
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,54 @@ func resourceComputeInstance() *schema.Resource {
},
},
},

"stack_type": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"IPV4_ONLY", "IPV4_IPV6", ""}, false),
Description: `The stack type for this network interface to identify whether the IPv6 feature is enabled or not. If not specified, IPV4_ONLY will be used.`,
Default: "IPV4_ONLY",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Conversely, this one does appear to be returned. There's a question of whether it's also returned for old resources created before the field was introduced, and I'm not sure what the answer is. If you happen to have an old GCE instance lying around, that'd be worth checking!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i checked, it is not returned on old instances. thanks for pointing this out!

},

"ipv6_access_type": {
Type: schema.TypeString,
Computed: true,
Description: `One of EXTERNAL, INTERNAL to indicate whether the IP can be accessed from the Internet. This field is always inherited from its subnetwork.`,
},

"ipv6_access_config": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make one of the subfields of this Required? That'll stop the empty block problem.

Type: schema.TypeList,
Optional: true,
Description: `An array of IPv6 access configurations for this interface. Currently, only one IPv6 access config, DIRECT_IPV6, is supported. If there is no ipv6AccessConfig specified, then this instance will have no external IPv6 Internet access.`,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: ordering Required -> Optional -> Computed would be nice, that's consistent with most of the code base (although there are certainly exceptions)

"external_ipv6": {
Type: schema.TypeString,
Computed: true,
Description: `The first IPv6 address of the external IPv6 range associated with this instance, prefix length is stored in externalIpv6PrefixLength in ipv6AccessConfig. The field is output only, an IPv6 address from a subnetwork associated with the instance will be allocated dynamically.`,
},

"external_ipv6_prefix_length": {
Type: schema.TypeString,
Computed: true,
Description: `The prefix length of the external IPv6 range.`,
},

"public_ptr_domain_name": {
Type: schema.TypeString,
Optional: true,
Description: `The domain name to be used when creating DNSv6 records for the external IPv6 ranges.`,
},

"network_tier": {
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"PREMIUM"}, false),
Description: `The service-level to be provided for IPv6 traffic when the subnet has an external subnet. Only PREMIUM tier is valid for IPv6`,
},
},
},
},
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,30 @@ func TestAccComputeInstance_IP(t *testing.T) {
})
}

func TestAccComputeInstance_IPv6(t *testing.T) {
t.Parallel()

var instance compute.Instance
var ipName = fmt.Sprintf("tf-test-%s", randString(t, 10))
var instanceName = fmt.Sprintf("tf-test-%s", randString(t, 10))
var ptrName = fmt.Sprintf("tf-test-%s", randString(t, 10))

vcrTest(t, resource.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Providers: testAccProviders,
CheckDestroy: testAccCheckComputeInstanceDestroyProducer(t),
Steps: []resource.TestStep{
{
Config: testAccComputeInstance_ipv6(ipName, instanceName, ptrName),
Check: resource.ComposeTestCheckFunc(
testAccCheckComputeInstanceExists(
t, "google_compute_instance.foobar", &instance),
),
},
},
})
}

func TestAccComputeInstance_PTRRecord(t *testing.T) {
t.Parallel()

Expand Down Expand Up @@ -3259,6 +3283,63 @@ resource "google_compute_instance" "foobar" {
`, ip, instance)
}

func testAccComputeInstance_ipv6(ip, instance, record string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
family = "debian-9"
project = "debian-cloud"
}

resource "google_compute_subnetwork" "subnetwork-ipv6" {
name = "%s-subnetwork"

ip_cidr_range = "10.0.0.0/22"
region = "us-west2"

stack_type = "IPV4_IPV6"
ipv6_access_type = "EXTERNAL"

network = google_compute_network.custom-test.id
}

resource "google_compute_network" "custom-test" {
name = "%s-network"
auto_create_subnetworks = false
}


resource "google_compute_address" "foo" {
name = "%s"
}

resource "google_compute_instance" "foobar" {
name = "%s"
machine_type = "e2-medium"
zone = "us-west2-a"
tags = ["foo", "bar"]

boot_disk {
initialize_params {
image = data.google_compute_image.my_image.self_link
}
}

network_interface {
subnetwork = google_compute_subnetwork.subnetwork-ipv6.name
stack_type = "IPV4_IPV6"
ipv6_access_config {
network_tier = "PREMIUM"
public_ptr_domain_name = "%s.gcp.tfacc.hashicorptest.com."
}
}

metadata = {
foo = "bar"
}
}
`, instance, instance, ip, instance, record)
}

func testAccComputeInstance_PTRRecord(record, instance string) string {
return fmt.Sprintf(`
data "google_compute_image" "my_image" {
Expand Down
57 changes: 44 additions & 13 deletions mmv1/third_party/terraform/utils/compute_instance_helpers.go.erb
Original file line number Diff line number Diff line change
Expand Up @@ -164,6 +164,17 @@ func flattenAccessConfigs(accessConfigs []*computeBeta.AccessConfig) ([]map[stri
return flattened, natIP
}

func flattenIpv6AccessConfigs(ipv6AccessConfigs []*computeBeta.AccessConfig) []map[string]interface{} {
flattened := make([]map[string]interface{}, len(ipv6AccessConfigs))
for i, ac := range ipv6AccessConfigs {
flattened[i] = map[string]interface{}{
"network_tier": ac.NetworkTier,
}
flattened[i]["public_ptr_domain_name"] = ac.PublicPtrDomainName
}
return flattened
}

func flattenNetworkInterfaces(d *schema.ResourceData, config *Config, networkInterfaces []*computeBeta.NetworkInterface) ([]map[string]interface{}, string, string, string, error) {
flattened := make([]map[string]interface{}, len(networkInterfaces))
var region, internalIP, externalIP string
Expand All @@ -179,13 +190,15 @@ func flattenNetworkInterfaces(d *schema.ResourceData, config *Config, networkInt
region = subnet.Region

flattened[i] = map[string]interface{}{
"network_ip": iface.NetworkIP,
"network": ConvertSelfLinkToV1(iface.Network),
"subnetwork": ConvertSelfLinkToV1(iface.Subnetwork),
"subnetwork_project": subnet.Project,
"access_config": ac,
"alias_ip_range": flattenAliasIpRange(iface.AliasIpRanges),
"nic_type": iface.NicType,
"network_ip": iface.NetworkIP,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instance template shares this code, so this is likely to crash it by setting a field that doesn't exist. Does template support configuring this value? We may need to guard it, otherwise.

"network": ConvertSelfLinkToV1(iface.Network),
"subnetwork": ConvertSelfLinkToV1(iface.Subnetwork),
"subnetwork_project": subnet.Project,
"access_config": ac,
"alias_ip_range": flattenAliasIpRange(iface.AliasIpRanges),
"nic_type": iface.NicType,
"stack_type": iface.StackType,
"ipv6_access_config": flattenIpv6AccessConfigs(iface.Ipv6AccessConfigs),
}
// Instance template interfaces never have names, so they're absent
// in the instance template network_interface schema. We want to use the
Expand Down Expand Up @@ -219,6 +232,22 @@ func expandAccessConfigs(configs []interface{}) []*computeBeta.AccessConfig {
return acs
}

func expandIpv6AccessConfigs(configs []interface{}) []*computeBeta.AccessConfig {
iacs := make([]*computeBeta.AccessConfig, len(configs))
for i, raw := range configs {
iacs[i] = &computeBeta.AccessConfig{}
if raw != nil {
data := raw.(map[string]interface{})
iacs[i].NetworkTier = data["network_tier"].(string)
if ptr, ok := data["public_ptr_domain_name"]; ok && ptr != "" {
iacs[i].PublicPtrDomainName = ptr.(string)
}
iacs[i].Type = "DIRECT_IPV6" // Currently only type supported
}
}
return iacs
}

func expandNetworkInterfaces(d TerraformResourceData, config *Config) ([]*computeBeta.NetworkInterface, error) {
configs := d.Get("network_interface").([]interface{})
ifaces := make([]*computeBeta.NetworkInterface, len(configs))
Expand All @@ -243,12 +272,14 @@ func expandNetworkInterfaces(d TerraformResourceData, config *Config) ([]*comput
}

ifaces[i] = &computeBeta.NetworkInterface{
NetworkIP: data["network_ip"].(string),
Network: nf.RelativeLink(),
Subnetwork: sf.RelativeLink(),
AccessConfigs: expandAccessConfigs(data["access_config"].([]interface{})),
AliasIpRanges: expandAliasIpRanges(data["alias_ip_range"].([]interface{})),
NicType: data["nic_type"].(string),
NetworkIP: data["network_ip"].(string),
Network: nf.RelativeLink(),
Subnetwork: sf.RelativeLink(),
AccessConfigs: expandAccessConfigs(data["access_config"].([]interface{})),
AliasIpRanges: expandAliasIpRanges(data["alias_ip_range"].([]interface{})),
NicType: data["nic_type"].(string),
StackType: data["stack_type"].(string),
Ipv6AccessConfigs: expandIpv6AccessConfigs(data["ipv6_access_config"].([]interface{})),
}
}
return ifaces, nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,14 @@ The `network_interface` block supports:

* `nic_type` - (Optional) The type of vNIC to be used on this interface. Possible values: GVNIC, VIRTIO_NET.


* `stack_type` - (Optional) The stack type for this network interface to identify whether the IPv6 feature is enabled or not. Values are IPV4_IPV6 or IPV4_ONLY. If not specified, IPV4_ONLY will be used.

* `ipv6_access_config` - (Optional) An array of IPv6 access configurations for this interface.
Currently, only one IPv6 access config, DIRECT_IPV6, is supported. If there is no ipv6AccessConfig
specified, then this instance will have no external IPv6 Internet access. Structure documented below.


The `access_config` block supports:

* `nat_ip` - (Optional) The IP address that will be 1:1 mapped to the instance's
Expand All @@ -308,6 +316,14 @@ The `access_config` block supports:
This field can take the following values: PREMIUM or STANDARD. If this field is
not specified, it is assumed to be PREMIUM.

The `ipv6_access_config` block supports:

* `public_ptr_domain_name` - (Optional) The domain name to be used when creating DNSv6
records for the external IPv6 ranges..

* `network_tier` - (Optional) The service-level to be provided for IPv6 traffic when the
subnet has an external subnet. Only PREMIUM tier is valid for IPv6.

The `alias_ip_range` block supports:

* `ip_cidr_range` - The IP CIDR range represented by this alias IP range. This IP CIDR range
Expand Down Expand Up @@ -420,10 +436,19 @@ exported:

* `cpu_platform` - The CPU platform used by this instance.

* `ipv6_access_type` - One of EXTERNAL, INTERNAL to indicate whether the IP can be accessed from the Internet.
This field is always inherited from its subnetwork.

* `network_interface.0.network_ip` - The internal ip address of the instance, either manually or dynamically assigned.

* `network_interface.0.access_config.0.nat_ip` - If the instance has an access config, either the given external ip (in the `nat_ip` field) or the ephemeral (generated) ip (if you didn't provide one).

* `network_interface.0.ipv6_access_config.0.external_ipv6` - The first IPv6 address of the external IPv6 range
associated with this instance, prefix length is stored in externalIpv6PrefixLength in ipv6AccessConfig.
The field is output only, an IPv6 address from a subnetwork associated with the instance will be allocated dynamically.

* `network_interface.0.ipv6_access_config.0.external_ipv6_prefix_length` - The prefix length of the external IPv6 range.

* `attached_disk.0.disk_encryption_key_sha256` - The [RFC 4648 base64](https://tools.ietf.org/html/rfc4648#section-4)
encoded SHA-256 hash of the [customer-supplied encryption key]
(https://cloud.google.com/compute/docs/disks/customer-supplied-encryption) that protects this resource.
Expand Down