diff --git a/go.mod b/go.mod index 7e3685eca7..e248d5efcf 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ go 1.26.5 require ( github.com/Masterminds/semver/v3 v3.5.0 github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3 - github.com/aws/aws-sdk-go-v2 v1.45.1 + github.com/aws/aws-sdk-go-v2 v1.47.4 github.com/aws/aws-sdk-go-v2/config v1.33.1 github.com/aws/aws-sdk-go-v2/credentials v1.20.1 github.com/aws/aws-sdk-go-v2/service/autoscaling v1.75.1 @@ -16,7 +16,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/cloudwatchlogs v1.84.1 github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.70.0 github.com/aws/aws-sdk-go-v2/service/ec2 v1.325.1 - github.com/aws/aws-sdk-go-v2/service/eks v1.95.1 + github.com/aws/aws-sdk-go-v2/service/eks v1.105.0 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.38.1 github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.60.1 github.com/aws/aws-sdk-go-v2/service/iam v1.61.1 @@ -24,7 +24,7 @@ require ( github.com/aws/aws-sdk-go-v2/service/outposts v1.70.1 github.com/aws/aws-sdk-go-v2/service/ssm v1.75.1 github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 - github.com/aws/smithy-go v1.28.1 + github.com/aws/smithy-go v1.28.6 github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20260213141146-147b13ea3f4a github.com/benjamintf1/unmarshalledmatchers v1.0.0 github.com/blang/semver/v4 v4.0.0 @@ -136,8 +136,8 @@ require ( github.com/atotto/clipboard v0.1.4 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 // indirect - github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.7 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.7 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 // indirect github.com/aws/aws-sdk-go-v2/service/eventbridge v1.45.17 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.19 // indirect diff --git a/go.sum b/go.sum index a25e03e7eb..55d23da1f0 100644 --- a/go.sum +++ b/go.sum @@ -108,8 +108,8 @@ github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI= github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3 h1:13qtG4reL2+2UiCm2U7gM9QJthDO8hPPzvn7hTjHask= github.com/aws/amazon-ec2-instance-selector/v3 v3.1.3/go.mod h1:wdlMRtz9G4IO6H1yZPsqfGBxR8E6B/bdxHlGkls4kGQ= -github.com/aws/aws-sdk-go-v2 v1.45.1 h1:iIoG3NaLhV6UZpPXyPXlDj2I9oS8tV/nMcMnITCC6Ks= -github.com/aws/aws-sdk-go-v2 v1.45.1/go.mod h1:bttEH6JqnUL8LepvDVfdrds/fZ5bCIxzpe3abyUrhDU= +github.com/aws/aws-sdk-go-v2 v1.47.4 h1:bTvk6UdSSSvaQsfLVj1JRDEEqj/PDRwGf1KanaRzau0= +github.com/aws/aws-sdk-go-v2 v1.47.4/go.mod h1:XkrjAZvr705Nytv6PkPUhoSB2W9+vVHBp5oAWpkh8Ak= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20 h1:GPRlPwz40I2B2VrBEASOA3Bi77NyeqejNLkifosX0rs= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.20/go.mod h1:g7PNzKcsOKWb4fkSRBA7BZVAS6Y8IcxzN+nRohhQ1Q8= github.com/aws/aws-sdk-go-v2/config v1.33.1 h1:bq9jze1hQ5YTCLoVxNnbp0T7rglrlOE7N9YsHqjGkEw= @@ -118,10 +118,10 @@ github.com/aws/aws-sdk-go-v2/credentials v1.20.1 h1:Z8GRNEx0u9sDkZOq4PUnN8mjGwbU github.com/aws/aws-sdk-go-v2/credentials v1.20.1/go.mod h1:uBIK00kFo95dnemqfFMTWx0X8YRqsh6ecIoCjjOkZqM= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1 h1:YIEBqcqRnpi4Pfv0YHImtgi6czGCwKHANC7SwmUAVD0= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.19.1/go.mod h1:imEf0oufgAo8KAkCHhrOdqGEC0YWx1PPBQH82shSxGw= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1 h1:pc138gM1CW+XPc60rEwUlwwuwWFQK16CI1T7v1F9Oec= -github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.1/go.mod h1:1+koxpPIbfBdfzP6vojm5/zTpTQ/micYwlxIiNB3TxI= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1 h1:K0JsbZQj+1h208Ro1zHeA4l7bMp0NvRffHQ91q8Ol1s= -github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.1/go.mod h1:W3/vL6EtCIatICGy9ab29QhMuae+cOKPWcMxv02CO+Q= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.7 h1:fSNAt6hs4qLjb9tMZc6vj3PHlOCYGXK2JjJZ7AW2tf4= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.5.7/go.mod h1:TMav+EuXQu5kNpZZz/6rIC8xx4mCsW+6AKHrjvOSAsE= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.7 h1:7NS6kqBjkWIN8P/AEdmtyq6eQGAhN1oHTZdzXclpH6Y= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.8.7/go.mod h1:1PFqtAUsIgF1nEVojzRJSf/v88zjzqrGW+w+UAtkXyQ= github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1 h1:yhw5KD1phVyP9vijxOUzDfEtJx+bt+L63k+VfuiYFAA= github.com/aws/aws-sdk-go-v2/internal/v4a v1.5.1/go.mod h1:ZW2e0d7DYlRxlS9hEiMXE47gTdX5KRN4byUiNbUpG+Q= github.com/aws/aws-sdk-go-v2/service/autoscaling v1.75.1 h1:bVjY9oIRGBNMkOicaoXWjzNV7ZucqCDGLL+48+3pPzI= @@ -136,8 +136,8 @@ github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.70.0 h1:DeAqe11n github.com/aws/aws-sdk-go-v2/service/cognitoidentityprovider v1.70.0/go.mod h1:LmtIJyc0o3ZgAHDMC0Vx/GiQG4TbPT07dcllPqwlItw= github.com/aws/aws-sdk-go-v2/service/ec2 v1.325.1 h1:rL19vNlxMMJHDSbX5JAwnpR/94kRgcebkdFRvmG6MWM= github.com/aws/aws-sdk-go-v2/service/ec2 v1.325.1/go.mod h1:M8AJ/M7737nKBNqZvXUNrADGBy+d3PNggJGmAStCWxI= -github.com/aws/aws-sdk-go-v2/service/eks v1.95.1 h1:Xz6LhWPiYuE5BzHgBS07a3GMR0kVXHkzOPzQVheQ0Vw= -github.com/aws/aws-sdk-go-v2/service/eks v1.95.1/go.mod h1:VW9+jZicSbAPGHczf4tlIwgjkNT+5KXYYDbz/PxsRTs= +github.com/aws/aws-sdk-go-v2/service/eks v1.105.0 h1:+bTTJtMBFZQopQKHC/JhJP6liBN057nl6agVeQur17g= +github.com/aws/aws-sdk-go-v2/service/eks v1.105.0/go.mod h1:yqmvzWK8FGGbWvjhW1wIpbjxpJNXgz0ciG2ISBXho+Q= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.38.1 h1:k2gT2b6kesMfFreEyOM86U3Vu/IgVz6lPZDUpECJfSM= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing v1.38.1/go.mod h1:rp/ZIrMxsdYHnUW9urnQ5EroaDmT1OJAg1Pc9LF8eWI= github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2 v1.60.1 h1:FiQmqRTw/46KRhlefi+0PlHscSuw6wcuosjRfau7b2g= @@ -176,8 +176,8 @@ github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1 h1:6yeYCWFvgbI2TI3K6jr9LtBN github.com/aws/aws-sdk-go-v2/service/ssooidc v1.40.1/go.mod h1:naFe83jSMuYkH+QjQPX8n1MLhBkeCFM5Lsnh5m5wz3c= github.com/aws/aws-sdk-go-v2/service/sts v1.47.1 h1:Sv2xPnRHlThSUtVujYuUBPI/Il8si6UPHXL8DMiB/F0= github.com/aws/aws-sdk-go-v2/service/sts v1.47.1/go.mod h1:mKo/CzaCz8qytGW70NG4vIIGAx1HXTlb5lHNkC5k3lk= -github.com/aws/smithy-go v1.28.1 h1:R/nXH00c8qcfCzQVELtRw+eLQWtzv+VAIEFJ1/xxXlQ= -github.com/aws/smithy-go v1.28.1/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= +github.com/aws/smithy-go v1.28.6 h1:Pj+/sUviNYjow8ELRtNwKHZ9ECeF+7pATX3+shjLIrw= +github.com/aws/smithy-go v1.28.6/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20260213141146-147b13ea3f4a h1:xGY9gNZ4pGlqZti3DlsR8WiHz9sjjfaofG0KH0UgAhg= github.com/awslabs/amazon-eks-ami/nodeadm v0.0.0-20260213141146-147b13ea3f4a/go.mod h1:JndTvVCUQsR9TiNZ6g9J5V2LGQkuhhgUGuxzWhNZLA0= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= diff --git a/pkg/actions/automode/auto_mode_updater.go b/pkg/actions/automode/auto_mode_updater.go index 7066075ea0..828d856958 100644 --- a/pkg/actions/automode/auto_mode_updater.go +++ b/pkg/actions/automode/auto_mode_updater.go @@ -54,6 +54,9 @@ type Updater struct { PodsGetter corev1client.PodsGetter EKSUpdater EKSUpdater Drainer NodeGroupDrainer + // ElasticLoadBalancing is the requested load balancing capability config sent when enabling Auto Mode. + // It must be captured before the cluster state is refreshed, and defaults to enabled when unset. + ElasticLoadBalancing *ekstypes.ElasticLoadBalancing } // Update updates the cluster to match the autoModeConfig settings supplied in clusterConfig. @@ -129,9 +132,7 @@ func (u *Updater) enableAutoMode(ctx context.Context, autoModeConfig *api.AutoMo Name: aws.String(clusterName), ComputeConfig: computeConfigReq, KubernetesNetworkConfig: &ekstypes.KubernetesNetworkConfigRequest{ - ElasticLoadBalancing: &ekstypes.ElasticLoadBalancing{ - Enabled: aws.Bool(true), - }, + ElasticLoadBalancing: u.elasticLoadBalancing(), }, StorageConfig: &ekstypes.StorageConfigRequest{ BlockStorage: &ekstypes.BlockStorage{ @@ -223,3 +224,10 @@ func (u *Updater) preflightCheck(ctx context.Context) error { } return nil } + +func (u *Updater) elasticLoadBalancing() *ekstypes.ElasticLoadBalancing { + if u.ElasticLoadBalancing != nil { + return u.ElasticLoadBalancing + } + return &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)} +} diff --git a/pkg/actions/automode/auto_mode_updater_test.go b/pkg/actions/automode/auto_mode_updater_test.go index 18ffb054e3..e553b719f3 100644 --- a/pkg/actions/automode/auto_mode_updater_test.go +++ b/pkg/actions/automode/auto_mode_updater_test.go @@ -21,11 +21,12 @@ import ( ) type updaterTest struct { - autoModeConfig *api.AutoModeConfig - vpc *api.ClusterVPC - currentCluster *ekstypes.Cluster - updateMocks func(*updaterMocks) - drainNodes bool + autoModeConfig *api.AutoModeConfig + elasticLoadBalancing *ekstypes.ElasticLoadBalancing + vpc *api.ClusterVPC + currentCluster *ekstypes.Cluster + updateMocks func(*updaterMocks) + drainNodes bool expectedErr string } @@ -49,10 +50,11 @@ var _ = DescribeTable("Auto Mode Updater", func(t updaterTest) { clusterConfig.AutoModeConfig = t.autoModeConfig clusterConfig.VPC = t.vpc updater := &automodeactions.Updater{ - RoleManager: &um.roleManager, - ClusterRoleManager: &um.clusterRoleManager, - PodsGetter: um.clientSet.CoreV1(), - EKSUpdater: &um.eksUpdater, + RoleManager: &um.roleManager, + ClusterRoleManager: &um.clusterRoleManager, + PodsGetter: um.clientSet.CoreV1(), + EKSUpdater: &um.eksUpdater, + ElasticLoadBalancing: t.elasticLoadBalancing, } if t.drainNodes { updater.Drainer = &um.drainer @@ -131,6 +133,27 @@ var _ = DescribeTable("Auto Mode Updater", func(t updaterTest) { mockEnableAutoMode(u, "arn:aws:iam::000:role/CustomNodeRole", []string{api.AutoModeNodePoolGeneralPurpose}) }, }), + Entry("enabling Auto Mode with elasticLoadBalancing explicitly disabled", updaterTest{ + autoModeConfig: &api.AutoModeConfig{ + Enabled: api.Enabled(), + NodeRoleARN: api.MustParseARN("arn:aws:iam::000:role/CustomNodeRole"), + NodePools: &[]string{api.AutoModeNodePoolGeneralPurpose}, + }, + elasticLoadBalancing: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(false)}, + currentCluster: &ekstypes.Cluster{ + ComputeConfig: &ekstypes.ComputeConfigResponse{ + Enabled: aws.Bool(false), + }, + }, + updateMocks: func(u *updaterMocks) { + u.clusterRoleManager.EXPECT().UpdateRoleForAutoMode(mock.Anything).Return(nil).Once() + mockUpdateClusterConfigWithELB(u, &ekstypes.ComputeConfigRequest{ + Enabled: aws.Bool(true), + NodePools: []string{api.AutoModeNodePoolGeneralPurpose}, + NodeRoleArn: aws.String("arn:aws:iam::000:role/CustomNodeRole"), + }, aws.Bool(false)) + }, + }), Entry("enabling Auto Mode with a pre-existing VPC", updaterTest{ autoModeConfig: &api.AutoModeConfig{ Enabled: aws.Bool(true), @@ -227,12 +250,16 @@ func mockEnableAutoMode(u *updaterMocks, nodeRoleARN string, nodePools []string) } func mockUpdateClusterConfig(u *updaterMocks, computeConfig *ekstypes.ComputeConfigRequest) { + mockUpdateClusterConfigWithELB(u, computeConfig, computeConfig.Enabled) +} + +func mockUpdateClusterConfigWithELB(u *updaterMocks, computeConfig *ekstypes.ComputeConfigRequest, elbEnabled *bool) { u.eksUpdater.EXPECT().UpdateClusterConfig(mock.Anything, &awseks.UpdateClusterConfigInput{ Name: aws.String("cluster"), ComputeConfig: computeConfig, KubernetesNetworkConfig: &ekstypes.KubernetesNetworkConfigRequest{ ElasticLoadBalancing: &ekstypes.ElasticLoadBalancing{ - Enabled: computeConfig.Enabled, + Enabled: elbEnabled, }, }, StorageConfig: &ekstypes.StorageConfigRequest{ diff --git a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json index dbd2f0a03f..1adac9bf53 100755 --- a/pkg/apis/eksctl.io/v1alpha5/assets/schema.json +++ b/pkg/apis/eksctl.io/v1alpha5/assets/schema.json @@ -1330,6 +1330,27 @@ "description": "holds control plane scaling configuration.", "x-intellij-html-description": "holds control plane scaling configuration." }, + "ElasticLoadBalancing": { + "properties": { + "enabled": { + "type": "boolean", + "description": "enables or disables the load balancing capability", + "x-intellij-html-description": "enables or disables the load balancing capability" + }, + "gatewayApi": { + "$ref": "#/definitions/GatewayAPI", + "description": "holds the Gateway API config for the load balancing capability", + "x-intellij-html-description": "holds the Gateway API config for the load balancing capability" + } + }, + "preferredOrder": [ + "enabled", + "gatewayApi" + ], + "additionalProperties": false, + "description": "holds the config for the load balancing capability", + "x-intellij-html-description": "holds the config for the load balancing capability" + }, "FargateProfile": { "required": [ "name" @@ -1446,6 +1467,36 @@ "x-intellij-html-description": "a map of string for passing arbitrary flags to Flux bootstrap", "default": "{}" }, + "GatewayAPI": { + "properties": { + "crds": { + "$ref": "#/definitions/GatewayAPICRDs", + "description": "holds the config for the Gateway API CRDs", + "x-intellij-html-description": "holds the config for the Gateway API CRDs" + } + }, + "preferredOrder": [ + "crds" + ], + "additionalProperties": false, + "description": "holds the Gateway API config for the load balancing capability", + "x-intellij-html-description": "holds the Gateway API config for the load balancing capability" + }, + "GatewayAPICRDs": { + "properties": { + "managed": { + "type": "boolean", + "description": "lets EKS install and upgrade the Gateway API CRDs", + "x-intellij-html-description": "lets EKS install and upgrade the Gateway API CRDs" + } + }, + "preferredOrder": [ + "managed" + ], + "additionalProperties": false, + "description": "holds the config for the Gateway API CRDs", + "x-intellij-html-description": "holds the config for the Gateway API CRDs" + }, "GitOps": { "properties": { "flux": { @@ -1715,6 +1766,11 @@ }, "KubernetesNetworkConfig": { "properties": { + "elasticLoadBalancing": { + "$ref": "#/definitions/ElasticLoadBalancing", + "description": "holds the config for the load balancing capability", + "x-intellij-html-description": "holds the config for the load balancing capability" + }, "ipFamily": { "type": "string", "description": "Valid variants are: `\"IPv4\"` defines an IP family of v4 to be used when creating a new VPC and cluster., `\"IPv6\"` defines an IP family of v6 to be used when creating a new VPC and cluster..", @@ -1738,7 +1794,8 @@ "preferredOrder": [ "ipFamily", "serviceIPv4CIDR", - "serviceIPv6CIDR" + "serviceIPv6CIDR", + "elasticLoadBalancing" ], "additionalProperties": false, "description": "contains cluster networking options", diff --git a/pkg/apis/eksctl.io/v1alpha5/elastic_load_balancing_validation_test.go b/pkg/apis/eksctl.io/v1alpha5/elastic_load_balancing_validation_test.go new file mode 100644 index 0000000000..4004c06f41 --- /dev/null +++ b/pkg/apis/eksctl.io/v1alpha5/elastic_load_balancing_validation_test.go @@ -0,0 +1,107 @@ +package v1alpha5_test + +import ( + "github.com/aws/aws-sdk-go-v2/aws" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +var _ = DescribeTable("Elastic Load Balancing Validation", func(c *api.ClusterConfig, expectedErr string) { + err := api.ValidateElasticLoadBalancing(c) + if expectedErr != "" { + Expect(err).To(MatchError(expectedErr)) + } else { + Expect(err).NotTo(HaveOccurred()) + } +}, + Entry("no kubernetesNetworkConfig", &api.ClusterConfig{}, ""), + Entry("no elasticLoadBalancing", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{}, + }, ""), + Entry("elasticLoadBalancing enabled without gatewayApi", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + }, + }, + }, ""), + Entry("gatewayApi when the capability is disabled", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: api.Disabled(), + GatewayAPI: &api.GatewayAPI{ + CRDs: &api.GatewayAPICRDs{Managed: api.Enabled()}, + }, + }, + }, + }, "cannot set kubernetesNetworkConfig.elasticLoadBalancing.gatewayApi when the load balancing capability is disabled"), + Entry("gatewayApi without crds.managed", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{}, + }, + }, + }, "kubernetesNetworkConfig.elasticLoadBalancing.gatewayApi.crds.managed must be set to either true or false"), + Entry("gatewayApi with crds.managed", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{ + CRDs: &api.GatewayAPICRDs{Managed: api.Enabled()}, + }, + }, + }, + }, ""), +) + +var _ = DescribeTable("ElasticLoadBalancingEnabled", func(c *api.ClusterConfig, expected bool) { + Expect(c.ElasticLoadBalancingEnabled()).To(Equal(expected)) +}, + Entry("no config", &api.ClusterConfig{}, false), + Entry("defaults to Auto Mode when unset", &api.ClusterConfig{ + AutoModeConfig: &api.AutoModeConfig{Enabled: api.Enabled()}, + }, true), + Entry("explicitly disabled with Auto Mode enabled", &api.ClusterConfig{ + AutoModeConfig: &api.AutoModeConfig{Enabled: api.Enabled()}, + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{Enabled: api.Disabled()}, + }, + }, false), + Entry("explicitly enabled without Auto Mode", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{Enabled: api.Enabled()}, + }, + }, true), +) + +var _ = DescribeTable("ElasticLoadBalancingRequest", func(c *api.ClusterConfig, expected *ekstypes.ElasticLoadBalancing) { + Expect(c.ElasticLoadBalancingRequest()).To(Equal(expected)) +}, + Entry("defaults to Auto Mode", &api.ClusterConfig{ + AutoModeConfig: &api.AutoModeConfig{Enabled: api.Enabled()}, + }, &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}), + Entry("omits gatewayApi when unset", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{Enabled: api.Disabled()}, + }, + }, &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(false)}), + Entry("includes gatewayApi.crds.managed", &api.ClusterConfig{ + KubernetesNetworkConfig: &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{ + CRDs: &api.GatewayAPICRDs{Managed: api.Enabled()}, + }, + }, + }, + }, &ekstypes.ElasticLoadBalancing{ + Enabled: aws.Bool(true), + GatewayApi: &ekstypes.GatewayApiConfiguration{ + Crds: &ekstypes.CrdManagementConfiguration{Managed: aws.Bool(true)}, + }, + }), +) diff --git a/pkg/apis/eksctl.io/v1alpha5/types.go b/pkg/apis/eksctl.io/v1alpha5/types.go index 8be00c3ee0..a754397e74 100644 --- a/pkg/apis/eksctl.io/v1alpha5/types.go +++ b/pkg/apis/eksctl.io/v1alpha5/types.go @@ -752,12 +752,39 @@ type KubernetesNetworkConfig struct { ServiceIPv4CIDR string `json:"serviceIPv4CIDR,omitempty"` // ServiceIPv6CIDR is the IPv6 CIDR range from where `ClusterIP`s are assigned ServiceIPv6CIDR string `json:"serviceIPv6CIDR,omitempty"` + // ElasticLoadBalancing holds the config for the load balancing capability + // +optional + ElasticLoadBalancing *ElasticLoadBalancing `json:"elasticLoadBalancing,omitempty"` } func (k *KubernetesNetworkConfig) IPv6Enabled() bool { return strings.EqualFold(k.IPFamily, IPV6Family) } +// ElasticLoadBalancing holds the config for the load balancing capability +type ElasticLoadBalancing struct { + // Enabled enables or disables the load balancing capability + // +optional + Enabled *bool `json:"enabled,omitempty"` + // GatewayAPI holds the Gateway API config for the load balancing capability + // +optional + GatewayAPI *GatewayAPI `json:"gatewayApi,omitempty"` +} + +// GatewayAPI holds the Gateway API config for the load balancing capability +type GatewayAPI struct { + // CRDs holds the config for the Gateway API CRDs + // +optional + CRDs *GatewayAPICRDs `json:"crds,omitempty"` +} + +// GatewayAPICRDs holds the config for the Gateway API CRDs +type GatewayAPICRDs struct { + // Managed lets EKS install and upgrade the Gateway API CRDs + // +optional + Managed *bool `json:"managed,omitempty"` +} + // VPCGatewayID the ID of the gateway that facilitates external connectivity // from customer's VPC to their remote network(s). // Valid options are Transit Gateway and Virtual Private Gateway. @@ -950,6 +977,31 @@ func (c *ClusterConfig) IsAutoModeEnabled() bool { return c.AutoModeConfig != nil && IsEnabled(c.AutoModeConfig.Enabled) } +// ElasticLoadBalancingEnabled reports whether the load balancing capability is enabled, defaulting to Auto Mode. +func (c *ClusterConfig) ElasticLoadBalancingEnabled() bool { + if knc := c.KubernetesNetworkConfig; knc != nil && knc.ElasticLoadBalancing != nil && knc.ElasticLoadBalancing.Enabled != nil { + return *knc.ElasticLoadBalancing.Enabled + } + return c.IsAutoModeEnabled() +} + +// ElasticLoadBalancingRequest returns the EKS API representation of the load balancing capability config. +func (c *ClusterConfig) ElasticLoadBalancingRequest() *ekstypes.ElasticLoadBalancing { + elb := &ekstypes.ElasticLoadBalancing{ + Enabled: aws.Bool(c.ElasticLoadBalancingEnabled()), + } + if knc := c.KubernetesNetworkConfig; knc != nil && knc.ElasticLoadBalancing != nil { + if gatewayAPI := knc.ElasticLoadBalancing.GatewayAPI; gatewayAPI != nil && gatewayAPI.CRDs != nil { + elb.GatewayApi = &ekstypes.GatewayApiConfiguration{ + Crds: &ekstypes.CrdManagementConfiguration{ + Managed: gatewayAPI.CRDs.Managed, + }, + } + } + } + return elb +} + // ClusterProvider is the interface to AWS APIs type ClusterProvider interface { CloudFormation() awsapi.CloudFormation @@ -1373,6 +1425,14 @@ func (c *ClusterConfig) SetClusterState(cluster *ekstypes.Cluster) error { knc.IPFamily = IPV6Family knc.ServiceIPv6CIDR = aws.ToString(networkConfig.ServiceIpv6Cidr) } + if elb := networkConfig.ElasticLoadBalancing; elb != nil { + knc.ElasticLoadBalancing = &ElasticLoadBalancing{Enabled: elb.Enabled} + if elb.GatewayApi != nil && elb.GatewayApi.Crds != nil { + knc.ElasticLoadBalancing.GatewayAPI = &GatewayAPI{ + CRDs: &GatewayAPICRDs{Managed: elb.GatewayApi.Crds.Managed}, + } + } + } c.KubernetesNetworkConfig = knc c.Status.KubernetesNetworkConfig = knc } diff --git a/pkg/apis/eksctl.io/v1alpha5/validation.go b/pkg/apis/eksctl.io/v1alpha5/validation.go index f057917ca7..5cea5a3334 100644 --- a/pkg/apis/eksctl.io/v1alpha5/validation.go +++ b/pkg/apis/eksctl.io/v1alpha5/validation.go @@ -774,6 +774,9 @@ func (c *ClusterConfig) validateKubernetesNetworkConfig() error { if c.KubernetesNetworkConfig == nil { return nil } + if err := ValidateElasticLoadBalancing(c); err != nil { + return err + } if c.KubernetesNetworkConfig.ServiceIPv4CIDR != "" { if c.IPv6Enabled() { return errors.New("service IPv4 CIDR is not supported with IPv6") @@ -830,6 +833,25 @@ func (c *ClusterConfig) validateKubernetesNetworkConfig() error { return nil } +// ValidateElasticLoadBalancing validates the load balancing capability config. +func ValidateElasticLoadBalancing(clusterConfig *ClusterConfig) error { + knc := clusterConfig.KubernetesNetworkConfig + if knc == nil || knc.ElasticLoadBalancing == nil { + return nil + } + gatewayAPI := knc.ElasticLoadBalancing.GatewayAPI + if gatewayAPI == nil { + return nil + } + if !clusterConfig.ElasticLoadBalancingEnabled() { + return errors.New("cannot set kubernetesNetworkConfig.elasticLoadBalancing.gatewayApi when the load balancing capability is disabled") + } + if gatewayAPI.CRDs == nil || gatewayAPI.CRDs.Managed == nil { + return errors.New("kubernetesNetworkConfig.elasticLoadBalancing.gatewayApi.crds.managed must be set to either true or false") + } + return nil +} + // NoAccess returns true if neither public are private cluster endpoint access is enabled and false otherwise func noAccess(ces *ClusterEndpoints) bool { return !IsEnabled(ces.PublicAccess) && !IsEnabled(ces.PrivateAccess) diff --git a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go index c5ecc943ba..031ea7bac4 100644 --- a/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go +++ b/pkg/apis/eksctl.io/v1alpha5/zz_generated.deepcopy.go @@ -572,7 +572,7 @@ func (in *ClusterConfig) DeepCopyInto(out *ClusterConfig) { if in.KubernetesNetworkConfig != nil { in, out := &in.KubernetesNetworkConfig, &out.KubernetesNetworkConfig *out = new(KubernetesNetworkConfig) - **out = **in + (*in).DeepCopyInto(*out) } if in.AutoModeConfig != nil { in, out := &in.AutoModeConfig, &out.AutoModeConfig @@ -1064,7 +1064,7 @@ func (in *ClusterStatus) DeepCopyInto(out *ClusterStatus) { if in.KubernetesNetworkConfig != nil { in, out := &in.KubernetesNetworkConfig, &out.KubernetesNetworkConfig *out = new(KubernetesNetworkConfig) - **out = **in + (*in).DeepCopyInto(*out) } return } @@ -1258,6 +1258,32 @@ func (in CustomStringSlice) DeepCopy() CustomStringSlice { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *ElasticLoadBalancing) DeepCopyInto(out *ElasticLoadBalancing) { + *out = *in + if in.Enabled != nil { + in, out := &in.Enabled, &out.Enabled + *out = new(bool) + **out = **in + } + if in.GatewayAPI != nil { + in, out := &in.GatewayAPI, &out.GatewayAPI + *out = new(GatewayAPI) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ElasticLoadBalancing. +func (in *ElasticLoadBalancing) DeepCopy() *ElasticLoadBalancing { + if in == nil { + return nil + } + out := new(ElasticLoadBalancing) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *EndpointService) DeepCopyInto(out *EndpointService) { *out = *in @@ -1377,6 +1403,48 @@ func (in FluxFlags) DeepCopy() FluxFlags { return *out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayAPI) DeepCopyInto(out *GatewayAPI) { + *out = *in + if in.CRDs != nil { + in, out := &in.CRDs, &out.CRDs + *out = new(GatewayAPICRDs) + (*in).DeepCopyInto(*out) + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayAPI. +func (in *GatewayAPI) DeepCopy() *GatewayAPI { + if in == nil { + return nil + } + out := new(GatewayAPI) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GatewayAPICRDs) DeepCopyInto(out *GatewayAPICRDs) { + *out = *in + if in.Managed != nil { + in, out := &in.Managed, &out.Managed + *out = new(bool) + **out = **in + } + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GatewayAPICRDs. +func (in *GatewayAPICRDs) DeepCopy() *GatewayAPICRDs { + if in == nil { + return nil + } + out := new(GatewayAPICRDs) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitOps) DeepCopyInto(out *GitOps) { *out = *in @@ -1727,6 +1795,11 @@ func (in *KubeSchedulerConfig) DeepCopy() *KubeSchedulerConfig { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubernetesNetworkConfig) DeepCopyInto(out *KubernetesNetworkConfig) { *out = *in + if in.ElasticLoadBalancing != nil { + in, out := &in.ElasticLoadBalancing, &out.ElasticLoadBalancing + *out = new(ElasticLoadBalancing) + (*in).DeepCopyInto(*out) + } return } diff --git a/pkg/cfn/builder/cluster.go b/pkg/cfn/builder/cluster.go index 914864f98d..a7026eac01 100644 --- a/pkg/cfn/builder/cluster.go +++ b/pkg/cfn/builder/cluster.go @@ -123,6 +123,22 @@ func (c *ClusterResourceSet) AddAllResources(ctx context.Context) error { return nil } +func (c *ClusterResourceSet) newElasticLoadBalancing() *gfneks.Cluster_ElasticLoadBalancing { + elb := &gfneks.Cluster_ElasticLoadBalancing{ + Enabled: gfnt.NewBoolean(c.spec.ElasticLoadBalancingEnabled()), + } + if knc := c.spec.KubernetesNetworkConfig; knc != nil && knc.ElasticLoadBalancing != nil { + if gatewayAPI := knc.ElasticLoadBalancing.GatewayAPI; gatewayAPI != nil && gatewayAPI.CRDs != nil && gatewayAPI.CRDs.Managed != nil { + elb.GatewayApi = &gfneks.Cluster_GatewayApi{ + Crds: &gfneks.Cluster_GatewayApiCrds{ + Managed: gfnt.NewBoolean(*gatewayAPI.CRDs.Managed), + }, + } + } + } + return elb +} + func (c *ClusterResourceSet) addResourcesForSecurityGroups(vpcID *gfnt.Value) (*clusterSecurityGroup, error) { var refControlPlaneSG, refClusterSharedNodeSG *gfnt.Value @@ -405,9 +421,7 @@ func (c *ClusterResourceSet) addResourcesForControlPlane(subnetDetails *SubnetDe } cluster.KubernetesNetworkConfig = &gfneks.Cluster_KubernetesNetworkConfig{ - ElasticLoadBalancing: &gfneks.Cluster_ElasticLoadBalancing{ - Enabled: gfnt.NewBoolean(true), - }, + ElasticLoadBalancing: c.newElasticLoadBalancing(), } cluster.StorageConfig = &gfneks.Cluster_StorageConfig{ BlockStorage: &gfneks.Cluster_BlockStorage{ @@ -457,6 +471,10 @@ func (c *ClusterResourceSet) addResourcesForControlPlane(subnetDetails *SubnetDe ipFamily = api.IPV4Family } kubernetesNetworkConfig.IpFamily = gfnt.NewString(strings.ToLower(ipFamily)) + // the Auto Mode block above already set this when Auto Mode is enabled + if knc.ElasticLoadBalancing != nil && kubernetesNetworkConfig.ElasticLoadBalancing == nil { + kubernetesNetworkConfig.ElasticLoadBalancing = c.newElasticLoadBalancing() + } } cluster.KubernetesNetworkConfig = kubernetesNetworkConfig if c.spec.ZonalShiftConfig != nil && api.IsEnabled(c.spec.ZonalShiftConfig.Enabled) { diff --git a/pkg/cfn/builder/cluster_elastic_load_balancing_test.go b/pkg/cfn/builder/cluster_elastic_load_balancing_test.go new file mode 100644 index 0000000000..2268ba8f63 --- /dev/null +++ b/pkg/cfn/builder/cluster_elastic_load_balancing_test.go @@ -0,0 +1,100 @@ +package builder_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/tidwall/gjson" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/cfn/builder" + "github.com/weaveworks/eksctl/pkg/testutils/mockprovider" +) + +type elbEntry struct { + autoMode bool + elb *api.ElasticLoadBalancing + expectedEnabled *bool + expectedManaged *bool +} + +var _ = DescribeTable("Cluster template elasticLoadBalancing", func(e elbEntry) { + provider := mockprovider.NewMockProvider() + cfg := api.NewClusterConfig() + api.SetClusterConfigDefaults(cfg) + cfg.VPC = vpcConfig() + cfg.AvailabilityZones = []string{"us-west-2a", "us-west-2b"} + cfg.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ + IPFamily: api.IPV4Family, + ElasticLoadBalancing: e.elb, + } + if e.autoMode { + cfg.AutoModeConfig = &api.AutoModeConfig{Enabled: api.Enabled()} + } + + crs := builder.NewClusterResourceSet(provider.EC2(), provider.STS(), provider.Region(), cfg, nil, false) + Expect(crs.AddAllResources(context.Background())).To(Succeed()) + body, err := crs.RenderJSON() + Expect(err).NotTo(HaveOccurred()) + + elb := gjson.GetBytes(body, "Resources.ControlPlane.Properties.KubernetesNetworkConfig.ElasticLoadBalancing") + if e.expectedEnabled == nil { + Expect(elb.Exists()).To(BeFalse(), "unexpected ElasticLoadBalancing: %s", elb.Raw) + return + } + Expect(elb.Get("Enabled").Exists()).To(BeTrue(), "missing ElasticLoadBalancing.Enabled") + Expect(elb.Get("Enabled").Bool()).To(Equal(*e.expectedEnabled)) + managed := elb.Get("GatewayApi.Crds.Managed") + if e.expectedManaged == nil { + Expect(elb.Get("GatewayApi").Exists()).To(BeFalse(), "unexpected GatewayApi: %s", elb.Get("GatewayApi").Raw) + return + } + Expect(managed.Exists()).To(BeTrue(), "missing GatewayApi.Crds.Managed") + Expect(managed.Bool()).To(Equal(*e.expectedManaged)) +}, + Entry("Auto Mode, ELB unset defaults to enabled", elbEntry{ + autoMode: true, + expectedEnabled: api.Enabled(), + }), + Entry("Auto Mode, ELB explicitly disabled", elbEntry{ + autoMode: true, + elb: &api.ElasticLoadBalancing{Enabled: api.Disabled()}, + expectedEnabled: api.Disabled(), + }), + Entry("no Auto Mode, ELB explicitly enabled", elbEntry{ + elb: &api.ElasticLoadBalancing{Enabled: api.Enabled()}, + expectedEnabled: api.Enabled(), + }), + Entry("no Auto Mode, ELB explicitly disabled", elbEntry{ + elb: &api.ElasticLoadBalancing{Enabled: api.Disabled()}, + expectedEnabled: api.Disabled(), + }), + Entry("no Auto Mode, ELB unset is omitted", elbEntry{}), + Entry("Auto Mode, gatewayApi with managed CRDs", elbEntry{ + autoMode: true, + elb: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{CRDs: &api.GatewayAPICRDs{Managed: api.Enabled()}}, + }, + expectedEnabled: api.Enabled(), + expectedManaged: api.Enabled(), + }), + Entry("Auto Mode, gatewayApi with self-managed CRDs", elbEntry{ + autoMode: true, + elb: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{CRDs: &api.GatewayAPICRDs{Managed: api.Disabled()}}, + }, + expectedEnabled: api.Enabled(), + expectedManaged: api.Disabled(), + }), + Entry("no Auto Mode, gatewayApi with managed CRDs", elbEntry{ + elb: &api.ElasticLoadBalancing{ + Enabled: api.Enabled(), + GatewayAPI: &api.GatewayAPI{CRDs: &api.GatewayAPICRDs{Managed: api.Enabled()}}, + }, + expectedEnabled: api.Enabled(), + expectedManaged: api.Enabled(), + }), +) diff --git a/pkg/ctl/cmdutils/kubernetes_network_config.go b/pkg/ctl/cmdutils/kubernetes_network_config.go new file mode 100644 index 0000000000..d0d1013ea4 --- /dev/null +++ b/pkg/ctl/cmdutils/kubernetes_network_config.go @@ -0,0 +1,36 @@ +package cmdutils + +import ( + "errors" + "fmt" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" +) + +// NewKubernetesNetworkConfigLoader creates a new loader for the Kubernetes network config. +func NewKubernetesNetworkConfigLoader(cmd *Cmd) ClusterConfigLoader { + l := newCommonClusterConfigLoader(cmd) + l.flagsIncompatibleWithConfigFile.Insert( + "enable-elastic-load-balancing", + "cluster", + ) + + l.validateWithConfigFile = func() error { + if cmd.NameArg != "" { + return fmt.Errorf("config file and cluster name argument %s", IncompatibleFlags) + } + knc := l.ClusterConfig.KubernetesNetworkConfig + if knc == nil || knc.ElasticLoadBalancing == nil || knc.ElasticLoadBalancing.Enabled == nil { + return errors.New("field kubernetesNetworkConfig.elasticLoadBalancing.enabled is required") + } + return api.ValidateElasticLoadBalancing(l.ClusterConfig) + } + + l.validateWithoutConfigFile = func() error { + if !cmd.CobraCommand.Flag("enable-elastic-load-balancing").Changed { + return errors.New("--enable-elastic-load-balancing is required when a config file is not specified") + } + return nil + } + return l +} diff --git a/pkg/ctl/update/auto_mode_config.go b/pkg/ctl/update/auto_mode_config.go index c1ed79df10..5f8775989d 100644 --- a/pkg/ctl/update/auto_mode_config.go +++ b/pkg/ctl/update/auto_mode_config.go @@ -64,6 +64,8 @@ func updateAutoMode(cmd *cmdutils.Cmd, options autoModeOptions) error { } ctx, cancel := context.WithTimeout(context.Background(), cmd.ProviderConfig.WaitTimeout) defer cancel() + // capture the requested state first, as NewProviderForExistingCluster overwrites kubernetesNetworkConfig + elb := cmd.ClusterConfig.ElasticLoadBalancingRequest() ctl, err := cmd.NewProviderForExistingCluster(ctx) if err != nil { return err @@ -94,7 +96,8 @@ func updateAutoMode(cmd *cmdutils.Cmd, options autoModeOptions) error { ClusterRoleName: clusterRoleName, Region: cmd.ClusterConfig.Metadata.Region, }, - PodsGetter: clientSet.CoreV1(), + PodsGetter: clientSet.CoreV1(), + ElasticLoadBalancing: elb, } if options.drainNodeGroups { autoModeUpdater.Drainer = &nodeGroupDrainer{ diff --git a/pkg/ctl/utils/update_kubernetes_network_config.go b/pkg/ctl/utils/update_kubernetes_network_config.go new file mode 100644 index 0000000000..3c965d81bc --- /dev/null +++ b/pkg/ctl/utils/update_kubernetes_network_config.go @@ -0,0 +1,103 @@ +package utils + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/aws" + "github.com/aws/aws-sdk-go-v2/service/eks" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" + + "github.com/kris-nova/logger" + "github.com/spf13/cobra" + "github.com/spf13/pflag" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha5" + "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" +) + +func updateKubernetesNetworkConfig(cmd *cmdutils.Cmd, handler func(*cmdutils.Cmd) error) { + cfg := api.NewClusterConfig() + cmd.ClusterConfig = cfg + + cmd.SetDescription("update-kubernetes-network-config", "update Kubernetes network config", "update the Kubernetes network config on a cluster") + + var enableELB bool + cmd.CobraCommand.RunE = func(_ *cobra.Command, args []string) error { + cmd.NameArg = cmdutils.GetNameArg(args) + if err := cmdutils.NewKubernetesNetworkConfigLoader(cmd).Load(); err != nil { + return err + } + if cmd.ClusterConfigFile == "" { + cfg.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{ + ElasticLoadBalancing: &api.ElasticLoadBalancing{ + Enabled: &enableELB, + }, + } + } + return handler(cmd) + } + + cmdutils.AddCommonFlagsForAWS(cmd, &cmd.ProviderConfig, false) + + cmd.FlagSetGroup.InFlagSet("General", func(fs *pflag.FlagSet) { + cmdutils.AddClusterFlag(fs, cfg.Metadata) + cmdutils.AddRegionFlag(fs, &cmd.ProviderConfig) + cmdutils.AddConfigFileFlag(fs, &cmd.ClusterConfigFile) + fs.BoolVar(&enableELB, "enable-elastic-load-balancing", true, "Enable the load balancing capability on a cluster") + }) +} + +func updateKubernetesNetworkConfigCmd(cmd *cmdutils.Cmd) { + updateKubernetesNetworkConfig(cmd, doUpdateKubernetesNetworkConfig) +} + +func doUpdateKubernetesNetworkConfig(cmd *cmdutils.Cmd) error { + cfg := cmd.ClusterConfig + ctx := context.Background() + if cfg.Metadata.Name == "" { + return cmdutils.ErrMustBeSet(cmdutils.ClusterNameFlag(cmd)) + } + // capture the requested state first, as NewProviderForExistingCluster overwrites kubernetesNetworkConfig + elb := cfg.ElasticLoadBalancingRequest() + ctl, err := cmd.NewProviderForExistingCluster(ctx) + if err != nil { + return err + } + makeELBStatus := func(enabled *bool) string { + if api.IsEnabled(enabled) { + return "enabled" + } + return "disabled" + } + if knc := ctl.Status.ClusterInfo.Cluster.KubernetesNetworkConfig; knc != nil && elasticLoadBalancingUpToDate(knc.ElasticLoadBalancing, elb) { + logger.Info("the load balancing capability is already %s", makeELBStatus(elb.Enabled)) + return nil + } + if err := ctl.UpdateClusterConfig(ctx, &eks.UpdateClusterConfigInput{ + Name: aws.String(cfg.Metadata.Name), + KubernetesNetworkConfig: &ekstypes.KubernetesNetworkConfigRequest{ + ElasticLoadBalancing: elb, + }, + }); err != nil { + return fmt.Errorf("updating Kubernetes network config: %w", err) + } + logger.Info("the load balancing capability was %s successfully", makeELBStatus(elb.Enabled)) + return nil +} + +// elasticLoadBalancingUpToDate reports whether the current config already matches the request. +// An unset GatewayApi in the request leaves the current setting unchanged, but a requested GatewayApi +// only matches if the cluster has one, so that `managed: false` still enables it on a cluster without one. +func elasticLoadBalancingUpToDate(current, requested *ekstypes.ElasticLoadBalancing) bool { + if current == nil || aws.ToBool(current.Enabled) != aws.ToBool(requested.Enabled) { + return false + } + if requested.GatewayApi == nil { + return true + } + if current.GatewayApi == nil || current.GatewayApi.Crds == nil { + return false + } + return aws.ToBool(current.GatewayApi.Crds.Managed) == aws.ToBool(requested.GatewayApi.Crds.Managed) +} diff --git a/pkg/ctl/utils/update_kubernetes_network_config_request_test.go b/pkg/ctl/utils/update_kubernetes_network_config_request_test.go new file mode 100644 index 0000000000..1bd3b1f99a --- /dev/null +++ b/pkg/ctl/utils/update_kubernetes_network_config_request_test.go @@ -0,0 +1,62 @@ +package utils + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/aws/aws-sdk-go-v2/aws" + ekstypes "github.com/aws/aws-sdk-go-v2/service/eks/types" +) + +var _ = Describe("elasticLoadBalancingUpToDate", func() { + gatewayAPI := func(managed bool) *ekstypes.GatewayApiConfiguration { + return &ekstypes.GatewayApiConfiguration{ + Crds: &ekstypes.CrdManagementConfiguration{Managed: aws.Bool(managed)}, + } + } + + type entry struct { + current *ekstypes.ElasticLoadBalancing + requested *ekstypes.ElasticLoadBalancing + expected bool + } + + DescribeTable("compares the current and requested config", func(e entry) { + Expect(elasticLoadBalancingUpToDate(e.current, e.requested)).To(Equal(e.expected)) + }, + Entry("current is unset", entry{ + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}, + expected: false, + }), + Entry("enabled matches and gatewayApi is not requested", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(true)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}, + expected: true, + }), + Entry("enabled differs", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(false)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}, + expected: false, + }), + Entry("self-managed CRDs requested on a cluster without gatewayApi", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(false)}, + expected: false, + }), + Entry("managed CRDs requested on a cluster without gatewayApi", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(true)}, + expected: false, + }), + Entry("managed differs", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(true)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(false)}, + expected: false, + }), + Entry("managed matches", entry{ + current: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(false)}, + requested: &ekstypes.ElasticLoadBalancing{Enabled: aws.Bool(true), GatewayApi: gatewayAPI(false)}, + expected: true, + }), + ) +}) diff --git a/pkg/ctl/utils/utils.go b/pkg/ctl/utils/utils.go index caa1b15920..ae7e5d4edd 100644 --- a/pkg/ctl/utils/utils.go +++ b/pkg/ctl/utils/utils.go @@ -31,6 +31,7 @@ func Command(flagGrouping *cmdutils.FlagGrouping) *cobra.Command { cmdutils.AddResourceCmd(flagGrouping, verbCmd, migrateToPodIdentityCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, migrateAccessEntryCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateZonalShiftConfigCmd) + cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateKubernetesNetworkConfigCmd) cmdutils.AddResourceCmd(flagGrouping, verbCmd, updateControlPlaneComponentConfigCmd) return verbCmd diff --git a/pkg/goformation/cloudformation/eks/aws-eks-cluster_elasticloadbalancing.go b/pkg/goformation/cloudformation/eks/aws-eks-cluster_elasticloadbalancing.go index d4efbf2f7c..86fe556822 100644 --- a/pkg/goformation/cloudformation/eks/aws-eks-cluster_elasticloadbalancing.go +++ b/pkg/goformation/cloudformation/eks/aws-eks-cluster_elasticloadbalancing.go @@ -15,6 +15,11 @@ type Cluster_ElasticLoadBalancing struct { // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-elasticloadbalancing.html#cfn-eks-cluster-elasticloadbalancing-enabled Enabled *types.Value `json:"Enabled,omitempty"` + // GatewayApi AWS CloudFormation Property + // Required: false + // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-elasticloadbalancing.html#cfn-eks-cluster-elasticloadbalancing-gatewayapi + GatewayApi *Cluster_GatewayApi `json:"GatewayApi,omitempty"` + // AWSCloudFormationDeletionPolicy represents a CloudFormation DeletionPolicy AWSCloudFormationDeletionPolicy policies.DeletionPolicy `json:"-"` diff --git a/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapi.go b/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapi.go new file mode 100644 index 0000000000..2f797a0e7b --- /dev/null +++ b/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapi.go @@ -0,0 +1,35 @@ +package eks + +import ( + "github.com/weaveworks/eksctl/pkg/goformation/cloudformation/policies" +) + +// Cluster_GatewayApi AWS CloudFormation Resource (AWS::EKS::Cluster.GatewayApi) +// See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-gatewayapi.html +type Cluster_GatewayApi struct { + + // Crds AWS CloudFormation Property + // Required: true + // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-gatewayapi.html#cfn-eks-cluster-gatewayapi-crds + Crds *Cluster_GatewayApiCrds `json:"Crds,omitempty"` + + // AWSCloudFormationDeletionPolicy represents a CloudFormation DeletionPolicy + AWSCloudFormationDeletionPolicy policies.DeletionPolicy `json:"-"` + + // AWSCloudFormationUpdateReplacePolicy represents a CloudFormation UpdateReplacePolicy + AWSCloudFormationUpdateReplacePolicy policies.UpdateReplacePolicy `json:"-"` + + // AWSCloudFormationDependsOn stores the logical ID of the resources to be created before this resource + AWSCloudFormationDependsOn []string `json:"-"` + + // AWSCloudFormationMetadata stores structured data associated with this resource + AWSCloudFormationMetadata map[string]interface{} `json:"-"` + + // AWSCloudFormationCondition stores the logical ID of the condition that must be satisfied for this resource to be created + AWSCloudFormationCondition string `json:"-"` +} + +// AWSCloudFormationType returns the AWS CloudFormation resource type +func (r *Cluster_GatewayApi) AWSCloudFormationType() string { + return "AWS::EKS::Cluster.GatewayApi" +} diff --git a/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapicrds.go b/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapicrds.go new file mode 100644 index 0000000000..b853dbc20f --- /dev/null +++ b/pkg/goformation/cloudformation/eks/aws-eks-cluster_gatewayapicrds.go @@ -0,0 +1,37 @@ +package eks + +import ( + "github.com/weaveworks/eksctl/pkg/goformation/cloudformation/types" + + "github.com/weaveworks/eksctl/pkg/goformation/cloudformation/policies" +) + +// Cluster_GatewayApiCrds AWS CloudFormation Resource (AWS::EKS::Cluster.GatewayApiCrds) +// See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-gatewayapicrds.html +type Cluster_GatewayApiCrds struct { + + // Managed AWS CloudFormation Property + // Required: true + // See: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-eks-cluster-gatewayapicrds.html#cfn-eks-cluster-gatewayapicrds-managed + Managed *types.Value `json:"Managed,omitempty"` + + // AWSCloudFormationDeletionPolicy represents a CloudFormation DeletionPolicy + AWSCloudFormationDeletionPolicy policies.DeletionPolicy `json:"-"` + + // AWSCloudFormationUpdateReplacePolicy represents a CloudFormation UpdateReplacePolicy + AWSCloudFormationUpdateReplacePolicy policies.UpdateReplacePolicy `json:"-"` + + // AWSCloudFormationDependsOn stores the logical ID of the resources to be created before this resource + AWSCloudFormationDependsOn []string `json:"-"` + + // AWSCloudFormationMetadata stores structured data associated with this resource + AWSCloudFormationMetadata map[string]interface{} `json:"-"` + + // AWSCloudFormationCondition stores the logical ID of the condition that must be satisfied for this resource to be created + AWSCloudFormationCondition string `json:"-"` +} + +// AWSCloudFormationType returns the AWS CloudFormation resource type +func (r *Cluster_GatewayApiCrds) AWSCloudFormationType() string { + return "AWS::EKS::Cluster.GatewayApiCrds" +} diff --git a/userdocs/src/usage/auto-mode.md b/userdocs/src/usage/auto-mode.md index 04d0a13f69..f14fa4f5dd 100644 --- a/userdocs/src/usage/auto-mode.md +++ b/userdocs/src/usage/auto-mode.md @@ -114,6 +114,79 @@ autoModeConfig: $ eksctl update auto-mode-config -f cluster.yaml ``` +## Configuring the load balancing capability + +The load balancing capability follows `autoModeConfig.enabled` by default. To configure it explicitly, set +`kubernetesNetworkConfig.elasticLoadBalancing`. The shape of the field is + +```yaml +kubernetesNetworkConfig: + elasticLoadBalancing: + # defaults to autoModeConfig.enabled + enabled: boolean + # optional; enables the Gateway API + gatewayApi: + crds: + # true: AWS manages the Gateway API CRDs; false: you manage them + managed: boolean +``` + +To update the capability on an existing cluster, run + +```shell +$ eksctl utils update-kubernetes-network-config -f cluster.yaml +``` + +or, without a config file, + +```shell +$ eksctl utils update-kubernetes-network-config --cluster auto-mode-cluster --enable-elastic-load-balancing +``` + +### Enabling the Gateway API + +EKS Auto Mode supports the Kubernetes Gateway API through the load balancing capability. Setting +`elasticLoadBalancing.gatewayApi` enables it, and `gatewayApi.crds.managed` decides who owns the lifecycle of the +Gateway API CRDs: + +- `managed: true` lets AWS install and upgrade the Gateway API CRDs. If the cluster already has Gateway API CRDs, they + must be at v1, otherwise EKS rejects the update and makes no change. +- `managed: false` keeps the Gateway API CRDs under your ownership; install and upgrade them yourself. Choose this if + you already run the Gateway API on your cluster or another controller manages the CRDs. + +`elasticLoadBalancing.enabled` must be `true` to set `gatewayApi`. For example: + +```yaml +apiVersion: eksctl.io/v1alpha5 +kind: ClusterConfig + +metadata: + name: auto-mode-cluster + region: us-west-2 + +kubernetesNetworkConfig: + elasticLoadBalancing: + enabled: true + gatewayApi: + crds: + managed: true +``` + +```shell +$ eksctl utils update-kubernetes-network-config -f cluster.yaml +``` + +If `gatewayApi` is omitted, the cluster's current Gateway API setting is left unchanged. The +`--enable-elastic-load-balancing` flag does not configure `gatewayApi`; use a config file to set it. Changing +`managed` from `true` to `false` hands ownership of the CRDs back to you and leaves the installed CRDs in place. + +`gatewayApi` can also be set when creating a cluster by adding the same `kubernetesNetworkConfig` block to the config +file passed to `eksctl create cluster -f`. + +Once the Gateway API is enabled, create a `GatewayClass` that uses the `eks.amazonaws.com/gateway-alb` (L7) or +`eks.amazonaws.com/gateway-nlb` (L4) controller. For more information, see +[Configure the Kubernetes Gateway API in EKS Auto Mode](https://docs.aws.amazon.com/eks/latest/userguide/auto-configure-gateway-api.html). + ## Further information - [EKS Auto Mode][eks-user-guide]