Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
eb3b688
WIP: Alias IPv6 secondary range support
Ummulkiram2410 Apr 27, 2026
03a48d7
Add support for secondary IPv6 ranges in Subnetworks
Ummulkiram2410 May 4, 2026
fbb1b14
adding proper tests
Ummulkiram2410 May 4, 2026
9ede616
Resolve the build errors
Ummulkiram2410 May 4, 2026
87059d6
Removing trailing spaces
Ummulkiram2410 May 4, 2026
37bc4da
Updated the test with region where the test would work
Ummulkiram2410 May 6, 2026
268b107
Updating the code for lint error
Ummulkiram2410 May 8, 2026
af10f4a
Adding the condition to run acceptance test only in beta
Ummulkiram2410 May 8, 2026
b343043
Adding the condition to run acceptance test only in beta
Ummulkiram2410 May 8, 2026
3ef28d5
updating the test region
Ummulkiram2410 May 12, 2026
b008252
fix tests
Ummulkiram2410 May 13, 2026
b6a680c
fix tests
Ummulkiram2410 May 13, 2026
352b4a1
fix tests
Ummulkiram2410 May 13, 2026
1e9f31e
fix tests
Ummulkiram2410 May 13, 2026
20ca448
fix tests
Ummulkiram2410 May 13, 2026
fda0adb
fix tests
Ummulkiram2410 May 13, 2026
bd85719
Fixing IPv4 test sendEmpty failure
Ummulkiram2410 May 13, 2026
b6e20da
Fixing IPv4 test sendEmpty failure
Ummulkiram2410 May 13, 2026
9a87968
adding deletion policy for fixing ipv4 deletion test
Ummulkiram2410 May 13, 2026
952df8d
Fixing ipv4 deletion test via self link bypass
Ummulkiram2410 May 13, 2026
67bf892
Fixing ipv4 deletion test via self link bypass
Ummulkiram2410 May 13, 2026
b731e13
Add cty IsNull protection to compute_subnetwork post_update
Ummulkiram2410 May 13, 2026
9188282
Fix TGC nil schema panic in subnetwork custom diff
Ummulkiram2410 May 13, 2026
6ef9339
Adding unit tests for customizeDiff
Ummulkiram2410 May 14, 2026
0b9bc8a
Fixing imports for beta
Ummulkiram2410 May 14, 2026
055860e
to merge with latest changes.
Ummulkiram2410 May 17, 2026
4bc9a50
Implement BypassClientsideUpdateCheck flag
Ummulkiram2410 May 17, 2026
e3cdcce
Fix TGC Next integration compilation error by excluding terraformgoog…
Ummulkiram2410 May 18, 2026
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
5 changes: 5 additions & 0 deletions mmv1/api/resource.go
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@ type Resource struct {
// [Optional] If set to true, the resource is not able to be updated.
Immutable bool `yaml:"immutable,omitempty"`

// [Optional] If set to true, bypasses the generated client-side only check during update.
// This should only be used for edge cases of an edge case where virtual field flags that can be set
// in advance of an intended change are being used to detect explicit nulls on Optional+Computed fields.
BypassClientsideUpdateCheck bool `yaml:"bypass_clientside_update_check,omitempty"`

// [Optional] If set to true, the object has a `self_link` field. This is
// typical of older GCP APIs.
HasSelfLink bool `yaml:"has_self_link,omitempty"`
Expand Down
30 changes: 30 additions & 0 deletions mmv1/products/compute/Subnetwork.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ references:
base_url: projects/{{project}}/regions/{{region}}/subnetworks
immutable: true
has_self_link: true
bypass_clientside_update_check: true
collection_url_key: items
iam_policy:
allowed_iam_role: roles/compute.networkUser
Expand All @@ -55,6 +56,7 @@ id_format: projects/{{project}}/regions/{{region}}/subnetworks/{{name}}
custom_diff:
- customdiff.ForceNewIfChange("ip_cidr_range", IsShrinkageIpCidr)
- sendSecondaryIpRangeIfEmptyDiff
- resourceComputeSubnetworkSecondaryIpRangeCustomDiff
sweeper:
url_substitutions:
- region: us-west2
Expand Down Expand Up @@ -174,6 +176,15 @@ examples:
vars:
network_name: network-ipv6-only
subnetwork_name: subnet-ipv6-only
- name: subnetwork_with_secondary_ipv6_range
primary_resource_id: subnetwork_with_secondary_ipv6_range
vars:
network_name: network-with-secondary-ranges
subnetwork_name: subnet-with-secondary-ranges
pap_name: pap-for-secondary-ranges
pdp_name: pdp-for-secondary-ranges
sub_pdp_name: sub-pdp-for-secondary-ranges
min_version: 'beta'
virtual_fields:
- name: send_secondary_ip_range_if_empty
type: Boolean
Expand Down Expand Up @@ -333,6 +344,25 @@ properties:
E.g. `networkconnectivity.googleapis.com/projects/{project}/locations/global/internalRanges/{rangeId}`
resource: InternalRange
imports: selfLink
- name: ipVersion
Comment thread
c2thorn marked this conversation as resolved.
type: Enum
description: |
The IP version of the secondary range. If not specified, IPV4 is used.
Comment thread
Ummulkiram2410 marked this conversation as resolved.
default_from_api: true
min_version: 'beta'
enum_values:
- IPV4
- IPV6
- name: ipCollection
type: ResourceRef
description: |
Reference to a Public Delegated Prefix (PDP) for BYOIP.
This field should be specified for configuring BYOGUA internal IPv6 secondary range.
When specified along with the ip_cidr_range, the ip_cidr_range must lie within the PDP referenced by the `ipCollection` field.
When specified without the ip_cidr_range, the range is auto-allocated from the PDP referenced by the `ipCollection` field.
min_version: 'beta'
resource: PublicDelegatedPrefix
imports: selfLink
- name: privateIpGoogleAccess
type: Boolean
description: |
Expand Down
137 changes: 136 additions & 1 deletion mmv1/templates/terraform/constants/subnetwork.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ func IpDiffSuppress(_, old, new string, d *schema.ResourceData) bool {
addr_netmask_old := strings.Split(old, "/")
addr_netmask_new := strings.Split(new, "/")

if (!((len(addr_netmask_old)) == 2 && (len(addr_netmask_new) == 2))) {
if !((len(addr_netmask_old)) == 2 && (len(addr_netmask_new) == 2)) {
return false
}

Expand All @@ -85,3 +85,138 @@ func IpDiffSuppress(_, old, new string, d *schema.ResourceData) bool {

return addr_equality && netmask_equality
}

// CustomDiff function for secondary_ip_range.
// Normalizes old state and new config sets before Set comparison to prevent false TypeSet diffs.
// Specifically handles two Beta-only scenarios where state diverges from HCL config:
// 1. Automatically inherits allocated ULA CIDRs from state when omitted in HCL config.
// 2. Normalizes ip_collection self-links to relative paths to match user config short names.
func resourceComputeSubnetworkSecondaryIpRangeCustomDiff(_ context.Context, diff *schema.ResourceDiff, meta interface{}) error {
return resourceComputeSubnetworkSecondaryIpRangeCustomDiffFunc(diff)
}

func resourceComputeSubnetworkSecondaryIpRangeCustomDiffFunc(diff tpgresource.TerraformResourceDiff) error {
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We now have two handwritten custom diff functions on the same parent field (technically three customdiffs total). For maintenance, I believe we should consider refactoring them to a single, large function so the different states and diff outcomes can be followed along within a single function vs mentally mapping them sequentially.

Maybe not for this PR as it's already large and having a large amount of testing as is.

{{ if and (ne $.TargetVersionName `ga`) (and (ne $.ProductMetadata.Compiler `terraformgoogleconversion-codegen`) (ne $.ProductMetadata.Compiler `terraformgoogleconversionnext-codegen`)) -}}
if len(diff.GetChangedKeysPrefix("secondary_ip_range")) == 0 {
return nil
}

oldCount, newCount := diff.GetChange("secondary_ip_range.#")
count := oldCount.(int)
if newCount.(int) > count {
count = newCount.(int)
}
if count < 1 {
return nil
}

// Extract pristine user CIDR config directly from HCL
configCidrByName := make(map[string]string)
rawConfig := diff.GetRawConfig().GetAttr("secondary_ip_range")
if rawConfig.IsKnown() && !rawConfig.IsNull() {
for _, item := range rawConfig.AsValueSlice() {
if item.IsKnown() && !item.IsNull() && item.GetAttr("range_name").IsKnown() && !item.GetAttr("range_name").IsNull() {
nameVal := item.GetAttr("range_name").AsString()
if nameVal != "" {
if cidrVal := item.GetAttr("ip_cidr_range"); cidrVal.IsKnown() && !cidrVal.IsNull() {
configCidrByName[nameVal] = cidrVal.AsString()
} else {
configCidrByName[nameVal] = ""
}
}
}
}
}

// Extract old state and new proposed maps
oldByName := make(map[string]map[string]interface{})
var oldList, newList []interface{}
for i := 0; i < count; i++ {
o, n := diff.GetChange(fmt.Sprintf("secondary_ip_range.%d", i))
if o != nil {
oldList = append(oldList, o)
if m, ok := o.(map[string]interface{}); ok && m["range_name"] != nil {
oldByName[m["range_name"].(string)] = m
}
}
if n != nil {
newList = append(newList, n)
}
}

// Helper: normalizes CIDR strings and ip_collection self-links
normalizeItem := func(m map[string]interface{}, overrideCidr string) map[string]interface{} {
clean := make(map[string]interface{})
for k, v := range m {
if k == "ip_cidr_range" {
cidrStr, _ := v.(string)
if overrideCidr != "" {
cidrStr = overrideCidr
}
if cidrStr == "" {
continue
}
if ip, ipNet, err := net.ParseCIDR(cidrStr); err == nil {
maskSize, _ := ipNet.Mask.Size()
cidrStr = fmt.Sprintf("%s/%d", ip.String(), maskSize)
}
clean[k] = cidrStr
continue
}
if v == nil || v == "" {
continue
}
if k == "ip_collection" {
if rel, err := tpgresource.GetRelativePath(v.(string)); err == nil {
clean[k] = rel
} else {
clean[k] = v
}
} else {
clean[k] = v
}
}
return clean
}

// Build clean normalized sets
var normalizedOld, normalizedNew []interface{}
for _, o := range oldList {
normalizedOld = append(normalizedOld, normalizeItem(o.(map[string]interface{}), ""))
}
for _, n := range newList {
m := n.(map[string]interface{})
rangeName, _ := m["range_name"].(string)
userCidr := configCidrByName[rangeName]
if userCidr == "" {
// Inherit allocated state CIDR for ULA ranges
if oldItem, ok := oldByName[rangeName]; ok && oldItem["ip_cidr_range"] != nil {
userCidr, _ = oldItem["ip_cidr_range"].(string)
}
}
normalizedNew = append(normalizedNew, normalizeItem(m, userCidr))
}

// Compare Sets and Clear Diff
var hashFunc schema.SchemaSetFunc
if ResourceComputeSubnetwork().Schema != nil && ResourceComputeSubnetwork().Schema["secondary_ip_range"] != nil {
hashFunc = schema.HashResource(ResourceComputeSubnetwork().Schema["secondary_ip_range"].Elem.(*schema.Resource))
} else {
hashFunc = func(v interface{}) int {
buf := &bytes.Buffer{}
json.NewEncoder(buf).Encode(v)
return schema.HashString(buf.String())
}
}

if schema.NewSet(hashFunc, normalizedOld).Equal(schema.NewSet(hashFunc, normalizedNew)) {
if err := diff.Clear("secondary_ip_range"); err != nil {
return err
}
}

return nil
{{ else -}}
return nil
{{ end -}}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
resource "google_compute_subnetwork" "{{$.PrimaryResourceId}}" {
provider = google-beta
name = "{{index $.Vars "subnetwork_name"}}"
region = "us-central1"
network = google_compute_network.custom-test.id
stack_type = "IPV6_ONLY"
ipv6_access_type = "INTERNAL"

secondary_ip_range {
range_name = "v6-ula"
ip_version = "IPV6"
}

secondary_ip_range {
range_name = "v6-byogua-auto"
ip_version = "IPV6"
ip_collection = google_compute_public_delegated_prefix.ipv6_sub_pdp.self_link
}

secondary_ip_range {
range_name = "v6-byogua-manual"
ip_version = "IPV6"
ip_collection = google_compute_public_delegated_prefix.ipv6_sub_pdp.self_link
ip_cidr_range = "2001:db8:0:2::/64"
}
}

resource "google_compute_network" "custom-test" {
provider = google-beta
name = "{{index $.Vars "network_name"}}"
auto_create_subnetworks = false
enable_ula_internal_ipv6 = true
}

resource "google_compute_public_advertised_prefix" "ipv6_pap" {
provider = google-beta
name = "{{index $.Vars "pap_name"}}"
ip_cidr_range = "2001:db8::/40"
pdp_scope = "REGIONAL"
ipv6_access_type = "INTERNAL"
description = "GOOGLE_INTERNAL_TEST_PREFIX"
}

resource "google_compute_public_delegated_prefix" "ipv6_pdp" {
provider = google-beta
name = "{{index $.Vars "pdp_name"}}"
region = "us-central1"
description = "PDP in internal subnet mode"
ip_cidr_range = "2001:db8::/48"
parent_prefix = google_compute_public_advertised_prefix.ipv6_pap.id
mode = "DELEGATION"
}

resource "google_compute_public_delegated_prefix" "ipv6_sub_pdp" {
provider = google-beta
name = "{{index $.Vars "sub_pdp_name"}}"
region = "us-central1"
ip_cidr_range = "2001:db8::/56"
parent_prefix = google_compute_public_delegated_prefix.ipv6_pdp.id
mode = "INTERNAL_IPV6_SUBNETWORK_CREATION"
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
// Handle the "Send Empty" override logic
if v, ok := d.GetOk("send_secondary_ip_range_if_empty"); ok && v.(bool) {
if sv, ok := d.GetOk("secondary_ip_range"); ok {
oldRanges, _ := d.GetChange("secondary_ip_range")
if oldRanges != nil {
configValue := d.GetRawConfig().GetAttr("secondary_ip_range")
stateValue := sv.([]interface{})
if configValue.LengthInt() == 0 && len(stateValue) != 0 {
stateValue := oldRanges.([]interface{})
if (configValue.IsNull() || configValue.LengthInt() == 0) && len(stateValue) != 0 {
log.Printf("[DEBUG] Sending empty secondary_ip_range in update")
obj := make(map[string]interface{})
obj["secondaryIpRanges"] = make([]interface{}, 0)
Expand Down
2 changes: 1 addition & 1 deletion mmv1/templates/terraform/resource.go.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -852,7 +852,7 @@ func resource{{ $.ResourceName -}}Read(d *schema.ResourceData, meta interface{})

{{if $.Updatable -}}
func resource{{ $.ResourceName -}}Update(d *schema.ResourceData, meta interface{}) error {
{{- if and (not $.ExcludeDelete) (not $.DeletionPolicyExclude)}}
{{- if and (and (not $.ExcludeDelete) (not $.DeletionPolicyExclude)) (not $.BypassClientsideUpdateCheck)}}
clientSideFields := map[string]bool{"deletion_policy": true}
clientSideOnly := true
for field := range Resource{{ $.ResourceName -}}().Schema {
Expand Down
Loading
Loading