Skip to content
This repository was archived by the owner on Apr 17, 2025. It is now read-only.

Commit bb68f18

Browse files
TiberiuGChspencer77
authored andcommitted
Allow nodegroup creation after a cluster subnet is deleted (eksctl-io#7714)
* Preserve eksctl commands correctness when user deletes subnets * update error when subnet availability validation fails * address PR comments
1 parent 72001b9 commit bb68f18

File tree

5 files changed

+286
-15
lines changed

5 files changed

+286
-15
lines changed

pkg/actions/nodegroup/create.go

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,10 @@ func (m *Manager) Create(ctx context.Context, options CreateOpts, nodegroupFilte
135135
}
136136
}
137137

138+
if err := validateSubnetsAvailability(cfg); err != nil {
139+
return err
140+
}
141+
138142
if err := vpc.ValidateLegacySubnetsForNodeGroups(ctx, cfg, ctl.AWSProvider); err != nil {
139143
return err
140144
}
@@ -404,3 +408,52 @@ func validateSecurityGroup(ctx context.Context, ec2API awsapi.EC2, securityGroup
404408
}
405409
return hasDefaultEgressRule, nil
406410
}
411+
412+
func validateSubnetsAvailability(spec *api.ClusterConfig) error {
413+
validateSubnetsAvailabilityForNg := func(np api.NodePool) error {
414+
ng := np.BaseNodeGroup()
415+
subnetTypeForPrivateNetworking := map[bool]string{
416+
true: "private",
417+
false: "public",
418+
}
419+
unavailableSubnetsErr := func(subnetLocation string) error {
420+
return fmt.Errorf("all %[1]s subnets from %[2]s, that the cluster was originally created on, have been deleted; to create %[1]s nodegroups within %[2]s please manually set valid %[1]s subnets via nodeGroup.SubnetIDs",
421+
subnetTypeForPrivateNetworking[ng.PrivateNetworking], subnetLocation)
422+
}
423+
424+
// don't check private networking compatibility for:
425+
// self-managed nodegroups on local zones
426+
if nodeGroup, ok := np.(*api.NodeGroup); (ok && len(nodeGroup.LocalZones) > 0) ||
427+
// nodegroups on outposts
428+
(ng.OutpostARN != "" || spec.IsControlPlaneOnOutposts()) ||
429+
// nodegroups on user specified subnets
430+
len(ng.Subnets) > 0 {
431+
return nil
432+
}
433+
shouldCheckAcrossAllAZs := true
434+
for _, az := range ng.AvailabilityZones {
435+
shouldCheckAcrossAllAZs = false
436+
if _, ok := spec.VPC.Subnets.Private[az]; !ok && ng.PrivateNetworking {
437+
return unavailableSubnetsErr(az)
438+
}
439+
if _, ok := spec.VPC.Subnets.Public[az]; !ok && !ng.PrivateNetworking {
440+
return unavailableSubnetsErr(az)
441+
}
442+
}
443+
if shouldCheckAcrossAllAZs {
444+
if ng.PrivateNetworking && len(spec.VPC.Subnets.Private) == 0 {
445+
return unavailableSubnetsErr(spec.VPC.ID)
446+
}
447+
if !ng.PrivateNetworking && len(spec.VPC.Subnets.Public) == 0 {
448+
return unavailableSubnetsErr(spec.VPC.ID)
449+
}
450+
}
451+
return nil
452+
}
453+
for _, np := range nodes.ToNodePools(spec) {
454+
if err := validateSubnetsAvailabilityForNg(np); err != nil {
455+
return err
456+
}
457+
}
458+
return nil
459+
}

pkg/actions/nodegroup/create_test.go

Lines changed: 156 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,11 @@ type expectedCalls struct {
6262
clientset *fake.Clientset
6363
}
6464

65+
type vpcSubnets struct {
66+
publicIDs []string
67+
privateIDs []string
68+
}
69+
6570
//counterfeiter:generate -o fakes/fake_nodegroup_task_creator.go . nodeGroupTaskCreator
6671
type nodeGroupTaskCreator interface {
6772
NewUnmanagedNodeGroupTask(context.Context, []*api.NodeGroup, bool, bool, bool, vpc.Importer) *tasks.TaskTree
@@ -253,6 +258,51 @@ var _ = DescribeTable("Create", func(t ngEntry) {
253258
},
254259
expectedErr: errors.Wrap(errors.New("shared node security group missing, to fix this run 'eksctl update cluster --name=my-cluster --region='"), "cluster compatibility check failed")}),
255260

261+
Entry("fails when nodegroup uses privateNetworking:true and there's no private subnet within vpc", ngEntry{
262+
updateClusterConfig: func(c *api.ClusterConfig) {
263+
c.NodeGroups[0].PrivateNetworking = true
264+
},
265+
mockCalls: func(m mockCalls) {
266+
mockProviderWithVPCSubnets(m.mockProvider, &vpcSubnets{
267+
publicIDs: []string{"subnet-public-1", "subnet-public-2"},
268+
})
269+
},
270+
expectedErr: fmt.Errorf("all private subnets from vpc-1, that the cluster was originally created on, have been deleted; to create private nodegroups within vpc-1 please manually set valid private subnets via nodeGroup.SubnetIDs"),
271+
}),
272+
Entry("fails when nodegroup uses privateNetworking:false and there's no public subnet within vpc", ngEntry{
273+
mockCalls: func(m mockCalls) {
274+
mockProviderWithVPCSubnets(m.mockProvider, &vpcSubnets{
275+
publicIDs: []string{"subnet-private-1", "subnet-private-2"},
276+
})
277+
},
278+
expectedErr: fmt.Errorf("all public subnets from vpc-1, that the cluster was originally created on, have been deleted; to create public nodegroups within vpc-1 please manually set valid public subnets via nodeGroup.SubnetIDs"),
279+
}),
280+
Entry("fails when nodegroup uses privateNetworking:true and there's no private subnet within az", ngEntry{
281+
updateClusterConfig: func(c *api.ClusterConfig) {
282+
c.NodeGroups[0].PrivateNetworking = true
283+
c.NodeGroups[0].AvailabilityZones = []string{"us-west-2b"}
284+
},
285+
mockCalls: func(m mockCalls) {
286+
mockProviderWithVPCSubnets(m.mockProvider, &vpcSubnets{
287+
publicIDs: []string{"subnet-public-1", "subnet-public-2"},
288+
privateIDs: []string{"subnet-private-1"},
289+
})
290+
},
291+
expectedErr: fmt.Errorf("all private subnets from us-west-2b, that the cluster was originally created on, have been deleted; to create private nodegroups within us-west-2b please manually set valid private subnets via nodeGroup.SubnetIDs"),
292+
}),
293+
Entry("fails when nodegroup uses privateNetworking:false and there's no private subnet within az", ngEntry{
294+
updateClusterConfig: func(c *api.ClusterConfig) {
295+
c.NodeGroups[0].AvailabilityZones = []string{"us-west-2a", "us-west-2b"}
296+
},
297+
mockCalls: func(m mockCalls) {
298+
mockProviderWithVPCSubnets(m.mockProvider, &vpcSubnets{
299+
publicIDs: []string{"subnet-public-2"},
300+
privateIDs: []string{"subnet-private-1", "subnet-private-2"},
301+
})
302+
},
303+
expectedErr: fmt.Errorf("all public subnets from us-west-2a, that the cluster was originally created on, have been deleted; to create public nodegroups within us-west-2a please manually set valid public subnets via nodeGroup.SubnetIDs"),
304+
}),
305+
256306
Entry("fails when existing local ng stacks in config file is not listed", ngEntry{
257307
mockCalls: func(m mockCalls) {
258308
m.nodeGroupFilter.SetOnlyLocalReturns(errors.New("err"))
@@ -435,7 +485,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
435485
m.kubeProvider.NewRawClientReturns(nil, &kubernetes.APIServerUnreachableError{
436486
Err: errors.New("timeout"),
437487
})
438-
mockProviderWithConfig(m.mockProvider, defaultOutput, &ekstypes.VpcConfigResponse{
488+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, &ekstypes.VpcConfigResponse{
439489
ClusterSecurityGroupId: aws.String("csg-1234"),
440490
EndpointPublicAccess: false,
441491
EndpointPrivateAccess: true,
@@ -499,7 +549,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
499549
UpdateAuthConfigMap: api.Disabled(),
500550
},
501551
mockCalls: func(m mockCalls) {
502-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
552+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
503553
AuthenticationMode: ekstypes.AuthenticationModeApi,
504554
})
505555
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -519,7 +569,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
519569
UpdateAuthConfigMap: api.Enabled(),
520570
},
521571
mockCalls: func(m mockCalls) {
522-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
572+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
523573
AuthenticationMode: ekstypes.AuthenticationModeApi,
524574
})
525575
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -536,7 +586,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
536586

537587
Entry("creates nodegroup using access entries when authenticationMode is API_AND_CONFIG_MAP and updateAuthConfigMap is not supplied", ngEntry{
538588
mockCalls: func(m mockCalls) {
539-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
589+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
540590
AuthenticationMode: ekstypes.AuthenticationModeApiAndConfigMap,
541591
})
542592
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -553,7 +603,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
553603

554604
Entry("creates nodegroup using aws-auth ConfigMap when authenticationMode is CONFIG_MAP and updateAuthConfigMap is true", ngEntry{
555605
mockCalls: func(m mockCalls) {
556-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
606+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
557607
AuthenticationMode: ekstypes.AuthenticationModeConfigMap,
558608
})
559609
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -567,7 +617,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
567617

568618
Entry("creates nodegroup using aws-auth ConfigMap when authenticationMode is CONFIG_MAP and updateAuthConfigMap is not supplied", ngEntry{
569619
mockCalls: func(m mockCalls) {
570-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
620+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
571621
AuthenticationMode: ekstypes.AuthenticationModeConfigMap,
572622
})
573623
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -581,7 +631,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
581631

582632
Entry("creates nodegroup but does not use either aws-auth ConfigMap or access entries when authenticationMode is API_AND_CONFIG_MAP and updateAuthConfigMap is false", ngEntry{
583633
mockCalls: func(m mockCalls) {
584-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
634+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
585635
AuthenticationMode: ekstypes.AuthenticationModeApiAndConfigMap,
586636
})
587637
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -602,7 +652,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
602652

603653
Entry("creates nodegroup but does not use either aws-auth ConfigMap or access entries when authenticationMode is CONFIG_MAP and updateAuthConfigMap is false", ngEntry{
604654
mockCalls: func(m mockCalls) {
605-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
655+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
606656
AuthenticationMode: ekstypes.AuthenticationModeConfigMap,
607657
})
608658
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -623,7 +673,7 @@ var _ = DescribeTable("Create", func(t ngEntry) {
623673

624674
Entry("authorizes nodegroups using aws-auth ConfigMap when authenticationMode is API_AND_CONFIG_MAP and updateAuthConfigMap is true", ngEntry{
625675
mockCalls: func(m mockCalls) {
626-
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, &ekstypes.AccessConfigResponse{
676+
mockProviderWithConfig(m.mockProvider, defaultOutput, nil, nil, nil, &ekstypes.AccessConfigResponse{
627677
AuthenticationMode: ekstypes.AuthenticationModeApiAndConfigMap,
628678
})
629679
defaultProviderMocks(m.mockProvider, defaultOutput)
@@ -734,6 +784,14 @@ var defaultOutput = []cftypes.Output{
734784
OutputKey: aws.String("SharedNodeSecurityGroup"),
735785
OutputValue: aws.String("sg-1"),
736786
},
787+
{
788+
OutputKey: aws.String("SubnetsPublic"),
789+
OutputValue: aws.String("subnet-public-1,subnet-public-2"),
790+
},
791+
{
792+
OutputKey: aws.String("SubnetsPrivate"),
793+
OutputValue: aws.String("subnet-private-1,subnet-private-2"),
794+
},
737795
}
738796

739797
func getIAMIdentities(clientset kubernetes.Interface) []iam.Identity {
@@ -766,14 +824,18 @@ func expectedCallsForAWSAuth(e expectedCalls) {
766824
}
767825

768826
func defaultProviderMocks(p *mockprovider.MockProvider, output []cftypes.Output) {
769-
mockProviderWithConfig(p, output, nil, nil, nil)
827+
mockProviderWithConfig(p, output, nil, nil, nil, nil)
770828
}
771829

772830
func mockProviderWithOutpostConfig(p *mockprovider.MockProvider, describeStacksOutput []cftypes.Output, outpostConfig *ekstypes.OutpostConfigResponse) {
773-
mockProviderWithConfig(p, describeStacksOutput, nil, outpostConfig, nil)
831+
mockProviderWithConfig(p, describeStacksOutput, nil, nil, outpostConfig, nil)
832+
}
833+
834+
func mockProviderWithVPCSubnets(p *mockprovider.MockProvider, subnets *vpcSubnets) {
835+
mockProviderWithConfig(p, defaultOutput, subnets, nil, nil, nil)
774836
}
775837

776-
func mockProviderWithConfig(p *mockprovider.MockProvider, describeStacksOutput []cftypes.Output, vpcConfigRes *ekstypes.VpcConfigResponse, outpostConfig *ekstypes.OutpostConfigResponse, accessConfig *ekstypes.AccessConfigResponse) {
838+
func mockProviderWithConfig(p *mockprovider.MockProvider, describeStacksOutput []cftypes.Output, subnets *vpcSubnets, vpcConfigRes *ekstypes.VpcConfigResponse, outpostConfig *ekstypes.OutpostConfigResponse, accessConfig *ekstypes.AccessConfigResponse) {
777839
p.MockCloudFormation().On("ListStacks", mock.Anything, mock.Anything).Return(&cloudformation.ListStacksOutput{
778840
StackSummaries: []cftypes.StackSummary{
779841
{
@@ -854,6 +916,88 @@ func mockProviderWithConfig(p *mockprovider.MockProvider, describeStacksOutput [
854916
},
855917
},
856918
}, nil)
919+
920+
if subnets == nil {
921+
subnets = &vpcSubnets{
922+
publicIDs: []string{"subnet-public-1", "subnet-public-2"},
923+
privateIDs: []string{"subnet-private-1", "subnet-private-2"},
924+
}
925+
}
926+
927+
subnetPublic1 := ec2types.Subnet{
928+
SubnetId: aws.String("subnet-public-1"),
929+
CidrBlock: aws.String("192.168.64.0/20"),
930+
AvailabilityZone: aws.String("us-west-2a"),
931+
VpcId: aws.String("vpc-1"),
932+
MapPublicIpOnLaunch: aws.Bool(true),
933+
}
934+
subnetPrivate1 := ec2types.Subnet{
935+
SubnetId: aws.String("subnet-private-1"),
936+
CidrBlock: aws.String("192.168.128.0/20"),
937+
AvailabilityZone: aws.String("us-west-2a"),
938+
VpcId: aws.String("vpc-1"),
939+
MapPublicIpOnLaunch: aws.Bool(false),
940+
}
941+
subnetPublic2 := ec2types.Subnet{
942+
SubnetId: aws.String("subnet-public-2"),
943+
CidrBlock: aws.String("192.168.80.0/20"),
944+
AvailabilityZone: aws.String("us-west-2b"),
945+
VpcId: aws.String("vpc-1"),
946+
MapPublicIpOnLaunch: aws.Bool(true),
947+
}
948+
subnetPrivate2 := ec2types.Subnet{
949+
SubnetId: aws.String("subnet-private-2"),
950+
CidrBlock: aws.String("192.168.32.0/20"),
951+
AvailabilityZone: aws.String("us-west-2b"),
952+
VpcId: aws.String("vpc-1"),
953+
MapPublicIpOnLaunch: aws.Bool(false),
954+
}
955+
956+
subnetsForID := map[string]ec2types.Subnet{
957+
"subnet-public-1": subnetPublic1,
958+
"subnet-private-1": subnetPrivate1,
959+
"subnet-public-2": subnetPublic2,
960+
"subnet-private-2": subnetPrivate2,
961+
}
962+
963+
mockDescribeSubnets := func(mp *mockprovider.MockProvider, vpcID string, subnetIDs []string) {
964+
var subnets []ec2types.Subnet
965+
for _, id := range subnetIDs {
966+
subnets = append(subnets, subnetsForID[id])
967+
}
968+
if vpcID == "" {
969+
mp.MockEC2().On("DescribeSubnets", mock.Anything, &ec2.DescribeSubnetsInput{
970+
SubnetIds: subnetIDs,
971+
}).Return(&ec2.DescribeSubnetsOutput{Subnets: subnets}, nil)
972+
return
973+
}
974+
mp.MockEC2().On("DescribeSubnets", mock.Anything, &ec2.DescribeSubnetsInput{
975+
Filters: []ec2types.Filter{
976+
{
977+
Name: aws.String("vpc-id"),
978+
Values: []string{vpcID},
979+
},
980+
},
981+
}).Return(&ec2.DescribeSubnetsOutput{Subnets: subnets}, nil)
982+
}
983+
984+
mockDescribeSubnets(p, "", subnets.publicIDs)
985+
mockDescribeSubnets(p, "", subnets.privateIDs)
986+
mockDescribeSubnets(p, "vpc-1", append(subnets.publicIDs, subnets.privateIDs...))
987+
988+
p.MockEC2().On("DescribeVpcs", mock.Anything, mock.Anything).Return(&ec2.DescribeVpcsOutput{
989+
Vpcs: []ec2types.Vpc{
990+
{
991+
CidrBlock: aws.String("192.168.0.0/20"),
992+
VpcId: aws.String("vpc-1"),
993+
CidrBlockAssociationSet: []ec2types.VpcCidrBlockAssociation{
994+
{
995+
CidrBlock: aws.String("192.168.0.0/20"),
996+
},
997+
},
998+
},
999+
},
1000+
}, nil)
8571001
}
8581002

8591003
func mockProviderForUnownedCluster(p *mockprovider.MockProvider, k *eksfakes.FakeKubeProvider, extraSGRules ...ec2types.SecurityGroupRule) {

pkg/cfn/outputs/api.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,10 @@ func Exists(stack types.Stack, key string) bool {
108108

109109
// Collect the outputs of a stack using required and optional CollectorSets
110110
func Collect(stack types.Stack, required, optional map[string]Collector) error {
111-
if err := NewCollectorSet(optional).doCollect(false, stack); err != nil {
111+
if err := NewCollectorSet(required).doCollect(true, stack); err != nil {
112112
return err
113113
}
114-
return NewCollectorSet(required).doCollect(true, stack)
114+
return NewCollectorSet(optional).doCollect(false, stack)
115115
}
116116

117117
// MustCollect will error if any of the outputs are missing

0 commit comments

Comments
 (0)