Skip to content

Commit a161067

Browse files
compute: canonicalize backend.group self-links to avoid spurious diffs between v1/beta and variants (#14939)
1 parent 34e26e7 commit a161067

File tree

3 files changed

+315
-1
lines changed

3 files changed

+315
-1
lines changed

mmv1/products/compute/RegionBackendService.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -251,7 +251,7 @@ properties:
251251
Group resource using the fully-qualified URL, rather than a
252252
partial URL.
253253
required: true
254-
diff_suppress_func: 'tpgresource.CompareSelfLinkRelativePaths'
254+
diff_suppress_func: 'tpgresource.CompareSelfLinkCanonicalPaths'
255255
custom_flatten: 'templates/terraform/custom_flatten/guard_self_link.go.tmpl'
256256
- name: 'maxConnections'
257257
type: Integer

mmv1/third_party/terraform/services/compute/resource_compute_region_backend_service_test.go.tmpl

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -433,6 +433,286 @@ func TestAccComputeRegionBackendService_withLogConfig(t *testing.T) {
433433
})
434434
}
435435

436+
func TestAccComputeRegionBackendService_zonalILB(t *testing.T) {
437+
t.Parallel()
438+
439+
serviceName := fmt.Sprintf("tf-test-ilb-bs-%s", acctest.RandString(t, 10))
440+
checkName := fmt.Sprintf("tf-test-ilb-hc-%s", acctest.RandString(t, 10))
441+
checkName2 := fmt.Sprintf("tf-test-ilb-hc2-%s", acctest.RandString(t, 10))
442+
negName := fmt.Sprintf("tf-test-ilb-neg-%s", acctest.RandString(t, 10))
443+
negName2 := fmt.Sprintf("tf-test-ilb-neg2-%s", acctest.RandString(t, 10))
444+
instanceName := fmt.Sprintf("tf-test-ilb-vm-%s", acctest.RandString(t, 10))
445+
instanceName2 := fmt.Sprintf("tf-test-ilb-vm2-%s", acctest.RandString(t, 10))
446+
447+
// subnetwork with random suffix
448+
subnetName := fmt.Sprintf("tf-test-subnet-%s", acctest.RandString(t, 8))
449+
450+
acctest.VcrTest(t, resource.TestCase{
451+
PreCheck: func() { acctest.AccTestPreCheck(t) },
452+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
453+
CheckDestroy: testAccCheckComputeRegionBackendServiceDestroyProducer(t),
454+
Steps: []resource.TestStep{
455+
// STEP 1: base (self-link v1)
456+
{
457+
Config: testAccComputeRegionBackendService_zonalILB_withGroup(
458+
testAccComputeRegionBackendService_common(checkName, negName, instanceName, subnetName),
459+
serviceName,
460+
"google_compute_network_endpoint_group.neg.id",
461+
),
462+
},
463+
{
464+
ResourceName: "google_compute_region_backend_service.default",
465+
ImportState: true,
466+
ImportStateVerify: true,
467+
},
468+
469+
// STEP 2: same NEG with /compute/beta/ (apply OK)
470+
{
471+
Config: fmt.Sprintf(`
472+
%s
473+
474+
locals {
475+
neg_beta = replace(google_compute_network_endpoint_group.neg.id, "/compute/v1/", "/compute/beta/")
476+
}
477+
478+
%s
479+
`, testAccComputeRegionBackendService_common(checkName, negName, instanceName, subnetName),
480+
testAccComputeRegionBackendService_zonalILB_withGroup("", serviceName, "local.neg_beta"),
481+
),
482+
},
483+
{
484+
ResourceName: "google_compute_region_backend_service.default",
485+
ImportState: true,
486+
ImportStateVerify: true,
487+
},
488+
489+
// STEP 3: Invalid variation for API (UPPERCASE + "/") — tested only in PLAN
490+
{
491+
PlanOnly: true, // does not call the API; only exercises diff/canonicalization
492+
Config: fmt.Sprintf(`
493+
%s
494+
495+
locals {
496+
neg_slash_upper = "${google_compute_network_endpoint_group.neg.id}"
497+
}
498+
499+
%s
500+
`, testAccComputeRegionBackendService_common(checkName, negName, instanceName, subnetName),
501+
testAccComputeRegionBackendService_zonalILB_withGroup("", serviceName, "local.neg_slash_upper"),
502+
),
503+
},
504+
505+
// STEP 4: Modified scenario (changes NEG/HC/VM) — continues validating real updates
506+
{
507+
Config: testAccComputeRegionBackendService_zonalILBModified(serviceName, checkName, negName, instanceName, checkName2, negName2, instanceName2, subnetName),
508+
},
509+
{
510+
ResourceName: "google_compute_region_backend_service.default",
511+
ImportState: true,
512+
ImportStateVerify: true,
513+
},
514+
},
515+
})
516+
}
517+
518+
func testAccComputeRegionBackendService_common(checkName, negName, instanceName, subnetworkName string) string {
519+
return fmt.Sprintf(`
520+
resource "google_compute_network" "default" {
521+
name = "tf-test-net"
522+
auto_create_subnetworks = false
523+
}
524+
525+
resource "google_compute_subnetwork" "default" {
526+
name = "%s"
527+
ip_cidr_range = "10.10.0.0/16"
528+
region = "us-central1"
529+
network = google_compute_network.default.id
530+
}
531+
532+
resource "google_compute_region_health_check" "hc1" {
533+
name = "%s"
534+
region = "us-central1"
535+
http_health_check {
536+
port = 8080
537+
request_path = "/status"
538+
}
539+
}
540+
541+
resource "google_compute_instance" "default" {
542+
name = "%s"
543+
zone = "us-central1-a"
544+
machine_type = "e2-micro"
545+
546+
boot_disk {
547+
initialize_params {
548+
image = "debian-cloud/debian-11"
549+
}
550+
}
551+
552+
network_interface {
553+
network = google_compute_network.default.id
554+
subnetwork = google_compute_subnetwork.default.id
555+
access_config {}
556+
}
557+
}
558+
559+
resource "google_compute_network_endpoint_group" "neg" {
560+
name = "%s"
561+
network = google_compute_network.default.id
562+
subnetwork = google_compute_subnetwork.default.id
563+
zone = "us-central1-a"
564+
network_endpoint_type = "GCE_VM_IP_PORT"
565+
}
566+
567+
resource "google_compute_network_endpoint" "endpoint" {
568+
network_endpoint_group = google_compute_network_endpoint_group.neg.name
569+
zone = "us-central1-a"
570+
instance = google_compute_instance.default.name
571+
ip_address = google_compute_instance.default.network_interface[0].network_ip
572+
port = 8080
573+
}
574+
`, subnetworkName, checkName, instanceName, negName)
575+
}
576+
577+
func testAccComputeRegionBackendService_zonalILB_withGroup(commonHCL string, serviceName string, groupExpr string) string {
578+
header := commonHCL
579+
return fmt.Sprintf(`
580+
%s
581+
resource "google_compute_region_backend_service" "default" {
582+
name = "%s"
583+
region = "us-central1"
584+
protocol = "HTTP"
585+
load_balancing_scheme = "INTERNAL_MANAGED"
586+
health_checks = [google_compute_region_health_check.hc1.id]
587+
588+
backend {
589+
group = %s
590+
balancing_mode = "RATE"
591+
max_rate_per_endpoint = 100
592+
capacity_scaler = 1.0
593+
}
594+
595+
session_affinity = "CLIENT_IP"
596+
locality_lb_policy = "ROUND_ROBIN"
597+
}
598+
`, header, serviceName, groupExpr)
599+
}
600+
601+
func testAccComputeRegionBackendService_zonalILBModified(serviceName, checkName, negName, instanceName, checkName2, negName2, instanceName2, subnetworkName string) string {
602+
return fmt.Sprintf(`
603+
resource "google_compute_network" "default" {
604+
name = "tf-test-net"
605+
auto_create_subnetworks = false
606+
}
607+
608+
resource "google_compute_subnetwork" "default" {
609+
name = "%s"
610+
ip_cidr_range = "10.10.0.0/16"
611+
region = "us-central1"
612+
network = google_compute_network.default.id
613+
}
614+
615+
resource "google_compute_region_health_check" "hc1" {
616+
name = "%s"
617+
region = "us-central1"
618+
http_health_check {
619+
port = 8080
620+
request_path = "/status"
621+
}
622+
}
623+
624+
resource "google_compute_instance" "default" {
625+
name = "%s"
626+
zone = "us-central1-a"
627+
machine_type = "e2-micro"
628+
629+
boot_disk {
630+
initialize_params {
631+
image = "debian-cloud/debian-11"
632+
}
633+
}
634+
635+
network_interface {
636+
network = google_compute_network.default.id
637+
subnetwork = google_compute_subnetwork.default.id
638+
access_config {}
639+
}
640+
}
641+
642+
resource "google_compute_network_endpoint_group" "neg" {
643+
name = "%s"
644+
network = google_compute_network.default.id
645+
subnetwork = google_compute_subnetwork.default.id
646+
zone = "us-central1-a"
647+
network_endpoint_type = "GCE_VM_IP_PORT"
648+
}
649+
650+
resource "google_compute_network_endpoint" "endpoint" {
651+
network_endpoint_group = google_compute_network_endpoint_group.neg.name
652+
zone = "us-central1-a"
653+
instance = google_compute_instance.default.name
654+
ip_address = google_compute_instance.default.network_interface[0].network_ip
655+
port = 8080
656+
}
657+
658+
resource "google_compute_instance" "instance2" {
659+
name = "%s"
660+
zone = "us-central1-a"
661+
machine_type = "e2-micro"
662+
663+
boot_disk {
664+
initialize_params {
665+
image = "debian-cloud/debian-11"
666+
}
667+
}
668+
669+
network_interface {
670+
network = google_compute_network.default.id
671+
subnetwork = google_compute_subnetwork.default.id
672+
access_config {}
673+
}
674+
}
675+
676+
resource "google_compute_region_health_check" "hc2" {
677+
name = "%s"
678+
region = "us-central1"
679+
http_health_check {
680+
port = 80
681+
}
682+
}
683+
684+
resource "google_compute_network_endpoint_group" "neg2" {
685+
name = "%s"
686+
network = google_compute_network.default.id
687+
subnetwork = google_compute_subnetwork.default.id
688+
zone = "us-central1-a"
689+
network_endpoint_type = "GCE_VM_IP_PORT"
690+
}
691+
692+
resource "google_compute_network_endpoint" "endpoint2" {
693+
network_endpoint_group = google_compute_network_endpoint_group.neg2.name
694+
zone = "us-central1-a"
695+
instance = google_compute_instance.instance2.name
696+
ip_address = google_compute_instance.instance2.network_interface[0].network_ip
697+
port = 8080
698+
}
699+
700+
resource "google_compute_region_backend_service" "default" {
701+
name = "%s"
702+
region = "us-central1"
703+
load_balancing_scheme = "INTERNAL_MANAGED"
704+
health_checks = [google_compute_region_health_check.hc2.id]
705+
706+
backend {
707+
group = google_compute_network_endpoint_group.neg2.id
708+
balancing_mode = "RATE"
709+
max_rate_per_endpoint = 200
710+
capacity_scaler = 0.5
711+
}
712+
}
713+
`, subnetworkName, checkName, instanceName, negName, instanceName2, checkName2, negName2, serviceName)
714+
}
715+
436716
func TestAccComputeRegionBackendService_withDynamicBackendCount(t *testing.T) {
437717
t.Parallel()
438718

mmv1/third_party/terraform/tpgresource/self_link_helpers.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,40 @@ func CompareSelfLinkOrResourceName(_, old, new string, _ *schema.ResourceData) b
7373
return CompareSelfLinkRelativePaths("", old, new, nil)
7474
}
7575

76+
// canonicalizeSelfLink normalizes Compute API self-links by removing the version prefix (v1/beta),
77+
// ensuring a leading "/", collapsing duplicate slashes, trimming any trailing "/",
78+
// and lowercasing the result so logically identical links compare equal.
79+
func CompareSelfLinkCanonicalPaths(_, old, new string, _ *schema.ResourceData) bool {
80+
return canonicalizeSelfLink(old) == canonicalizeSelfLink(new)
81+
}
82+
83+
var (
84+
rePrefix = regexp.MustCompile(`(?i)^https?://[a-z0-9.-]*/compute/(v1|beta)/`)
85+
reDuplicateSlashes = regexp.MustCompile(`/+`)
86+
)
87+
88+
func canonicalizeSelfLink(link string) string {
89+
if link == "" {
90+
return ""
91+
}
92+
93+
// Remove "https://…/compute/v1/" or "https://…/compute/beta/"
94+
path := rePrefix.ReplaceAllString(link, "/")
95+
96+
// Ensure leading "/"
97+
if !strings.HasPrefix(path, "/") {
98+
path = "/" + path
99+
}
100+
101+
// Collapse "//"
102+
path = reDuplicateSlashes.ReplaceAllString(path, "/")
103+
104+
// Remove trailing "/"
105+
path = strings.TrimSuffix(path, "/")
106+
107+
return strings.ToLower(path)
108+
}
109+
76110
// Hash the relative path of a self link.
77111
func SelfLinkRelativePathHash(selfLink interface{}) int {
78112
path, _ := GetRelativePath(selfLink.(string))

0 commit comments

Comments
 (0)