From 1cd85055f5fe46fdc8ca2f6a3a22dc9b697788b3 Mon Sep 17 00:00:00 2001 From: Alexandre Barone Date: Fri, 14 Nov 2025 17:01:02 -0500 Subject: [PATCH] gcemodel: Add support for forwarding cloud labels to instance group Signed-off-by: Alexandre Barone --- pkg/model/gcemodel/autoscalinggroup.go | 16 +-- pkg/model/gcemodel/context.go | 26 ++++ pkg/model/gcemodel/context_test.go | 158 +++++++++++++++++++++++++ 3 files changed, 185 insertions(+), 15 deletions(-) create mode 100644 pkg/model/gcemodel/context_test.go diff --git a/pkg/model/gcemodel/autoscalinggroup.go b/pkg/model/gcemodel/autoscalinggroup.go index 44d7ea32d59cb..6184132253086 100644 --- a/pkg/model/gcemodel/autoscalinggroup.go +++ b/pkg/model/gcemodel/autoscalinggroup.go @@ -223,16 +223,6 @@ func (b *AutoscalingGroupModelBuilder) buildInstanceTemplate(c *fi.CloudupModelB case kops.InstanceGroupRoleBastion: t.Tags = append(t.Tags, b.GCETagForRole(kops.InstanceGroupRoleBastion)) } - clusterLabel := gce.LabelForCluster(b.ClusterName()) - roleLabel := gce.GceLabelNameRolePrefix + ig.Spec.Role.ToLowerString() - t.Labels = map[string]string{ - clusterLabel.Key: clusterLabel.Value, - roleLabel: ig.Spec.Role.ToLowerString(), - gce.GceLabelNameInstanceGroup: ig.ObjectMeta.Name, - } - if ig.Spec.Role == kops.InstanceGroupRoleControlPlane { - t.Labels[gce.GceLabelNameRolePrefix+"master"] = "master" - } if gce.UsesIPAliases(b.Cluster) { t.CanIPForward = fi.PtrTo(false) @@ -251,11 +241,7 @@ func (b *AutoscalingGroupModelBuilder) buildInstanceTemplate(c *fi.CloudupModelB t.ServiceAccounts = append(t.ServiceAccounts, b.LinkToServiceAccount(ig)) - // labels, err := b.CloudTagsForInstanceGroup(ig) - // if err != nil { - // return fmt.Errorf("error building cloud tags: %v", err) - // } - // t.Labels = labels + t.Labels = b.CloudTagsForInstanceGroup(ig) t.GuestAccelerators = []gcetasks.AcceleratorConfig{} for _, accelerator := range ig.Spec.GuestAccelerators { diff --git a/pkg/model/gcemodel/context.go b/pkg/model/gcemodel/context.go index 6aae41567fa00..65d2e49c6d89b 100644 --- a/pkg/model/gcemodel/context.go +++ b/pkg/model/gcemodel/context.go @@ -17,6 +17,8 @@ limitations under the License. package gcemodel import ( + "maps" + "k8s.io/klog/v2" "k8s.io/kops/pkg/apis/kops" "k8s.io/kops/pkg/model" @@ -176,3 +178,27 @@ func (c *GCEModelContext) LinkToServiceAccount(ig *kops.InstanceGroup) *gcetasks return &gcetasks.ServiceAccount{Name: s(name), Email: s(email)} } + +// CloudTagsForInstanceGroup computes the tags to apply to instances in the specified InstanceGroup. +// Keep the same func name as other models for consistency even though GCE instance templates use labels. +func (c *GCEModelContext) CloudTagsForInstanceGroup(ig *kops.InstanceGroup) map[string]string { + labels := make(map[string]string) + + // Apply any user-specified global labels first so they can be overridden by IG-specific labels + maps.Copy(labels, c.Cluster.Spec.CloudLabels) + + // Apply any user-specified labels + maps.Copy(labels, ig.Spec.CloudLabels) + + clusterLabel := gce.LabelForCluster(c.ClusterName()) + roleLabel := gce.GceLabelNameRolePrefix + ig.Spec.Role.ToLowerString() + + labels[clusterLabel.Key] = clusterLabel.Value + labels[roleLabel] = ig.Spec.Role.ToLowerString() + labels[gce.GceLabelNameInstanceGroup] = ig.ObjectMeta.Name + if ig.Spec.Role == kops.InstanceGroupRoleControlPlane { + labels[gce.GceLabelNameRolePrefix+"master"] = "master" + } + + return labels +} diff --git a/pkg/model/gcemodel/context_test.go b/pkg/model/gcemodel/context_test.go new file mode 100644 index 0000000000000..cb2499be33f5a --- /dev/null +++ b/pkg/model/gcemodel/context_test.go @@ -0,0 +1,158 @@ +/* +Copyright 2020 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 gcemodel + +import ( + "reflect" + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/kops/pkg/apis/kops" + "k8s.io/kops/pkg/model" + "k8s.io/kops/pkg/model/iam" + "k8s.io/kops/upup/pkg/fi" +) + +func TestCloudTagsForInstanceGroup(t *testing.T) { + c := newTestGCEModelContext() + c.Cluster.Spec.CloudLabels = map[string]string{ + "cluster_label_key": "cluster_label_value", + "test_label": "from_cluster", + } + igCP := c.InstanceGroups[0] + igCP.Spec.CloudLabels = map[string]string{ + "ig_cp_label_key": "ig_cp_label_value", + "test_label": "from_ig_cp", + } + igWorkers := c.InstanceGroups[1] + igWorkers.Spec.CloudLabels = map[string]string{ + "ig_workers_label_key": "ig_workers_label_value", + "test_label": "from_ig_workers", + } + + t.Run("control-plane", func(t *testing.T) { + actual := c.CloudTagsForInstanceGroup(igCP) + expected := map[string]string{ + "cluster_label_key": "cluster_label_value", + "ig_cp_label_key": "ig_cp_label_value", + "test_label": "from_ig_cp", + + "k8s-io-cluster-name": "testcluster-test-com", + "k8s-io-role-control-plane": "control-plane", + "k8s-io-instance-group": "cp-nodes", + "k8s-io-role-master": "master", + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected control-plane tags %+v, but got %+v", expected, actual) + } + }) + t.Run("workers", func(t *testing.T) { + actual := c.CloudTagsForInstanceGroup(igWorkers) + expected := map[string]string{ + "cluster_label_key": "cluster_label_value", + "ig_workers_label_key": "ig_workers_label_value", + "test_label": "from_ig_workers", + + "k8s-io-cluster-name": "testcluster-test-com", + "k8s-io-role-node": "node", + "k8s-io-instance-group": "worker-nodes", + } + + if !reflect.DeepEqual(actual, expected) { + t.Errorf("expected worker tags %+v, but got %+v", expected, actual) + } + }) +} + +func newTestGCEModelContext() *GCEModelContext { + cluster := newTestCluster() + igCP := newTestControlPlaneInstanceGroup() + igWorkers := newTestWorkersInstanceGroup() + return &GCEModelContext{ + KopsModelContext: &model.KopsModelContext{ + IAMModelContext: iam.IAMModelContext{ + Cluster: cluster, + }, + AllInstanceGroups: []*kops.InstanceGroup{igCP, igWorkers}, + InstanceGroups: []*kops.InstanceGroup{igCP, igWorkers}, + SSHPublicKeys: [][]byte{[]byte("ssh-rsa ...")}, + }, + } +} + +func newTestCluster() *kops.Cluster { + return &kops.Cluster{ + ObjectMeta: metav1.ObjectMeta{ + Name: "testcluster.test.com", + }, + Spec: kops.ClusterSpec{ + API: kops.APISpec{ + LoadBalancer: &kops.LoadBalancerAccessSpec{ + Type: kops.LoadBalancerTypeInternal, + }, + }, + CloudProvider: kops.CloudProviderSpec{ + GCE: &kops.GCESpec{ + Project: "test-project", + }, + }, + Networking: kops.NetworkingSpec{ + NetworkID: "test-virtual-network", + NetworkCIDR: "10.0.0.0/8", + Subnets: []kops.ClusterSubnetSpec{ + { + Name: "test-subnet", + CIDR: "10.0.1.0/24", + Type: kops.SubnetTypePrivate, + }, + }, + }, + }, + } +} + +func newTestControlPlaneInstanceGroup() *kops.InstanceGroup { + return &kops.InstanceGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "cp-nodes", + }, + Spec: kops.InstanceGroupSpec{ + Role: kops.InstanceGroupRoleControlPlane, + Image: "Canonical:UbuntuServer:18.04-LTS:latest", + RootVolume: &kops.InstanceRootVolumeSpec{ + Size: fi.PtrTo(int32(32)), + }, + Subnets: []string{"test-subnet"}, + }, + } +} +func newTestWorkersInstanceGroup() *kops.InstanceGroup { + return &kops.InstanceGroup{ + ObjectMeta: metav1.ObjectMeta{ + Name: "worker-nodes", + }, + Spec: kops.InstanceGroupSpec{ + Role: kops.InstanceGroupRoleNode, + Image: "Canonical:UbuntuServer:18.04-LTS:latest", + RootVolume: &kops.InstanceRootVolumeSpec{ + Size: fi.PtrTo(int32(32)), + }, + Subnets: []string{"test-subnet"}, + }, + } +}