Skip to content

Commit fd9e4d8

Browse files
authored
Merge pull request #17849 from Camila-B/fix-instancegroup
gce: Require zones on instancegroup creation
2 parents 39ec807 + 6170b47 commit fd9e4d8

30 files changed

Lines changed: 416 additions & 49 deletions

File tree

cloudmock/gce/mockcompute/zone.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ func newZoneClient(project string) *zoneClient {
3838
Name: "us-test1-a",
3939
Region: "https://www.googleapis.com/compute/v1/projects/testproject/regions/us-test1",
4040
},
41+
"us-test1-b": {
42+
Name: "us-test1-b",
43+
Region: "https://www.googleapis.com/compute/v1/projects/testproject/regions/us-test1",
44+
},
4145
},
4246
},
4347
}

cmd/kops/create_instancegroup.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ type CreateInstanceGroupOptions struct {
4848
InstanceGroupName string
4949
Role string
5050
Subnets []string
51+
Zones []string
5152
// DryRun mode output an ig manifest of Output type.
5253
DryRun bool
5354
// Output type during a DryRun
@@ -137,6 +138,9 @@ func NewCmdCreateInstanceGroup(f *util.Factory, out io.Writer) *cobra.Command {
137138
})
138139
cmd.Flags().StringSliceVar(&options.Subnets, "subnet", options.Subnets, "Subnet in which to create instance group. One of Availability Zone like eu-west-1a or a comma-separated list of multiple Availability Zones.")
139140
cmd.RegisterFlagCompletionFunc("subnet", completeClusterSubnet(f, &options.Subnets))
141+
142+
cmd.Flags().StringSliceVar(&options.Zones, "zone", options.Zones, "Zones in which to create instance group. One of Availability Zone like eu-west-1a or a comma-separated list of multiple Availability Zones.")
143+
140144
// DryRun mode that will print YAML or JSON
141145
cmd.Flags().BoolVar(&options.DryRun, "dry-run", options.DryRun, "Only print the object that would be created, without created it. This flag can be used to create an instance group YAML or JSON manifest.")
142146
cmd.Flags().StringVarP(&options.Output, "output", "o", options.Output, "Output format. One of json or yaml")
@@ -188,6 +192,8 @@ func RunCreateInstanceGroup(ctx context.Context, f *util.Factory, out io.Writer,
188192

189193
ig.Spec.Subnets = options.Subnets
190194

195+
ig.Spec.Zones = options.Zones
196+
191197
cloud, err := cloudup.BuildCloud(cluster)
192198
if err != nil {
193199
return err
@@ -200,6 +206,9 @@ func RunCreateInstanceGroup(ctx context.Context, f *util.Factory, out io.Writer,
200206

201207
ig.AddInstanceGroupNodeLabel()
202208
if cluster.GetCloudProvider() == kopsapi.CloudProviderGCE {
209+
if len(ig.Spec.Zones) == 0 {
210+
return fmt.Errorf("zone flag is required for GCE clusters")
211+
}
203212
fmt.Println("detected a GCE cluster; labeling nodes to receive metadata-proxy.")
204213
ig.Spec.NodeLabels["cloud.google.com/metadata-proxy-ready"] = "true"
205214
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
/*
2+
Copyright 2026 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package main
18+
19+
import (
20+
"bytes"
21+
"context"
22+
"testing"
23+
24+
"k8s.io/kops/cmd/kops/util"
25+
"k8s.io/kops/pkg/testutils"
26+
)
27+
28+
func TestRunCreateInstanceGroupGCE(t *testing.T) {
29+
h := testutils.NewIntegrationTestHarness(t)
30+
defer h.Close()
31+
32+
clusterName := "test.k8s.io"
33+
34+
cloud := h.SetupMockGCE()
35+
36+
ctx := context.Background()
37+
f := util.NewFactory(&util.FactoryOptions{
38+
RegistryPath: "memfs://tests",
39+
})
40+
41+
cluster := testutils.BuildMinimalClusterGCE(clusterName, cloud.Project())
42+
43+
clientset, err := f.KopsClient()
44+
if err != nil {
45+
t.Fatalf("error getting clientset: %v", err)
46+
}
47+
if _, err := clientset.CreateCluster(ctx, cluster); err != nil {
48+
t.Fatalf("error creating cluster: %v", err)
49+
}
50+
51+
var stdout bytes.Buffer
52+
options := &CreateInstanceGroupOptions{
53+
ClusterName: clusterName,
54+
InstanceGroupName: "nodes",
55+
Role: "Node",
56+
Subnets: []string{"us-test-1a"},
57+
}
58+
59+
if err := RunCreateInstanceGroup(ctx, f, &stdout, options); err == nil {
60+
t.Fatalf("Expected error when creating instancegroup, got nil")
61+
}
62+
63+
options.Zones = []string{"us-test-1a"}
64+
65+
if err := RunCreateInstanceGroup(ctx, f, &stdout, options); err != nil {
66+
t.Fatalf("could not create instance group: %v", err)
67+
}
68+
}

cmd/kops/edit_instancegroup_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ func TestEditInstanceGroup(t *testing.T) {
3636

3737
clusterName := "test.k8s.io"
3838

39-
cluster := testutils.BuildMinimalCluster(clusterName)
39+
cluster := testutils.BuildMinimalClusterAWS(clusterName)
4040
nodes := testutils.BuildMinimalNodeInstanceGroup("nodes", "subnet-us-test-1a")
4141
nodes.Spec.Taints = []string{"e2etest:NoSchedule"}
4242

docs/cli/kops_create_instancegroup.md

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pkg/apis/kops/validation/gce.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,5 +44,9 @@ func gceValidateInstanceGroup(ig *kops.InstanceGroup, cloud gce.GCECloud) field.
4444
fieldSpec := field.NewPath("spec")
4545
allErrs = append(allErrs, IsValidValue(fieldSpec.Child("gcpProvisioningModel"), ig.Spec.GCPProvisioningModel, []string{"STANDARD", "SPOT"})...)
4646
}
47+
48+
if len(ig.Spec.Zones) == 0 {
49+
allErrs = append(allErrs, field.Required(field.NewPath("spec", "zones"), "zones should be specified"))
50+
}
4751
return allErrs
4852
}

pkg/instancegroups/rollingupdate_os_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ func getTestSetupOS(t *testing.T, ctx context.Context) (*RollingUpdateCluster, *
4545

4646
mockcloud := testutils.SetupMockOpenstack()
4747

48-
inCluster := testutils.BuildMinimalCluster("test.k8s.local")
48+
inCluster := testutils.BuildMinimalClusterAWS("test.k8s.local")
4949

5050
inCluster.Spec.CloudProvider.Openstack = &kopsapi.OpenstackSpec{}
5151
inCluster.Name = "test.k8s.local"

pkg/kubeconfig/create_kubecfg_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ func (f fakeKeyStore) MirrorTo(ctx context.Context, basedir vfs.Path) error {
115115

116116
// build a generic minimal cluster
117117
func buildMinimalCluster(clusterName string, masterPublicName string, lbCert bool, nlb bool) *kops.Cluster {
118-
cluster := testutils.BuildMinimalCluster(clusterName)
118+
cluster := testutils.BuildMinimalClusterAWS(clusterName)
119119
cluster.Spec.API.PublicName = masterPublicName
120120
cluster.Spec.KubernetesVersion = "1.30.0"
121121
if lbCert || nlb {

pkg/model/awsmodel/autoscalinggroup_test.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ const (
3535
)
3636

3737
func buildMinimalCluster() *kops.Cluster {
38-
return testutils.BuildMinimalCluster("testcluster.test.com")
38+
return testutils.BuildMinimalClusterAWS("testcluster.test.com")
3939
}
4040

4141
func buildNodeInstanceGroup(subnets ...string) *kops.InstanceGroup {

pkg/model/gcemodel/autoscalinggroup.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,10 @@ func (b *AutoscalingGroupModelBuilder) splitToZones(ig *kops.InstanceGroup) (map
266266
return nil, err
267267
}
268268

269+
if len(zones) == 0 {
270+
return nil, fmt.Errorf("no zones found for instance group %q", ig.ObjectMeta.Name)
271+
}
272+
269273
// TODO: Duplicated from aws - move to defaults?
270274
minSize := 1
271275
if ig.Spec.MinSize != nil {
@@ -292,7 +296,7 @@ func (b *AutoscalingGroupModelBuilder) splitToZones(ig *kops.InstanceGroup) (map
292296
totalSize++
293297

294298
i++
295-
if i > len(targetSizes) {
299+
if i >= len(targetSizes) {
296300
i = 0
297301
}
298302
}

0 commit comments

Comments
 (0)