Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,4 @@ charts/

# Ignore asdf .tool-versions file
.tool-versions
go1.26.3.linux-amd64.tar.gz
6 changes: 6 additions & 0 deletions cloudmock/gce/mockcompute/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type MockClient struct {

instanceTemplateClient *instanceTemplateClient
instanceGroupManagerClient *instanceGroupManagerClient
regionInstanceGroupManagerClient *regionInstanceGroupManagerClient
targetPoolClient *targetPoolClient

diskClient *diskClient
Expand Down Expand Up @@ -70,6 +71,7 @@ func NewMockClient(project string) *MockClient {

instanceTemplateClient: newInstanceTemplateClient(),
instanceGroupManagerClient: newInstanceGroupManagerClient(instanceClient),
regionInstanceGroupManagerClient: newRegionInstanceGroupManagerClient(),
targetPoolClient: newTargetPoolClient(),

diskClient: newDiskClient(),
Expand Down Expand Up @@ -173,6 +175,10 @@ func (c *MockClient) InstanceGroupManagers() gce.InstanceGroupManagerClient {
return c.instanceGroupManagerClient
}

func (c *MockClient) RegionInstanceGroupManagers() gce.RegionInstanceGroupManagerClient {
return c.regionInstanceGroupManagerClient
}

func (c *MockClient) TargetPools() gce.TargetPoolClient {
return c.targetPoolClient
}
Expand Down
138 changes: 138 additions & 0 deletions cloudmock/gce/mockcompute/region_instance_group_manager.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
/*
Copyright 2026 The Kubernetes Authors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package mockcompute

import (
"context"
"fmt"
"sync"

compute "google.golang.org/api/compute/v1"
"k8s.io/kops/upup/pkg/fi/cloudup/gce"
)

type regionInstanceGroupManagerClient struct {
// regionInstanceGroupManagers keyed by project, region, and name.
regionInstanceGroupManagers map[string]map[string]map[string]*compute.InstanceGroupManager
sync.Mutex
}

var _ gce.RegionInstanceGroupManagerClient = &regionInstanceGroupManagerClient{}

func newRegionInstanceGroupManagerClient() *regionInstanceGroupManagerClient {
return &regionInstanceGroupManagerClient{
regionInstanceGroupManagers: map[string]map[string]map[string]*compute.InstanceGroupManager{},
}
}

func (c *regionInstanceGroupManagerClient) All() map[string]interface{} {
c.Lock()
defer c.Unlock()
m := map[string]interface{}{}
for _, regions := range c.regionInstanceGroupManagers {
for _, igms := range regions {
for n, igm := range igms {
m[n] = igm
}
}
}
return m
}

func (c *regionInstanceGroupManagerClient) Insert(project, region string, igm *compute.InstanceGroupManager) (*compute.Operation, error) {
c.Lock()
defer c.Unlock()
igmRegions, ok := c.regionInstanceGroupManagers[project]
if !ok {
igmRegions = map[string]map[string]*compute.InstanceGroupManager{}
c.regionInstanceGroupManagers[project] = igmRegions
}
igms, ok := igmRegions[region]
if !ok {
igms = map[string]*compute.InstanceGroupManager{}
igmRegions[region] = igms
}
igm.SelfLink = fmt.Sprintf("https://www.googleapis.com/compute/v1/projects/%s/regions/%s/instanceGroupManagers/%s", project, region, igm.Name)
igms[igm.Name] = igm
return doneOperation(), nil
}

func (c *regionInstanceGroupManagerClient) Delete(project, region, name string) (*compute.Operation, error) {
c.Lock()
defer c.Unlock()
regions, ok := c.regionInstanceGroupManagers[project]
if !ok {
return nil, notFoundError()
}
igms, ok := regions[region]
if !ok {
return nil, notFoundError()
}
if _, ok := igms[name]; !ok {
return nil, notFoundError()
}
delete(igms, name)
return doneOperation(), nil
}

func (c *regionInstanceGroupManagerClient) Get(project, region, name string) (*compute.InstanceGroupManager, error) {
c.Lock()
defer c.Unlock()
regions, ok := c.regionInstanceGroupManagers[project]
if !ok {
return nil, notFoundError()
}
igms, ok := regions[region]
if !ok {
return nil, notFoundError()
}
igm, ok := igms[name]
if !ok {
return nil, notFoundError()
}
return igm, nil
}

func (c *regionInstanceGroupManagerClient) List(ctx context.Context, project, region string) ([]*compute.InstanceGroupManager, error) {
c.Lock()
defer c.Unlock()
regions, ok := c.regionInstanceGroupManagers[project]
if !ok {
return nil, nil
}
igms, ok := regions[region]
if !ok {
return nil, nil
}
var l []*compute.InstanceGroupManager
for _, d := range igms {
l = append(l, d)
}
return l, nil
}

func (c *regionInstanceGroupManagerClient) SetTargetPools(project, region, name string, targetPools []string) (*compute.Operation, error) {
return doneOperation(), nil
}

func (c *regionInstanceGroupManagerClient) SetInstanceTemplate(project, region, name, instanceTemplateURL string) (*compute.Operation, error) {
return doneOperation(), nil
}

func (c *regionInstanceGroupManagerClient) Resize(project, region, name string, newSize int64) (*compute.Operation, error) {
return doneOperation(), nil
}
1 change: 1 addition & 0 deletions pkg/featureflag/featureflag.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ var (
SpotinstController = new("SpotinstController", Bool(true))
// VPCSkipEnableDNSSupport if set will make that a VPC does not need DNSSupport enabled.
VPCSkipEnableDNSSupport = new("VPCSkipEnableDNSSupport", Bool(false))
GCERegionalMIG = new("GCERegionalMIG", Bool(false))
// SkipEtcdVersionCheck will bypass the check that etcd-manager is using a supported etcd version
SkipEtcdVersionCheck = new("SkipEtcdVersionCheck", Bool(false))
// EtcdEventsHTTP enables HTTP (non-TLS) for the events etcd cluster.
Expand Down
65 changes: 55 additions & 10 deletions pkg/model/gcemodel/autoscalinggroup.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (

"k8s.io/klog/v2"
"k8s.io/kops/pkg/apis/kops"
"k8s.io/kops/pkg/featureflag"
"k8s.io/kops/pkg/model"
"k8s.io/kops/pkg/model/defaults"
"k8s.io/kops/pkg/model/iam"
Expand Down Expand Up @@ -334,26 +335,34 @@ func (b *AutoscalingGroupModelBuilder) Build(c *fi.CloudupModelBuilderContext) e
}
c.AddTask(instanceTemplate)

instanceCountByZone, err := b.splitToZones(ig)
if err != nil {
return err
}
if featureflag.GCERegionalMIG.Enabled() {
// Use a single regional MIG spanning all zones
zones, err := b.FindZonesForInstanceGroup(ig)
if err != nil {
return err
}

for zone, targetSize := range instanceCountByZone {
name := gce.NameForInstanceGroupManager(b.Cluster.ObjectMeta.Name, ig.ObjectMeta.Name, zone)
minSize := 1
if ig.Spec.MinSize != nil {
minSize = int(fi.ValueOf(ig.Spec.MinSize))
} else if ig.Spec.Role == kops.InstanceGroupRoleNode {
minSize = 2
}

name := gce.NameForRegionInstanceGroupManager(b.Cluster.ObjectMeta.Name, ig.ObjectMeta.Name)

t := &gcetasks.InstanceGroupManager{
t := &gcetasks.RegionInstanceGroupManager{
Name: s(name),
Lifecycle: b.Lifecycle,
Zone: s(zone),
TargetSize: fi.PtrTo(int64(targetSize)),
Region: s(b.Region),
TargetSize: fi.PtrTo(int64(minSize)),
UpdatePolicy: &gcetasks.UpdatePolicy{MinimalAction: "REPLACE", Type: "OPPORTUNISTIC"},
BaseInstanceName: s(ig.ObjectMeta.Name),
InstanceTemplate: instanceTemplate,
ListManagedInstancesResults: "PAGINATED",
DistributionPolicyZones: zones,
}

// Attach API server instances to load balancer if we're using one
if ig.HasAPIServer() {
if b.UseLoadBalancerForAPI() {
lbSpec := b.Cluster.Spec.API.LoadBalancer
Expand All @@ -369,6 +378,42 @@ func (b *AutoscalingGroupModelBuilder) Build(c *fi.CloudupModelBuilderContext) e
}

c.AddTask(t)
} else {
instanceCountByZone, err := b.splitToZones(ig)
if err != nil {
return err
}

for zone, targetSize := range instanceCountByZone {
name := gce.NameForInstanceGroupManager(b.Cluster.ObjectMeta.Name, ig.ObjectMeta.Name, zone)

t := &gcetasks.InstanceGroupManager{
Name: s(name),
Lifecycle: b.Lifecycle,
Zone: s(zone),
TargetSize: fi.PtrTo(int64(targetSize)),
UpdatePolicy: &gcetasks.UpdatePolicy{MinimalAction: "REPLACE", Type: "OPPORTUNISTIC"},
BaseInstanceName: s(ig.ObjectMeta.Name),
InstanceTemplate: instanceTemplate,
ListManagedInstancesResults: "PAGINATED",
}

if ig.HasAPIServer() {
if b.UseLoadBalancerForAPI() {
lbSpec := b.Cluster.Spec.API.LoadBalancer
if lbSpec != nil {
switch lbSpec.Type {
case kops.LoadBalancerTypePublic:
t.TargetPools = append(t.TargetPools, b.LinkToTargetPool("api"))
case kops.LoadBalancerTypeInternal:
klog.Warningf("Not hooking the instance group manager up to anything.")
}
}
}
}

c.AddTask(t)
}
}
}

Expand Down
65 changes: 65 additions & 0 deletions upup/pkg/fi/cloudup/gce/compute.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ type ComputeClient interface {
Instances() InstanceClient
InstanceTemplates() InstanceTemplateClient
InstanceGroupManagers() InstanceGroupManagerClient
RegionInstanceGroupManagers() RegionInstanceGroupManagerClient
TargetPools() TargetPoolClient
Disks() DiskClient
RegionBackendServices() RegionBackendServiceClient
Expand Down Expand Up @@ -843,3 +844,67 @@ func (c *diskClientImpl) SetLabels(project, zone, name string, req *compute.Zone
_, err := c.srv.SetLabels(project, zone, name, req).Do()
return err
}

// RegionInstanceGroupManagerClient wraps the GCE RegionInstanceGroupManagers API.
type RegionInstanceGroupManagerClient interface {
Insert(project, region string, i *compute.InstanceGroupManager) (*compute.Operation, error)
Delete(project, region, name string) (*compute.Operation, error)
Get(project, region, name string) (*compute.InstanceGroupManager, error)
List(ctx context.Context, project, region string) ([]*compute.InstanceGroupManager, error)
SetTargetPools(project, region, name string, targetPools []string) (*compute.Operation, error)
SetInstanceTemplate(project, region, name, instanceTemplateURL string) (*compute.Operation, error)
Resize(project, region, name string, newSize int64) (*compute.Operation, error)
}

type regionInstanceGroupManagerClientImpl struct {
srv *compute.RegionInstanceGroupManagersService
}

var _ RegionInstanceGroupManagerClient = (*regionInstanceGroupManagerClientImpl)(nil)

func (c *regionInstanceGroupManagerClientImpl) Insert(project, region string, i *compute.InstanceGroupManager) (*compute.Operation, error) {
return c.srv.Insert(project, region, i).Do()
}

func (c *regionInstanceGroupManagerClientImpl) Delete(project, region, name string) (*compute.Operation, error) {
return c.srv.Delete(project, region, name).Do()
}

func (c *regionInstanceGroupManagerClientImpl) Get(project, region, name string) (*compute.InstanceGroupManager, error) {
return c.srv.Get(project, region, name).Do()
}

func (c *regionInstanceGroupManagerClientImpl) List(ctx context.Context, project, region string) ([]*compute.InstanceGroupManager, error) {
var ms []*compute.InstanceGroupManager
if err := c.srv.List(project, region).Pages(ctx, func(page *compute.RegionInstanceGroupManagerList) error {
ms = append(ms, page.Items...)
return nil
}); err != nil {
return nil, err
}
return ms, nil
}

func (c *regionInstanceGroupManagerClientImpl) SetTargetPools(project, region, name string, targetPools []string) (*compute.Operation, error) {
req := &compute.RegionInstanceGroupManagersSetTargetPoolsRequest{
TargetPools: targetPools,
}
return c.srv.SetTargetPools(project, region, name, req).Do()
}

func (c *regionInstanceGroupManagerClientImpl) SetInstanceTemplate(project, region, name, instanceTemplateURL string) (*compute.Operation, error) {
req := &compute.RegionInstanceGroupManagersSetTemplateRequest{
InstanceTemplate: instanceTemplateURL,
}
return c.srv.SetInstanceTemplate(project, region, name, req).Do()
}

func (c *regionInstanceGroupManagerClientImpl) Resize(project, region, name string, newSize int64) (*compute.Operation, error) {
return c.srv.Resize(project, region, name, newSize).Do()
}

func (c *computeClientImpl) RegionInstanceGroupManagers() RegionInstanceGroupManagerClient {
return &regionInstanceGroupManagerClientImpl{
srv: c.srv.RegionInstanceGroupManagers,
}
}
6 changes: 6 additions & 0 deletions upup/pkg/fi/cloudup/gce/instancegroups.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,3 +322,9 @@ func addCloudInstanceData(cm *cloudinstances.CloudInstance, instance *compute.In
}
}
}

// NameForRegionInstanceGroupManager builds a name for a regional InstanceGroupManager.
func NameForRegionInstanceGroupManager(clusterName, instanceGroupName string) string {
name := SafeObjectName(instanceGroupName, clusterName)
return name
}
Loading