Skip to content

Commit b690f2a

Browse files
gce: dynamically refresh managed zones on resource cache miss and periodic sync (#1328)
Fixes dynamic zone discovery in external CCM mode where newly created nodes register without topology zone labels, preventing node informer event handlers from detecting unmanaged zones during initial bootstrap. 1. Trigger on-demand dynamic zone refresh on cache miss in getInstanceByName, getFoundInstanceByNames, and GetDiskByNameUnknownZone. 2. Support ProviderID fallback in getZone for uninitialized nodes. 3. Spawn a periodic background sync in Initialize to discover newly allowlisted zones proactively. 4. Add comprehensive unit tests covering uninitialized node metadata lookup, single/batch instance lookup, and disk lookup. BUG=516475109 TAG=agy CONV=b9619406-e590-4c1f-8747-80bf30fe7633 Co-authored-by: Arvind Bright <arvindbright@google.com>
1 parent b65c88f commit b690f2a

5 files changed

Lines changed: 344 additions & 38 deletions

File tree

providers/gce/gce.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ import (
4545

4646
v1 "k8s.io/api/core/v1"
4747
"k8s.io/apimachinery/pkg/util/sets"
48+
"k8s.io/apimachinery/pkg/util/wait"
4849
"k8s.io/client-go/informers"
4950
clientset "k8s.io/client-go/kubernetes"
5051
"k8s.io/client-go/kubernetes/scheme"
@@ -781,6 +782,9 @@ func (g *Cloud) Initialize(clientBuilder cloudprovider.ControllerClientBuilder,
781782

782783
go g.watchClusterID(stop)
783784
go g.metricsCollector.Run(stop)
785+
if g.dynamicZones {
786+
go g.syncManagedZonesPeriodically(stop)
787+
}
784788
}
785789

786790
// LoadBalancer returns an implementation of LoadBalancer for Google Compute Engine.
@@ -1128,3 +1132,11 @@ func (g *Cloud) refreshManagedZones() error {
11281132

11291133
return nil
11301134
}
1135+
1136+
func (g *Cloud) syncManagedZonesPeriodically(stop <-chan struct{}) {
1137+
wait.Until(func() {
1138+
if err := g.refreshManagedZones(); err != nil {
1139+
klog.Errorf("Periodic refresh of GCE managed zones failed: %v", err)
1140+
}
1141+
}, 5*time.Minute, stop)
1142+
}

providers/gce/gce_disks.go

Lines changed: 48 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -929,33 +929,13 @@ func (g *Cloud) getRegionalDiskByName(diskName string) (*Disk, error) {
929929
return disk, err
930930
}
931931

932-
// GetDiskByNameUnknownZone scans all managed zones to return the GCE PD
933-
// Prefer getDiskByName, if the zone can be established
934-
// Return cloudprovider.DiskNotFound if the given disk cannot be found in any zone
935-
func (g *Cloud) GetDiskByNameUnknownZone(diskName string) (*Disk, error) {
936-
regionalDisk, err := g.getRegionalDiskByName(diskName)
937-
if err == nil {
938-
return regionalDisk, err
939-
}
940-
941-
// Note: this is the gotcha right now with GCE PD support:
942-
// disk names are not unique per-region.
943-
// (I can create two volumes with name "myvol" in e.g. us-central1-b & us-central1-f)
944-
// For now, this is simply undefined behaviour.
945-
//
946-
// In future, we will have to require users to qualify their disk
947-
// "us-central1-a/mydisk". We could do this for them as part of
948-
// admission control, but that might be a little weird (values changing
949-
// on create)
950-
932+
func (g *Cloud) findDiskInZones(diskName string, zones []string) (*Disk, error) {
951933
var found *Disk
952-
for _, zone := range g.getManagedZones() {
934+
for _, zone := range zones {
953935
disk, err := g.findDiskByName(diskName, zone)
954936
if err != nil {
955937
return nil, err
956938
}
957-
// findDiskByName returns (nil,nil) if the disk doesn't exist, so we can't
958-
// assume that a disk was found unless disk is non-nil.
959939
if disk == nil {
960940
continue
961941
}
@@ -974,9 +954,55 @@ func (g *Cloud) GetDiskByNameUnknownZone(diskName string) (*Disk, error) {
974954
}
975955
found = disk
976956
}
957+
return found, nil
958+
}
959+
960+
// GetDiskByNameUnknownZone scans all managed zones to return the GCE PD
961+
// Prefer getDiskByName, if the zone can be established
962+
// Return cloudprovider.DiskNotFound if the given disk cannot be found in any zone
963+
func (g *Cloud) GetDiskByNameUnknownZone(diskName string) (*Disk, error) {
964+
regionalDisk, err := g.getRegionalDiskByName(diskName)
965+
if err == nil {
966+
return regionalDisk, err
967+
}
968+
969+
// Note: this is the gotcha right now with GCE PD support:
970+
// disk names are not unique per-region.
971+
// (I can create two volumes with name "myvol" in e.g. us-central1-b & us-central1-f)
972+
// For now, this is simply undefined behaviour.
973+
//
974+
// In future, we will have to require users to qualify their disk
975+
// "us-central1-a/mydisk". We could do this for them as part of
976+
// admission control, but that might be a little weird (values changing
977+
// on create)
978+
979+
initialZones := g.getManagedZones()
980+
found, err := g.findDiskInZones(diskName, initialZones)
981+
if err != nil {
982+
return nil, err
983+
}
977984
if found != nil {
978985
return found, nil
979986
}
987+
988+
if g.dynamicZones {
989+
if err := g.refreshManagedZones(); err != nil {
990+
klog.Errorf("Failed to refresh GCE managed zones for disk %s: %v", diskName, err)
991+
} else {
992+
refreshedZones := g.getManagedZones()
993+
newZones := sets.NewString(refreshedZones...).Difference(sets.NewString(initialZones...)).List()
994+
if len(newZones) > 0 {
995+
found, err := g.findDiskInZones(diskName, newZones)
996+
if err != nil {
997+
return nil, err
998+
}
999+
if found != nil {
1000+
return found, nil
1001+
}
1002+
}
1003+
}
1004+
}
1005+
9801006
klog.Warningf("GCE persistent disk %q not found in managed zones (%s)",
9811007
diskName, strings.Join(g.getManagedZones(), ","))
9821008

providers/gce/gce_disks_test.go

Lines changed: 45 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,18 +21,19 @@ package gce
2121

2222
import (
2323
"context"
24-
"testing"
25-
2624
"fmt"
25+
"net/http"
26+
"testing"
2727

28+
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud"
29+
"github.com/GoogleCloudPlatform/k8s-cloud-provider/pkg/cloud/meta"
2830
computealpha "google.golang.org/api/compute/v0.alpha"
2931
computebeta "google.golang.org/api/compute/v0.beta"
3032
compute "google.golang.org/api/compute/v1"
3133
"google.golang.org/api/googleapi"
3234
v1 "k8s.io/api/core/v1"
3335
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
3436
"k8s.io/apimachinery/pkg/util/sets"
35-
cloudprovider "k8s.io/cloud-provider"
3637
)
3738

3839
// TODO TODO write a test for GetDiskByNameUnknownZone and make sure casting logic works
@@ -916,7 +917,7 @@ func (manager *FakeServiceManager) GetDiskFromCloudProvider(
916917
zone string, diskName string) (*Disk, error) {
917918

918919
if manager.zonalDisks[zone] == "" {
919-
return nil, cloudprovider.DiskNotFound
920+
return nil, &googleapi.Error{Code: http.StatusNotFound}
920921
}
921922

922923
if manager.resourceInUse {
@@ -941,7 +942,7 @@ func (manager *FakeServiceManager) GetRegionalDiskFromCloudProvider(
941942
diskName string) (*Disk, error) {
942943

943944
if _, ok := manager.regionalDisks[diskName]; !ok {
944-
return nil, cloudprovider.DiskNotFound
945+
return nil, &googleapi.Error{Code: http.StatusNotFound}
945946
}
946947

947948
if manager.resourceInUse {
@@ -1019,3 +1020,42 @@ func createNodeZones(zones []string) map[string]sets.String {
10191020
}
10201021
return nodeZones
10211022
}
1023+
1024+
func TestGetDiskByNameUnknownZone_DynamicRefresh(t *testing.T) {
1025+
gceProjectID := "test-project"
1026+
gceRegion := "us-central1"
1027+
fakeManager := newFakeManager(gceProjectID, gceRegion)
1028+
fakeManager.zonalDisks["us-central1-c"] = "my-disk"
1029+
1030+
gce := Cloud{
1031+
manager: fakeManager,
1032+
managedZones: []string{"us-central1-b"},
1033+
projectID: gceProjectID,
1034+
region: gceRegion,
1035+
dynamicZones: true,
1036+
}
1037+
1038+
mockGCE := cloud.NewMockGCE(&gceProjectRouter{&gce})
1039+
keyC := meta.GlobalKey("key-c")
1040+
mockGCE.MockZones.Objects[*keyC] = &cloud.MockZonesObj{
1041+
Obj: &compute.Zone{Name: "us-central1-c", Region: gce.getRegionLink("us-central1")},
1042+
}
1043+
keyB := meta.GlobalKey("key-b")
1044+
mockGCE.MockZones.Objects[*keyB] = &cloud.MockZonesObj{
1045+
Obj: &compute.Zone{Name: "us-central1-b", Region: gce.getRegionLink("us-central1")},
1046+
}
1047+
gce.c = mockGCE
1048+
1049+
disk, err := gce.GetDiskByNameUnknownZone("my-disk")
1050+
if err != nil {
1051+
t.Fatalf("unexpected error: %v", err)
1052+
}
1053+
if disk == nil || disk.Name != "my-disk" {
1054+
t.Fatalf("expected disk my-disk, got %v", disk)
1055+
}
1056+
1057+
expectedZones := []string{"us-central1-b", "us-central1-c"}
1058+
if !sets.NewString(gce.getManagedZones()...).Equal(sets.NewString(expectedZones...)) {
1059+
t.Fatalf("expected managed zones %v, got %v", expectedZones, gce.getManagedZones())
1060+
}
1061+
}

providers/gce/gce_instances.go

Lines changed: 82 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,17 @@ func getZone(node *v1.Node) string {
7272
return zone
7373
}
7474
zone, ok = node.Labels[v1.LabelFailureDomainBetaZone]
75-
if !ok {
76-
klog.Warningf("Node without zone label, returning %q as zone. Node name: %v, node labels: %v", emptyZone, node.Name, node.Labels)
77-
return emptyZone
75+
if ok {
76+
return zone
7877
}
79-
return zone
78+
if node.Spec.ProviderID != "" {
79+
_, zone, _, err := splitProviderID(node.Spec.ProviderID)
80+
if err == nil && zone != "" {
81+
return zone
82+
}
83+
}
84+
klog.Warningf("Node without zone label or providerID, returning %q as zone. Node name: %v, node labels: %v", emptyZone, node.Name, node.Labels)
85+
return emptyZone
8086
}
8187

8288
func makeHostURL(projectsAPIEndpoint, projectID, zone, host string) string {
@@ -715,7 +721,8 @@ func (g *Cloud) getFoundInstanceByNames(names []string) ([]*gceInstance, error)
715721
found[name] = nil
716722
}
717723

718-
for _, zone := range g.getManagedZones() {
724+
initialZones := g.getManagedZones()
725+
for _, zone := range initialZones {
719726
if remaining == 0 {
720727
break
721728
}
@@ -745,6 +752,47 @@ func (g *Cloud) getFoundInstanceByNames(names []string) ([]*gceInstance, error)
745752
}
746753
}
747754

755+
if remaining > 0 && g.dynamicZones {
756+
if err := g.refreshManagedZones(); err != nil {
757+
klog.Errorf("Failed to refresh GCE managed zones for remaining instances: %v", err)
758+
} else {
759+
refreshedZones := g.getManagedZones()
760+
checkedZones := sets.NewString(initialZones...)
761+
for _, zone := range refreshedZones {
762+
if remaining == 0 {
763+
break
764+
}
765+
if checkedZones.Has(zone) {
766+
continue
767+
}
768+
instances, err := g.c.Instances().List(ctx, zone, filter.Regexp("name", nodeInstancePrefix+".*"))
769+
if err != nil {
770+
return nil, err
771+
}
772+
for _, inst := range instances {
773+
if remaining == 0 {
774+
break
775+
}
776+
if _, ok := found[inst.Name]; !ok {
777+
continue
778+
}
779+
if found[inst.Name] != nil {
780+
klog.Errorf("Instance name %q was duplicated (in zone %q and %q)", inst.Name, zone, found[inst.Name].Zone)
781+
continue
782+
}
783+
found[inst.Name] = &gceInstance{
784+
Zone: zone,
785+
Name: inst.Name,
786+
ID: inst.Id,
787+
Disks: inst.Disks,
788+
Type: lastComponent(inst.MachineType),
789+
}
790+
remaining--
791+
}
792+
}
793+
}
794+
}
795+
748796
var ret []*gceInstance
749797
var failed []string
750798
for name, instance := range found {
@@ -761,12 +809,8 @@ func (g *Cloud) getFoundInstanceByNames(names []string) ([]*gceInstance, error)
761809
return ret, nil
762810
}
763811

764-
// Gets the named instance, returning cloudprovider.InstanceNotFound if the instance is not found
765-
func (g *Cloud) getInstanceByName(name string) (*gceInstance, error) {
766-
klog.Infof("Searching node %s in managed zones %v", name, g.getManagedZones())
767-
768-
// Avoid changing behaviour when not managing multiple zones
769-
for _, zone := range g.getManagedZones() {
812+
func (g *Cloud) findInstanceInZones(name string, zones []string) (*gceInstance, error) {
813+
for _, zone := range zones {
770814
instance, err := g.getInstanceFromProjectInZoneByName(g.projectID, zone, name)
771815
if err != nil {
772816
if isHTTPErrorCode(err, http.StatusNotFound) {
@@ -777,6 +821,33 @@ func (g *Cloud) getInstanceByName(name string) (*gceInstance, error) {
777821
}
778822
return instance, nil
779823
}
824+
return nil, nil
825+
}
826+
827+
// Gets the named instance, returning cloudprovider.InstanceNotFound if the instance is not found
828+
func (g *Cloud) getInstanceByName(name string) (*gceInstance, error) {
829+
initialZones := g.getManagedZones()
830+
klog.Infof("Searching node %s in managed zones %v", name, initialZones)
831+
832+
if instance, err := g.findInstanceInZones(name, initialZones); err != nil || instance != nil {
833+
return instance, err
834+
}
835+
836+
if g.dynamicZones {
837+
klog.Infof("Node %s not found in managed zones %v; attempting dynamic zone refresh", name, initialZones)
838+
if err := g.refreshManagedZones(); err != nil {
839+
klog.Errorf("Failed to refresh GCE managed zones: %v", err)
840+
} else {
841+
refreshedZones := g.getManagedZones()
842+
newZones := sets.NewString(refreshedZones...).Difference(sets.NewString(initialZones...)).List()
843+
if len(newZones) > 0 {
844+
klog.Infof("Searching node %s in newly discovered zones %v", name, newZones)
845+
if instance, err := g.findInstanceInZones(name, newZones); err != nil || instance != nil {
846+
return instance, err
847+
}
848+
}
849+
}
850+
}
780851

781852
return nil, cloudprovider.InstanceNotFound
782853
}

0 commit comments

Comments
 (0)