package v1alpha5_test

import (
	"fmt"
	"strings"

	"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"
	cft "github.com/weaveworks/eksctl/pkg/cfn/template"
)

var _ = Describe("ClusterConfig validation", func() {
	Describe("nodeGroups[*].name", func() {
		var (
			cfg *api.ClusterConfig
			err error
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "ng0"
			ng1 := cfg.NewNodeGroup()
			ng1.Name = "ng1"
		})

		It("should handle unique nodegroups", func() {
			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())

			for i, ng := range cfg.NodeGroups {
				err = api.ValidateNodeGroup(i, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		It("should handle non-unique nodegroups", func() {
			cfg.NodeGroups[0].Name = "ng"
			cfg.NodeGroups[1].Name = "ng"

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
		})

		It("should handle unamed nodegroups", func() {
			cfg.NodeGroups[0].Name = ""

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
		})
	})

	Describe("nodeGroups[*].name validation", func() {
		var (
			cfg *api.ClusterConfig
			err error
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "ng_invalid-name-10"
			ng1 := cfg.NewNodeGroup()
			ng1.Name = "ng100_invalid_name"
			ng2 := cfg.NewNodeGroup()
			ng2.Name = "ng100@invalid-name"
		})

		It("should reject invalid nodegroup names", func() {
			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())

			for i, ng := range cfg.NodeGroups {
				err = api.ValidateNodeGroup(i, ng, cfg)
				Expect(err).To(HaveOccurred())
			}
		})
	})

	Describe("nodeGroups[*].containerRuntime validation", func() {

		It("allows accepted values", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"

			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeContainerD)
			err := api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).NotTo(HaveOccurred())

			// Docker container runtime is only supported up to K8s version 1.23
			cfg.Metadata.Version = api.Version1_23

			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeDockerForWindows)
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).NotTo(HaveOccurred())

			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeDockerD)
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should reject invalid container runtime", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.ContainerRuntime = aws.String("invalid")
			err := api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).To(HaveOccurred())
		})

		It("should reject docker runtime if AMI Family is AmazonLinux2023", func() {
			cfg := api.NewClusterConfig()
			cfg.Metadata.Version = api.Version1_23
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMIFamily = api.NodeImageFamilyAmazonLinux2023
			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeDockerD)
			err := api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("only %s is supported for container runtime on %s nodes", api.ContainerRuntimeContainerD, api.NodeImageFamilyAmazonLinux2023))))
		})

		It("should reject docker runtime if version is 1.24 or greater", func() {
			cfg := api.NewClusterConfig()
			cfg.Metadata.Version = api.DockershimDeprecationVersion
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeDockerD)
			err := api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring(fmt.Sprintf("only %s is supported for container runtime, starting with EKS version %s", api.ContainerRuntimeContainerD, api.DockershimDeprecationVersion))))
		})

		It("containerd cannot be set together with overrideBootstrapCommand", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMIFamily = api.NodeImageFamilyAmazonLinux2
			ng0.ContainerRuntime = aws.String(api.ContainerRuntimeContainerD)
			ng0.OverrideBootstrapCommand = aws.String("bootstrap command")
			err := api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).To(MatchError(ContainSubstring("overrideBootstrapCommand overwrites container runtime setting; please use --container-runtime in the bootsrap script instead")))
		})
	})

	Describe("nodeGroups[*].ami validation", func() {
		It("should require ami family if ami is set", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError(ContainSubstring("when using a custom AMI, amiFamily needs to be explicitly set via config file or via --node-ami-family flag")))
		})
		It("should require overrideBootstrapCommand if ami is set", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyAmazonLinux2
			errMsg := fmt.Sprintf("overrideBootstrapCommand is required when using a custom AMI based on %s", ng0.AMIFamily)
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError(ContainSubstring(errMsg)))
		})
		It("should not require overrideBootstrapCommand if ami is set and type is Bottlerocket", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyBottlerocket
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
		})
		It("should not require overrideBootstrapCommand if ami is set and type is Windows", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyWindowsServer2019CoreContainer
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
		})
		It("should not throw an error if overrideBootstrapCommand is set and type is Windows", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyWindowsServer2019CoreContainer
			ng0.OverrideBootstrapCommand = aws.String("echo 'yo'")
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
		})
		It("should throw an error if overrideBootstrapCommand is set and type is Bottlerocket", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyBottlerocket
			ng0.OverrideBootstrapCommand = aws.String("echo 'yo'")
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError(ContainSubstring(fmt.Sprintf("overrideBootstrapCommand is not supported for %s nodegroups", api.NodeImageFamilyBottlerocket))))
		})
		It("should accept ami with a overrideBootstrapCommand set", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.AMI = "ami-1234"
			ng0.AMIFamily = api.NodeImageFamilyAmazonLinux2
			ng0.OverrideBootstrapCommand = aws.String("echo 'yo'")
			Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
		})
	})

	Describe("nodeGroups[*].maxInstanceLifetime validation", func() {
		It("should reject if value is below a day", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.MaxInstanceLifetime = aws.Int(5)
			err := api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).To(MatchError(ContainSubstring("maximum instance lifetime must have a minimum value of 86,400 seconds (one day), but was: 5")))
		})
		It("setting it if greater than or equal to one day", func() {
			cfg := api.NewClusterConfig()
			ng0 := cfg.NewNodeGroup()
			ng0.Name = "node-group"
			ng0.MaxInstanceLifetime = aws.Int(api.OneDay)
			err := api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
			err = api.ValidateNodeGroup(0, ng0, cfg)
			Expect(err).NotTo(HaveOccurred())
			ng0.MaxInstanceLifetime = aws.Int(api.OneDay + 1000)
			Expect(err).NotTo(HaveOccurred())
		})
	})

	Describe("nodeGroups[*].volumeX", func() {
		var (
			cfg *api.ClusterConfig
			ng0 *api.NodeGroup
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			ng0 = cfg.NewNodeGroup()
			ng0.Name = "ng0"
		})

		When("volumeIOPS is set", func() {
			BeforeEach(func() {
				ng0.VolumeIOPS = aws.Int(3000)
			})

			When("VolumeType is gp3", func() {
				BeforeEach(func() {
					*ng0.VolumeType = api.NodeVolumeTypeGP3
				})

				It("does not fail", func() {
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
				})

				When(fmt.Sprintf("the value of volumeIOPS is < %d", api.MinGP3Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MinGP3Iops - 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 3000-16000"))
					})
				})

				When(fmt.Sprintf("the value of volumeIOPS is > %d", api.MaxGP3Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MaxGP3Iops + 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 3000-16000"))
					})
				})
			})

			When("VolumeType is io1", func() {
				BeforeEach(func() {
					*ng0.VolumeType = api.NodeVolumeTypeIO1
				})

				It("does not fail", func() {
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
				})

				When(fmt.Sprintf("the value of volumeIOPS is < %d", api.MinIO1Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MinIO1Iops - 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-64000"))
					})
				})

				When(fmt.Sprintf("the value of volumeIOPS is > %d", api.MaxIO1Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MaxIO1Iops + 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-64000"))
					})
				})
			})

			When("VolumeType is io2", func() {
				BeforeEach(func() {
					*ng0.VolumeType = api.NodeVolumeTypeIO2
				})

				It("does not fail", func() {
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
				})

				When(fmt.Sprintf("the value of volumeIOPS is < %d", api.MinIO2Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MinIO2Iops - 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-256000"))
					})
				})

				When(fmt.Sprintf("the value of volumeIOPS is > %d", api.MaxIO2Iops), func() {
					It("returns an error", func() {
						ng0.VolumeIOPS = aws.Int(api.MaxIO2Iops + 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeIOPS must be within range 100-256000"))
					})
				})
			})

			When("VolumeType is one for which IOPS is not supported", func() {
				It("returns an error", func() {
					*ng0.VolumeType = api.NodeVolumeTypeGP2
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("nodeGroups[0].volumeIOPS is only supported for io1, io2 and gp3 volume types"))
				})
			})
		})

		When("volumeThroughput is set", func() {
			BeforeEach(func() {
				ng0.VolumeThroughput = aws.Int(125)
			})

			When("VolumeType is gp3", func() {
				BeforeEach(func() {
					*ng0.VolumeType = api.NodeVolumeTypeGP3
				})

				It("does not fail", func() {
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(Succeed())
				})

				When(fmt.Sprintf("the value of volumeThroughput is < %d", api.MinThroughput), func() {
					It("returns an error", func() {
						ng0.VolumeThroughput = aws.Int(api.MinThroughput - 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeThroughput must be within range 125-1000"))
					})
				})

				When(fmt.Sprintf("the value of volumeIOPS is > %d", api.MaxThroughput), func() {
					It("returns an error", func() {
						ng0.VolumeThroughput = aws.Int(api.MaxThroughput + 1)
						Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("value for nodeGroups[0].volumeThroughput must be within range 125-1000"))
					})
				})
			})

			When("VolumeType is one for which Throughput is not supported", func() {
				It("returns an error", func() {
					*ng0.VolumeType = api.NodeVolumeTypeGP2
					Expect(api.ValidateNodeGroup(0, ng0, cfg)).To(MatchError("nodeGroups[0].volumeThroughput is only supported for gp3 volume type"))
				})
			})
		})
	})

	Describe("nodeGroups[*].iam", func() {
		var (
			cfg *api.ClusterConfig
			err error
			ng1 *api.NodeGroup
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()

			ng0 := cfg.NewNodeGroup()
			ng0.Name = "ng0"

			ng0.IAM.AttachPolicy = cft.MakePolicyDocument(
				cft.MapOfInterfaces{
					"Effect": "Allow",
					"Action": []string{
						"s3:Get*",
					},
					"Resource": "*",
				},
			)
			ng0.IAM.AttachPolicyARNs = []string{
				"arn:aws:iam::aws:policy/Foo",
				"arn:aws:iam::aws:policy/Bar",
			}
			ng0.IAM.WithAddonPolicies.ExternalDNS = api.Enabled()
			ng0.IAM.WithAddonPolicies.AWSLoadBalancerController = api.Enabled()
			ng0.IAM.WithAddonPolicies.ImageBuilder = api.Enabled()

			ng1 = cfg.NewNodeGroup()
			ng1.Name = "ng1"
		})

		JustBeforeEach(func() {
			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())

			for i, ng := range cfg.NodeGroups {
				err = api.ValidateNodeGroup(i, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			}
		})

		It("should allow setting only instanceProfileARN", func() {
			ng1.IAM.InstanceProfileARN = "p1"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should allow setting only instanceRoleARN", func() {
			ng1.IAM.InstanceRoleARN = "r1"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should not allow setting AWSLoadBalancerController and albIngress", func() {
			ng1.IAM.WithAddonPolicies.AWSLoadBalancerController = aws.Bool(true)
			ng1.IAM.WithAddonPolicies.DeprecatedALBIngress = aws.Bool(true)

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(MatchError(`"awsLoadBalancerController" and "albIngress" cannot both be configured, please use "awsLoadBalancerController" as "albIngress" is deprecated`))
		})

		It("should allow setting instanceProfileARN and instanceRoleARN", func() {
			ng1.IAM.InstanceProfileARN = "p1"
			ng1.IAM.InstanceRoleARN = "r1"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should not allow setting instanceProfileARN and instanceRoleName", func() {
			ng1.IAM.InstanceProfileARN = "p1"
			ng1.IAM.InstanceRoleName = "aRole"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceProfileARN and nodeGroups[1].iam.instanceRoleName cannot be set at the same time"))
		})

		It("should not allow setting instanceProfileARN and instanceRolePermissionsBoundary", func() {
			ng1.IAM.InstanceProfileARN = "p1"
			ng1.IAM.InstanceRolePermissionsBoundary = "aPolicy"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceProfileARN and nodeGroups[1].iam.instanceRolePermissionsBoundary cannot be set at the same time"))
		})

		It("should not allow setting instanceRoleARN and instanceRolePermissionsBoundary", func() {
			ng1.IAM.InstanceRoleARN = "r1"
			ng1.IAM.InstanceRolePermissionsBoundary = "p1"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceRoleARN and nodeGroups[1].iam.instanceRolePermissionsBoundary cannot be set at the same time"))
		})

		It("should not allow setting instanceRoleARN and instanceRoleName", func() {
			ng1.IAM.InstanceRoleARN = "r1"
			ng1.IAM.InstanceRoleName = "aRole"

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceRoleARN and nodeGroups[1].iam.instanceRoleName cannot be set at the same time"))
		})

		It("should not allow setting instanceRoleARN and attachPolicy", func() {
			ng1.IAM.InstanceRoleARN = "r1"
			ng1.IAM.AttachPolicy = cft.MakePolicyDocument(
				cft.MapOfInterfaces{
					"Effect": "Allow",
					"Action": []string{
						"s3:Get*",
					},
					"Resource": "*",
				},
			)

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceRoleARN and nodeGroups[1].iam.attachPolicy cannot be set at the same time"))
		})

		It("should not allow setting instanceRoleARN and attachPolicyARNs", func() {
			ng1.IAM.InstanceRoleARN = "r1"
			ng1.IAM.AttachPolicyARNs = []string{
				"arn:aws:iam::aws:policy/Foo",
				"arn:aws:iam::aws:policy/Bar",
			}

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceRoleARN and nodeGroups[1].iam.attachPolicyARNs cannot be set at the same time"))
		})

		It("should not allow setting instanceRoleARN and withAddonPolicies", func() {
			ng1.IAM.InstanceRoleARN = "r1"

			ng1.IAM.WithAddonPolicies.ExternalDNS = api.Enabled()

			err = api.ValidateNodeGroup(1, ng1, cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(Equal("nodeGroups[1].iam.instanceRoleARN and nodeGroups[1].iam.withAddonPolicies.externalDNS cannot be set at the same time"))
		})

	})

	Describe("iam.{withOIDC,serviceAccounts}", func() {
		var (
			cfg *api.ClusterConfig
			err error
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
		})

		It("should pass when iam.withOIDC is unset", func() {
			cfg.IAM.WithOIDC = nil

			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should pass when iam.withOIDC is disabled", func() {
			cfg.IAM.WithOIDC = api.Disabled()

			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should pass when iam.withOIDC is enabled", func() {
			cfg.IAM.WithOIDC = api.Enabled()

			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should fail when iam.withOIDC is disabled and some iam.serviceAccounts are given", func() {
			cfg.IAM.WithOIDC = api.Disabled()

			cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{{}, {}}
			cfg.IAM.ServiceAccounts[0].Name = "sa-1"
			cfg.IAM.ServiceAccounts[1].Name = "sa-2"

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(HavePrefix("iam.withOIDC must be enabled explicitly"))
		})

		It("should pass when iam.withOIDC is enabled and some iam.serviceAccounts are given", func() {
			cfg.IAM.WithOIDC = api.Enabled()

			cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{{}, {}}

			cfg.IAM.ServiceAccounts[0].Name = "sa-1"
			cfg.IAM.ServiceAccounts[0].AttachPolicyARNs = []string{""}

			cfg.IAM.ServiceAccounts[1].Name = "sa-2"
			cfg.IAM.ServiceAccounts[1].AttachPolicy = map[string]interface{}{"Statement": "foo"}

			err = api.ValidateClusterConfig(cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("should fail when unnamed iam.serviceAccounts[1] is given", func() {
			cfg.IAM.WithOIDC = api.Enabled()

			cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{{}, {}}
			cfg.IAM.ServiceAccounts[0].Name = "sa-1"
			cfg.IAM.ServiceAccounts[0].AttachPolicyARNs = []string{""}

			cfg.IAM.ServiceAccounts[1].Name = ""
			cfg.IAM.ServiceAccounts[1].AttachPolicy = map[string]interface{}{"Statement": "foo"}

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(HavePrefix("iam.serviceAccounts[1].name must be set"))
		})

		It("should fail when iam.serviceAccounts[1] has no policy", func() {
			cfg.IAM.WithOIDC = api.Enabled()

			cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{{}, {}}
			cfg.IAM.ServiceAccounts[0].Name = "sa-1"
			cfg.IAM.ServiceAccounts[0].AttachPolicyARNs = []string{""}

			cfg.IAM.ServiceAccounts[1].Name = "sa-2"

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())

			Expect(err.Error()).To(SatisfyAll(
				ContainSubstring("iam.serviceAccounts[1]"),
				ContainSubstring("attachPolicy"),
				ContainSubstring("must be set"),
			))
		})

		It("should fail when non-uniquely named iam.serviceAccounts are given", func() {
			cfg.IAM.WithOIDC = api.Enabled()

			cfg.IAM.ServiceAccounts = []*api.ClusterIAMServiceAccount{{}, {}, {}, {}, {}}
			cfg.IAM.ServiceAccounts[0].Name = "sa-1"
			cfg.IAM.ServiceAccounts[0].AttachPolicyARNs = []string{""}

			cfg.IAM.ServiceAccounts[1].Name = "sa-2"
			cfg.IAM.ServiceAccounts[1].Namespace = "ns-2"
			cfg.IAM.ServiceAccounts[1].AttachPolicyARNs = []string{""}

			cfg.IAM.ServiceAccounts[2].Name = "sa-2"
			cfg.IAM.ServiceAccounts[2].Namespace = "ns-2"
			cfg.IAM.ServiceAccounts[2].AttachPolicy = map[string]interface{}{"Statement": "foo"}

			cfg.IAM.ServiceAccounts[3].Name = "sa-3"
			cfg.IAM.ServiceAccounts[3].AttachPolicy = map[string]interface{}{"Statement": "foo"}

			cfg.IAM.ServiceAccounts[4].Name = "sa-1"
			cfg.IAM.ServiceAccounts[4].AttachPolicy = map[string]interface{}{"Statement": "foo"}

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(HavePrefix("<namespace>/<name> of iam.serviceAccounts[2] \"ns-2/sa-2\" is not unique"))

			cfg.IAM.ServiceAccounts[2].Namespace = "ns-3"

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(HavePrefix("<namespace>/<name> of iam.serviceAccounts[4] \"/sa-1\" is not unique"))
		})
	})

	Describe("cloudWatch.clusterLogging", func() {
		var (
			cfg *api.ClusterConfig
			err error
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
		})

		It("should handle known types", func() {
			cfg.CloudWatch.ClusterLogging.EnableTypes = []string{"api"}
			Expect(api.ValidateClusterConfig(cfg)).To(Succeed())
		})

		It("should handle unknown types", func() {
			cfg.CloudWatch.ClusterLogging.EnableTypes = []string{"anything"}

			err = api.ValidateClusterConfig(cfg)
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring(`log type "anything" (cloudWatch.clusterLogging.enableTypes[0]) is unknown`)))
		})
	})

	type logRetentionEntry struct {
		logging *api.ClusterCloudWatchLogging

		expectedErr string
	}

	DescribeTable("CloudWatch log retention", func(l logRetentionEntry) {
		clusterConfig := api.NewClusterConfig()
		clusterConfig.CloudWatch.ClusterLogging = l.logging
		err := api.ValidateClusterConfig(clusterConfig)
		if l.expectedErr != "" {
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring(l.expectedErr)))
			return
		}

		Expect(err).NotTo(HaveOccurred())
	},
		Entry("invalid value", logRetentionEntry{
			logging: &api.ClusterCloudWatchLogging{
				LogRetentionInDays: 42,
				EnableTypes:        []string{"api"},
			},
			expectedErr: `invalid value 42 for logRetentionInDays; supported values are [1 3 5 7`,
		}),

		Entry("valid value", logRetentionEntry{
			logging: &api.ClusterCloudWatchLogging{
				LogRetentionInDays: 545,
				EnableTypes:        []string{"api"},
			},
		}),

		Entry("log retention without enableTypes", logRetentionEntry{
			logging: &api.ClusterCloudWatchLogging{
				LogRetentionInDays: 545,
			},
			expectedErr: "cannot set cloudWatch.clusterLogging.logRetentionInDays without enabling log types",
		}),
	)

	type vpcHostnameTypeEntry struct {
		vpc         *api.ClusterVPC
		expectedErr string
	}

	DescribeTable("vpc.hostnameType", func(v vpcHostnameTypeEntry) {
		clusterConfig := api.NewClusterConfig()
		clusterConfig.VPC = v.vpc
		err := api.ValidateClusterConfig(clusterConfig)
		if v.expectedErr != "" {
			Expect(err).To(HaveOccurred())
			Expect(err).To(MatchError(ContainSubstring(v.expectedErr)))
		} else {
			Expect(err).NotTo(HaveOccurred())
		}
	},
		Entry("invalid value", vpcHostnameTypeEntry{
			vpc: &api.ClusterVPC{
				HostnameType: "invalid",
			},
			expectedErr: `invalid value "invalid" for vpc.hostnameType; supported values are`,
		}),

		Entry("valid value", vpcHostnameTypeEntry{
			vpc: &api.ClusterVPC{
				HostnameType: "ip-name",
			},
		}),

		Entry("valid value", vpcHostnameTypeEntry{
			vpc: &api.ClusterVPC{
				HostnameType: "resource-name",
			},
		}),

		Entry("hostnameType with a pre-existing VPC", vpcHostnameTypeEntry{
			vpc: &api.ClusterVPC{
				HostnameType: "resource-name",
				Subnets: &api.ClusterSubnets{
					Private: map[string]api.AZSubnetSpec{
						"us-west-2a": {
							ID: "subnet-1234",
						},
					},
				},
			},
			expectedErr: "vpc.hostnameType is not supported with a pre-existing VPC",
		}),
	)

	Describe("Cluster Endpoint access", func() {
		var cfg *api.ClusterConfig

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
		})

		When("VPC is not set", func() {
			It("should have cluster endpoint access", func() {
				err := api.ValidateClusterConfig(cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})

		When("VPC is set", func() {
			BeforeEach(func() {
				cfg.VPC = &api.ClusterVPC{}
			})

			When("no cluster endpoint config is set", func() {
				It("should not error", func() {
					err := api.ValidateClusterConfig(cfg)
					Expect(err).NotTo(HaveOccurred())
				})
			})

			When("cluster endpoint config exists", func() {
				It("should not error on private=true, public=true", func() {
					cfg.VPC.ClusterEndpoints =
						&api.ClusterEndpoints{PrivateAccess: api.Enabled(), PublicAccess: api.Enabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).NotTo(HaveOccurred())
				})

				It("should not error on private=false, public=true", func() {
					cfg.VPC.ClusterEndpoints =
						&api.ClusterEndpoints{PrivateAccess: api.Disabled(), PublicAccess: api.Enabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).NotTo(HaveOccurred())
				})

				It("should not error on private=true, public=false", func() {
					cfg.VPC.ClusterEndpoints =
						&api.ClusterEndpoints{PrivateAccess: api.Enabled(), PublicAccess: api.Disabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).NotTo(HaveOccurred())
				})

				It("should error on private=false, public=false", func() {
					cfg.VPC.ClusterEndpoints = &api.ClusterEndpoints{PrivateAccess: api.Disabled(), PublicAccess: api.Disabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).To(MatchError(api.ErrClusterEndpointNoAccess))
				})

				It("should error on private=false, public=nil", func() {
					cfg.VPC.ClusterEndpoints = &api.ClusterEndpoints{PrivateAccess: api.Disabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).To(MatchError(api.ErrClusterEndpointNoAccess))
				})

				It("should error on private=nil, public=false", func() {
					cfg.VPC.ClusterEndpoints = &api.ClusterEndpoints{PublicAccess: api.Disabled()}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).To(MatchError(api.ErrClusterEndpointNoAccess))
				})
			})
		})
	})

	type localZonesEntry struct {
		updateClusterConfig func(*api.ClusterConfig)

		expectedErr string
	}

	DescribeTable("AWS Local Zones", func(e localZonesEntry) {
		clusterConfig := api.NewClusterConfig()
		e.updateClusterConfig(clusterConfig)
		clusterConfig.LocalZones = []string{"us-west-2-lax-1a", "us-west-2-lax-1b"}

		err := api.ValidateClusterConfig(clusterConfig)
		if e.expectedErr != "" {
			Expect(err).To(MatchError(ContainSubstring(e.expectedErr)))
			return
		}
		Expect(err).NotTo(HaveOccurred())
	},
		Entry("custom VPC", localZonesEntry{
			updateClusterConfig: func(clusterConfig *api.ClusterConfig) {
				clusterConfig.VPC = &api.ClusterVPC{
					Network: api.Network{
						ID: "vpc-custom",
					},
				}

			},
			expectedErr: "localZones are not supported with a pre-existing VPC",
		}),

		Entry("HighlyAvailable NAT gateway", localZonesEntry{
			updateClusterConfig: func(clusterConfig *api.ClusterConfig) {
				clusterConfig.VPC = &api.ClusterVPC{
					NAT: &api.ClusterNAT{
						Gateway: aws.String("HighlyAvailable"),
					},
				}
			},
			expectedErr: "HighlyAvailable NAT gateway is not supported for localZones",
		}),

		Entry("private cluster", localZonesEntry{
			updateClusterConfig: func(clusterConfig *api.ClusterConfig) {
				clusterConfig.PrivateCluster = &api.PrivateCluster{
					Enabled: true,
				}
			},

			expectedErr: "localZones cannot be used in a fully-private cluster",
		}),

		Entry("IPv6", localZonesEntry{
			updateClusterConfig: func(clusterConfig *api.ClusterConfig) {
				clusterConfig.KubernetesNetworkConfig = &api.KubernetesNetworkConfig{
					IPFamily: api.IPV6Family,
				}
				clusterConfig.Addons = []*api.Addon{
					{
						Name: "vpc-cni",
					},
					{
						Name: "coredns",
					},
					{
						Name: "kube-proxy",
					},
				}
				clusterConfig.IAM.WithOIDC = api.Enabled()
				clusterConfig.VPC.NAT = nil
			},

			expectedErr: "localZones are not supported with IPv6",
		}),
	)

	Describe("ValidatePrivateCluster", func() {
		var (
			cfg *api.ClusterConfig
			vpc *api.ClusterVPC
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			vpc = api.NewClusterVPC(false)
			cfg.VPC = vpc
			cfg.PrivateCluster = &api.PrivateCluster{
				Enabled: true,
			}
		})
		When("private cluster is enabled", func() {
			It("validates the config", func() {
				err := api.ValidateClusterConfig(cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})
		When("vpc is provided but no private subnets", func() {
			It("fails the validation", func() {
				cfg.VPC.Subnets = &api.ClusterSubnets{}
				cfg.VPC.ID = "id"
				err := api.ValidateClusterConfig(cfg)
				Expect(err).To(MatchError(ContainSubstring("vpc.subnets.private must be specified in a fully-private cluster when a pre-existing VPC is supplied")))
			})
		})
		When("additional endpoints are defined with skip endpoints", func() {
			It("fails the validation", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{api.EndpointServiceCloudWatch.Name}
				cfg.PrivateCluster.SkipEndpointCreation = true
				err := api.ValidateClusterConfig(cfg)
				Expect(err).To(MatchError(ContainSubstring("privateCluster.additionalEndpointServices cannot be set when privateCluster.skipEndpointCreation is true")))
			})
		})
		When("additional endpoints are defined", func() {
			It("validates the endpoint configuration", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{api.EndpointServiceCloudWatch.Name}
				err := api.ValidateClusterConfig(cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})
		When("additional endpoints are defined incorrectly", func() {
			It("fails the endpoint validation", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{"unknown"}
				err := api.ValidateClusterConfig(cfg)
				Expect(err).To(MatchError(ContainSubstring("invalid value in privateCluster.additionalEndpointServices")))
			})
		})
		When("private cluster is enabled with skip endpoints", func() {
			It("does not fail the validation", func() {
				cfg.PrivateCluster.SkipEndpointCreation = true
				err := api.ValidateClusterConfig(cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	type remoteNetworkConfigEntry struct {
		overrideConfig func(*api.ClusterConfig)
		expectedErr    string
	}
	DescribeTable("RemoteNetworkConfig", func(e remoteNetworkConfigEntry) {
		cfg := api.NewClusterConfig()
		api.SetClusterConfigDefaults(cfg)
		api.SetClusterEndpointAccessDefaults(cfg.VPC)
		gatewayID := api.VPCGateway("tgw-1234")
		cfg.RemoteNetworkConfig = &api.RemoteNetworkConfig{
			RemoteNodeNetworks: []*api.RemoteNetwork{{CIDRs: []string{"192.168.0.1"}}},
			VPCGatewayID:       &gatewayID,
		}
		e.overrideConfig(cfg)
		Expect(api.ValidateClusterConfig(cfg)).To(MatchError(ContainSubstring(e.expectedErr)))
	},

		Entry("public cluster endpoint access is disabled", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.VPC.ClusterEndpoints = &api.ClusterEndpoints{
					PublicAccess: api.Disabled(),
				}
				cc.RemoteNetworkConfig.IAM = &api.RemoteNodesIAM{
					Provider: aws.String("BLOB"),
				}
			},
		}),
		Entry("fully private EKS cluster", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.PrivateCluster = &api.PrivateCluster{
					Enabled: true,
				}
				cc.RemoteNetworkConfig.IAM = &api.RemoteNodesIAM{
					Provider: aws.String("BLOB"),
				}
			},
		}),
		Entry("IPv6 family", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.KubernetesNetworkConfig.IPFamily = api.IPV6Family
				// setup other pre-requisites for IPv6
				cc.IAM.WithOIDC = aws.Bool(true)
				cc.Addons = []*api.Addon{
					{Name: api.VPCCNIAddon},
					{Name: api.CoreDNSAddon},
					{Name: api.KubeProxyAddon},
				}
			},
			expectedErr: "remoteNetworkConfig is not supported on EKS cluster configured with IPv6 address family",
		}),
		Entry("authenticationMode is ConfigMap", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.AccessConfig = &api.AccessConfig{
					AuthenticationMode: ekstypes.AuthenticationModeConfigMap,
				}
			},
			expectedErr: fmt.Sprintf("remoteNetworkConfig requires authenticationMode to be either %q or %q", ekstypes.AuthenticationModeApiAndConfigMap, ekstypes.AuthenticationModeApi),
		}),
		Entry("remoteNodeNetworks is empty", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.RemoteNetworkConfig = &api.RemoteNetworkConfig{}
			},
			expectedErr: "remoteNetworkConfig.remoteNodeNetworks must be set and non-empty",
		}),
		Entry("both vpcGatewayID and pre-existing VPC are set", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.VPC.ID = "vpc-1234"
			},
			expectedErr: "remoteNetworkConfig.vpcGatewayID is not supported when using pre-existing VPC",
		}),
		Entry("unsupported vpcGateway type", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				gatewayID := api.VPCGateway("igw-1234")
				cc.RemoteNetworkConfig.VPCGatewayID = &gatewayID
			},
			expectedErr: "invalid value \"igw-1234\" provided for remoteNetworkConfig.vpcGatewayID",
		}),
		Entry("unsupported credentials provider", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.RemoteNetworkConfig.IAM = &api.RemoteNodesIAM{
					Provider: aws.String("BLOB"),
				}
			},
			expectedErr: "invalid value \"BLOB\" provided for remoteNetworkConfig.iam.provider",
		}),
		Entry("CABundle cert is missing when using IRA", remoteNetworkConfigEntry{
			overrideConfig: func(cc *api.ClusterConfig) {
				cc.RemoteNetworkConfig.IAM = &api.RemoteNodesIAM{
					Provider: aws.String("IRA"),
				}
			},
			expectedErr: "remoteNetworkConfig.iam.caBundleCert is required when using IAMRolesAnywhere credentials provider",
		}),
	)

	Describe("network config", func() {
		var (
			cfg *api.ClusterConfig
			vpc *api.ClusterVPC
			err error
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			vpc = api.NewClusterVPC(false)
			cfg.VPC = vpc
		})

		Context("ipFamily", func() {
			It("should not error default ipFamily setting", func() {
				err = api.ValidateClusterConfig(cfg)
				Expect(err).ToNot(HaveOccurred())
				Expect(cfg.KubernetesNetworkConfig.IPFamily).To(Equal(api.IPV4Family))
			})

			When("ipFamily isn't IPv4 or IPv6", func() {
				It("returns an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = "invalid"
					err = api.ValidateClusterConfig(cfg)
					Expect(err).To(MatchError(ContainSubstring(`invalid value "invalid" for ipFamily; allowed are IPv4 and IPv6`)))
				})
			})

			When("ipFamily is empty", func() {
				It("treats it as IPv4 and does not return an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = ""
					Expect(api.ValidateClusterConfig(cfg)).To(Succeed())
				})
			})

			When("ipFamily is set to IPv6", func() {
				JustBeforeEach(func() {
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
				})

				It("accepts that setting", func() {
					cfg.VPC.NAT = nil
					cfg.VPC.IPv6Cidr = "foo"
					cfg.VPC.IPv6Pool = "bar"
					cfg.Addons = append(cfg.Addons,
						&api.Addon{Name: api.KubeProxyAddon},
						&api.Addon{Name: api.CoreDNSAddon},
						&api.Addon{Name: api.VPCCNIAddon},
					)
					cfg.IAM = &api.ClusterIAM{
						WithOIDC: api.Enabled(),
					}
					cfg.Metadata.Version = api.Version1_21
					err = cfg.ValidateVPCConfig()
					Expect(err).ToNot(HaveOccurred())
					cfg.Metadata.Version = "1.31"
					err = cfg.ValidateVPCConfig()
					Expect(err).ToNot(HaveOccurred())
				})

				When("the casing of ipv6 isn't standard", func() {
					It("accepts that setting", func() {
						cfg.KubernetesNetworkConfig.IPFamily = "iPv6"
						cfg.VPC.NAT = nil
						cfg.VPC.IPv6Cidr = "foo"
						cfg.VPC.IPv6Pool = "bar"
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Metadata.Version = api.Version1_21
						err = cfg.ValidateVPCConfig()
						Expect(err).ToNot(HaveOccurred())
						cfg.Metadata.Version = "1.31"
						err = cfg.ValidateVPCConfig()
						Expect(err).ToNot(HaveOccurred())
					})
				})

				When("ipFamily is set to IPv6 but version is not or too low", func() {
					It("returns an error", func() {
						cfg.VPC.NAT = nil
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Metadata.Version = ""
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("failed to convert  cluster version to semver: unable to parse first version")))
						cfg.Metadata.Version = api.Version1_12
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("cluster version must be >= 1.21")))
					})
				})

				When("ipFamily is set to IPv6 but no managed addons are provided", func() {
					It("returns an error including which addons are missing", func() {
						cfg.VPC.NAT = nil
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.KubeProxyAddon})
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("the default core addons must be defined for IPv6; missing addon(s): vpc-cni, coredns")))
					})
				})

				When("ipFamily is set to IPV6, OIDC is disabled", func() {
					JustBeforeEach(func() {
						cfg.VPC.NAT = nil
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Disabled(),
						}
						cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.KubeProxyAddon}, &api.Addon{Name: api.CoreDNSAddon})
					})
					When("Pod identity addon is missing", func() {
						It("returns an error", func() {
							cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.VPCCNIAddon})
							err = api.ValidateClusterConfig(cfg)
							Expect(err).To(MatchError(ContainSubstring("either pod identity or oidc needs to be enabled if IPv6 is set; set either one or use EKS Auto Mode")))
						})
					})

					When("Pod identity addon is present", func() {
						JustBeforeEach(func() {
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.PodIdentityAgentAddon})
						})

						When("Use default pod identity associations is set", func() {
							It("accepts the setting", func() {
								cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.VPCCNIAddon})
								cfg.AddonsConfig.AutoApplyPodIdentityAssociations = true

								err = api.ValidateClusterConfig(cfg)
								Expect(err).ToNot(HaveOccurred())
							})
						})

						When("Use default pod identity association is set on the vpc-cni addon", func() {
							It("accepts the setting", func() {
								cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.VPCCNIAddon, UseDefaultPodIdentityAssociations: true})

								err = api.ValidateClusterConfig(cfg)
								Expect(err).ToNot(HaveOccurred())
							})
						})

						When("The vpc-cni addon has a pod identity association configured", func() {
							It("accepts the setting", func() {
								cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.VPCCNIAddon,
									PodIdentityAssociations: &[]api.PodIdentityAssociation{{
										Namespace:          "test-namespace",
										ServiceAccountName: "fakeserviceaccount",
										RoleARN:            "fakerolearn",
									}}})

								err = api.ValidateClusterConfig(cfg)
								Expect(err).ToNot(HaveOccurred())
							})
						})

						When("The vpc-cni addon is missing a pod identity configuration", func() {
							It("returns an error", func() {
								cfg.Addons = append(cfg.Addons, &api.Addon{Name: api.VPCCNIAddon})
								err = api.ValidateClusterConfig(cfg)
								Expect(err).To(MatchError(ContainSubstring("Set one of: addonsConfig.autoApplyPodIdentityAssociations, useDefaultPodIdentityAssociations on the vpc-cni addon, apply a custom pod identity on the vpc-cni addon")))
							})
						})
					})
				})

				When("ipFamily is set to IPv6, no managed addons are provided, but auto-mode is used", func() {
					It("accepts the setting", func() {
						cfg.VPC.NAT = nil
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.AutoModeConfig = &api.AutoModeConfig{
							Enabled: aws.Bool(true),
						}
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(BeNil())
					})
				})

				When("the vpc-cni version is configured", func() {
					When("the version of the vpc-cni is too low", func() {
						It("returns an error", func() {
							cfg.Metadata.Version = api.Version1_22
							cfg.IAM = &api.ClusterIAM{
								WithOIDC: api.Enabled(),
							}
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.KubeProxyAddon},
								&api.Addon{Name: api.CoreDNSAddon},
								&api.Addon{Name: api.VPCCNIAddon, Version: "1.9.0"},
							)
							cfg.VPC.NAT = nil
							err = api.ValidateClusterConfig(cfg)
							Expect(err).To(MatchError(ContainSubstring("vpc-cni version must be at least version 1.10.0 for IPv6")))
						})
					})

					When("the version of the vpc-cni is supported", func() {
						It("does not error", func() {
							cfg.Metadata.Version = api.Version1_22
							cfg.IAM = &api.ClusterIAM{
								WithOIDC: api.Enabled(),
							}
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.KubeProxyAddon},
								&api.Addon{Name: api.CoreDNSAddon},
								&api.Addon{Name: api.VPCCNIAddon, Version: "1.10"},
							)
							cfg.VPC.NAT = nil
							err = cfg.ValidateVPCConfig()
							Expect(err).NotTo(HaveOccurred())
						})
					})

					When("the version of the vpc-cni is not configured", func() {
						It("does not error", func() {
							cfg.Metadata.Version = api.Version1_22
							cfg.IAM = &api.ClusterIAM{
								WithOIDC: api.Enabled(),
							}
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.KubeProxyAddon},
								&api.Addon{Name: api.CoreDNSAddon},
								&api.Addon{Name: api.VPCCNIAddon},
							)
							cfg.VPC.NAT = nil
							err = api.ValidateClusterConfig(cfg)
							Expect(err).NotTo(HaveOccurred())
						})
					})

					When("the version of the vpc-cni is latest", func() {
						It("does not error", func() {
							cfg.Metadata.Version = api.Version1_22
							cfg.IAM = &api.ClusterIAM{
								WithOIDC: api.Enabled(),
							}
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.KubeProxyAddon},
								&api.Addon{Name: api.CoreDNSAddon},
								&api.Addon{Name: api.VPCCNIAddon, Version: "latest"},
							)
							cfg.VPC.NAT = nil
							err = cfg.ValidateVPCConfig()
							Expect(err).NotTo(HaveOccurred())
						})
					})

					When("the version of the vpc-cni is invalid", func() {
						It("returns an error", func() {
							cfg.Metadata.Version = api.Version1_22
							cfg.IAM = &api.ClusterIAM{
								WithOIDC: api.Enabled(),
							}
							cfg.Addons = append(cfg.Addons,
								&api.Addon{Name: api.KubeProxyAddon},
								&api.Addon{Name: api.CoreDNSAddon},
								&api.Addon{Name: api.VPCCNIAddon, Version: "1.invalid!semver"},
							)
							cfg.VPC.NAT = nil
							err = api.ValidateClusterConfig(cfg)
							Expect(err).To(MatchError(ContainSubstring("failed to parse version")))
						})
					})
				})

				When("iam is not set", func() {
					It("returns an error", func() {
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("oidc needs to be enabled if IPv6 is set")))
					})
				})

				When("iam is not set, but auto-mode is used", func() {
					It("accepts the setting", func() {
						cfg.VPC.NAT = nil
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.AutoModeConfig = &api.AutoModeConfig{
							Enabled: aws.Bool(true),
						}
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(BeNil())
					})
				})

				When("iam is set but OIDC is disabled", func() {
					It("returns an error", func() {
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Disabled(),
						}
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("oidc needs to be enabled if IPv6 is set")))
					})
				})

				When("ipFamily is set to IPv6 and vpc.NAT is defined", func() {
					It("returns an error", func() {
						cfg.Metadata.Version = api.Version1_22
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.VPC.NAT = &api.ClusterNAT{}
						err = cfg.ValidateVPCConfig()
						Expect(err).To(MatchError(ContainSubstring("setting NAT is not supported with IPv6")))
					})
				})

				When("ipFamily is set to IPv6 and serviceIPv4CIDR is not empty", func() {
					It("returns an error", func() {
						cfg.Metadata.Version = api.Version1_22
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.KubernetesNetworkConfig.ServiceIPv4CIDR = "192.168.0.0/24"
						cfg.VPC.NAT = nil
						err = api.ValidateClusterConfig(cfg)
						Expect(err).To(MatchError(ContainSubstring("service IPv4 CIDR is not supported with IPv6")))
					})
				})

				When("ipFamily is set to IPv6 and AutoAllocateIPv6 is set", func() {
					It("returns an error", func() {
						cfg.VPC.AutoAllocateIPv6 = api.Enabled()
						cfg.Metadata.Version = api.Version1_22
						cfg.IAM = &api.ClusterIAM{
							WithOIDC: api.Enabled(),
						}
						cfg.Addons = append(cfg.Addons,
							&api.Addon{Name: api.KubeProxyAddon},
							&api.Addon{Name: api.CoreDNSAddon},
							&api.Addon{Name: api.VPCCNIAddon},
						)
						cfg.VPC.NAT = nil
						err = cfg.ValidateVPCConfig()
						Expect(err).To(MatchError(ContainSubstring("auto allocate ipv6 is not supported with IPv6")))
					})
				})
			})
		})

		Context("extraCIDRs", func() {
			It("validates cidrs", func() {
				cfg.VPC.ExtraCIDRs = []string{"192.168.0.0/24"}
				cfg.VPC.PublicAccessCIDRs = []string{"3.48.58.68/24"}
				err = cfg.ValidateVPCConfig()
				Expect(err).ToNot(HaveOccurred())
			})

			When("extraCIDRs has an invalid cidr", func() {
				It("returns an error", func() {
					cfg.VPC.ExtraCIDRs = []string{"not-a-cidr"}
					err = cfg.ValidateVPCConfig()
					Expect(err).To(HaveOccurred())
				})
			})

			When("public access cidrs has an invalid cidr", func() {
				It("returns an error", func() {
					cfg.VPC.PublicAccessCIDRs = []string{"48.58.68/24"}
					err = cfg.ValidateVPCConfig()
					Expect(err).To(HaveOccurred())
				})
			})
		})

		Context("ipv6 CIDRs", func() {
			When("IPv6Cidr or IPv6CidrPool is provided and ipv6 is not set", func() {
				It("returns an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV4Family
					cfg.VPC.IPv6Cidr = "foo"
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("Ipv6Cidr and Ipv6CidrPool are only supported when IPFamily is set to IPv6"))

					cfg.KubernetesNetworkConfig.IPFamily = api.IPV4Family
					cfg.VPC.IPv6Cidr = ""
					cfg.VPC.IPv6Pool = "bar"
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("Ipv6Cidr and Ipv6CidrPool are only supported when IPFamily is set to IPv6"))
				})
			})

			When("only one of IPv6Cidr or IPv6CidrPool is provided and ipv6 is set", func() {
				It("returns an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
					cfg.VPC.IPv6Cidr = "foo"
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("Ipv6Cidr and Ipv6Pool must both be configured to use a custom IPv6 CIDR and address pool"))

					cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
					cfg.VPC.IPv6Cidr = ""
					cfg.VPC.IPv6Pool = "bar"
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("Ipv6Cidr and Ipv6Pool must both be configured to use a custom IPv6 CIDR and address pool"))
				})
			})

			When("it's set alongside VPC.ID", func() {
				It("returns an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
					cfg.VPC.IPv6Cidr = "foo"
					cfg.VPC.IPv6Pool = "bar"
					cfg.VPC.ID = "123"
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("cannot provide VPC.IPv6Cidr when using a pre-existing VPC.ID"))
				})
			})
		})

		Context("extraIPv6CIDRs", func() {
			It("validates cidrs", func() {
				cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
				cfg.Metadata.Version = api.Version1_31
				cfg.Addons = append(cfg.Addons,
					&api.Addon{Name: api.KubeProxyAddon},
					&api.Addon{Name: api.CoreDNSAddon},
					&api.Addon{Name: api.VPCCNIAddon},
				)
				cfg.IAM = &api.ClusterIAM{
					WithOIDC: api.Enabled(),
				}
				cfg.VPC.ExtraIPv6CIDRs = []string{"2002::1234:abcd:ffff:c0a8:101/64"}
				cfg.VPC.NAT = nil
				err = cfg.ValidateVPCConfig()
				Expect(err).ToNot(HaveOccurred())
			})

			When("extraIPv6CIDRs has an invalid cidr", func() {
				It("returns an error", func() {
					cfg.VPC.ExtraIPv6CIDRs = []string{"not-a-cidr"}
					cfg.Metadata.Version = api.Version1_31
					cfg.Addons = append(cfg.Addons,
						&api.Addon{Name: api.KubeProxyAddon},
						&api.Addon{Name: api.CoreDNSAddon},
						&api.Addon{Name: api.VPCCNIAddon},
					)
					cfg.IAM = &api.ClusterIAM{
						WithOIDC: api.Enabled(),
					}
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV6Family
					err = cfg.ValidateVPCConfig()
					Expect(err).To(HaveOccurred())

					cfg.VPC.ExtraIPv6CIDRs = []string{"2002::1234:abcd:ffff:c0a8:101/644"}
					err = cfg.ValidateVPCConfig()
					Expect(err).To(HaveOccurred())
				})
			})

			When("when ipv4 is configured", func() {
				It("returns an error", func() {
					cfg.KubernetesNetworkConfig.IPFamily = api.IPV4Family
					cfg.Metadata.Version = api.Version1_31
					cfg.Addons = append(cfg.Addons,
						&api.Addon{Name: api.KubeProxyAddon},
						&api.Addon{Name: api.CoreDNSAddon},
						&api.Addon{Name: api.VPCCNIAddon},
					)
					cfg.IAM = &api.ClusterIAM{
						WithOIDC: api.Enabled(),
					}
					cfg.VPC.ExtraIPv6CIDRs = []string{"2002::1234:abcd:ffff:c0a8:101/644"}
					err = cfg.ValidateVPCConfig()
					Expect(err).To(MatchError("cannot specify vpc.extraIPv6CIDRs with an IPv4 cluster"))
				})
			})

		})

		type vpcSecurityGroupEntry struct {
			updateVPC   func(*api.ClusterVPC)
			expectedErr string
		}
		DescribeTable("vpc.securityGroup and vpc.controlPlaneSecurityGroupIDs", func(e vpcSecurityGroupEntry) {
			e.updateVPC(cfg.VPC)
			err := cfg.ValidateVPCConfig()
			if e.expectedErr != "" {
				Expect(err).To(MatchError(ContainSubstring(e.expectedErr)))
			} else {
				Expect(err).NotTo(HaveOccurred())
			}
		},
			Entry("both set", vpcSecurityGroupEntry{
				updateVPC: func(v *api.ClusterVPC) {
					v.SecurityGroup = "sg-1234"
					v.ControlPlaneSecurityGroupIDs = []string{"sg-1234"}
				},
				expectedErr: "only one of vpc.securityGroup and vpc.controlPlaneSecurityGroupIDs can be specified",
			}),
			Entry("vpc.securityGroup set", vpcSecurityGroupEntry{
				updateVPC: func(v *api.ClusterVPC) {
					v.SecurityGroup = "sg-1234"
				},
			}),
			Entry("vpc.controlPlaneSecurityGroupIDs set", vpcSecurityGroupEntry{
				updateVPC: func(v *api.ClusterVPC) {
					v.ControlPlaneSecurityGroupIDs = []string{"sg-1234"}
				},
			}),
			Entry("neither set", vpcSecurityGroupEntry{
				updateVPC: func(v *api.ClusterVPC) {},
			}),
		)
	})

	Describe("ValidatePrivateCluster", func() {
		var (
			cfg *api.ClusterConfig
			vpc *api.ClusterVPC
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			vpc = api.NewClusterVPC(false)
			cfg.VPC = vpc
			cfg.PrivateCluster = &api.PrivateCluster{
				Enabled: true,
			}
		})
		When("private cluster is enabled", func() {
			It("validates the config", func() {
				err := cfg.ValidatePrivateCluster()
				Expect(err).NotTo(HaveOccurred())
			})
		})
		When("vpc is provided but no private subnets", func() {
			It("fails the validation", func() {
				cfg.VPC.Subnets = &api.ClusterSubnets{}
				cfg.VPC.ID = "id"
				err := cfg.ValidatePrivateCluster()
				Expect(err).To(MatchError(ContainSubstring("vpc.subnets.private must be specified in a fully-private cluster when a pre-existing VPC is supplied")))
			})
		})
		When("additional endpoints are defined with skip endpoints", func() {
			It("fails the validation", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{api.EndpointServiceCloudWatch.Name}
				cfg.PrivateCluster.SkipEndpointCreation = true
				err := cfg.ValidatePrivateCluster()
				Expect(err).To(MatchError(ContainSubstring("privateCluster.additionalEndpointServices cannot be set when privateCluster.skipEndpointCreation is true")))
			})
		})
		When("additional endpoints are defined", func() {
			It("validates the endpoint configuration", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{api.EndpointServiceCloudWatch.Name}
				err := cfg.ValidatePrivateCluster()
				Expect(err).NotTo(HaveOccurred())
			})
		})
		When("additional endpoints are defined incorrectly", func() {
			It("fails the endpoint validation", func() {
				cfg.PrivateCluster.AdditionalEndpointServices = []string{"unknown"}
				err := cfg.ValidatePrivateCluster()
				Expect(err).To(MatchError(ContainSubstring("invalid value in privateCluster.additionalEndpointServices")))
			})
		})
		When("private cluster is enabled with skip endpoints", func() {
			It("does not fail the validation", func() {
				cfg.PrivateCluster.SkipEndpointCreation = true
				err := cfg.ValidatePrivateCluster()
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	type cpuCreditsEntry struct {
		modifyNodeGroup func(*api.NodeGroup)
		expectedError   string
	}

	DescribeTable("cpuCredits", func(e cpuCreditsEntry) {
		ng := &api.NodeGroup{
			NodeGroupBase: &api.NodeGroupBase{},
			InstancesDistribution: &api.NodeGroupInstancesDistribution{
				InstanceTypes: []string{"t3.medium", "t3.large"},
			},
			CPUCredits: aws.String("unlimited"),
		}
		if e.modifyNodeGroup != nil {
			e.modifyNodeGroup(ng)
		}

		err := api.ValidateNodeGroup(0, ng, api.NewClusterConfig())
		if e.expectedError != "" {
			Expect(err).To(MatchError(ContainSubstring(e.expectedError)))
		} else {
			Expect(err).NotTo(HaveOccurred())
		}
	},

		Entry("instanceType is set", cpuCreditsEntry{
			modifyNodeGroup: func(ng *api.NodeGroup) {
				ng.InstanceType = "mixed"
			},
		}),
		Entry("instanceType is not set", cpuCreditsEntry{}),
		Entry("instancesDistribution is not set", cpuCreditsEntry{
			modifyNodeGroup: func(ng *api.NodeGroup) {
				ng.InstancesDistribution = nil
			},
			expectedError: "cpuCredits option set for nodegroup, but it has no t2/t3 instance types",
		}),
		Entry("instancesDistribution.instanceTypes is not set", cpuCreditsEntry{
			modifyNodeGroup: func(ng *api.NodeGroup) {
				ng.InstancesDistribution.InstanceTypes = nil
			},
			expectedError: "at least two instance types have to be specified for mixed nodegroups",
		}),
	)

	Describe("ssh flags", func() {
		var (
			testKeyPath = "some/path/to/file.pub"
			testKeyName = "id_rsa.pub"
			testKey     = "THIS IS A KEY"
			ng          *api.NodeGroup
			cfg         *api.ClusterConfig
		)

		BeforeEach(func() {
			ng = newNodeGroup()
			cfg = api.NewClusterConfig()
		})

		It("fails when a key path and a key name are specified", func() {
			SSHConfig := &api.NodeGroupSSH{
				Allow:         api.Enabled(),
				PublicKeyPath: &testKeyPath,
				PublicKeyName: &testKeyName,
			}

			ng.SSH = SSHConfig
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError("only one of publicKeyName, publicKeyPath or publicKey can be specified for SSH per node-group"))
		})

		It("fails when a key path and a key are specified", func() {
			SSHConfig := &api.NodeGroupSSH{
				Allow:         api.Enabled(),
				PublicKeyPath: &testKeyPath,
				PublicKey:     &testKey,
			}

			ng.SSH = SSHConfig
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError("only one of publicKeyName, publicKeyPath or publicKey can be specified for SSH per node-group"))
		})

		It("fails when a key name and a key are specified", func() {
			SSHConfig := &api.NodeGroupSSH{
				Allow:         api.Enabled(),
				PublicKeyName: &testKeyName,
				PublicKey:     &testKey,
			}

			ng.SSH = SSHConfig
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError("only one of publicKeyName, publicKeyPath or publicKey can be specified for SSH per node-group"))
		})

		Context("Instances distribution", func() {
			var ng *api.NodeGroup
			BeforeEach(func() {
				ng = &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{},
					InstancesDistribution: &api.NodeGroupInstancesDistribution{
						InstanceTypes:                       []string{"t3.medium", "t3.large"},
						OnDemandBaseCapacity:                newInt(1),
						SpotInstancePools:                   newInt(2),
						OnDemandPercentageAboveBaseCapacity: newInt(50),
					},
				}
			})

			It("doesn't panic when instance distribution is not enabled", func() {
				ng.InstancesDistribution = nil
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			})

			It("doesn't fail when instance distribution is enabled and instanceType is \"mixed\"", func() {
				ng.InstanceType = "mixed"
				ng.InstancesDistribution.InstanceTypes = []string{"t3.medium"}

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			})

			It("fails in case of arm-gpu distribution instance type", func() {
				ng.InstanceType = "mixed"
				ng.InstancesDistribution.InstanceTypes = []string{"g5g.2xlarge"}
				ng.AMIFamily = api.NodeImageFamilyAmazonLinux2
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("g5g.2xlarge instance types are not supported for unmanaged nodegroups with AMIFamily AmazonLinux2"))
			})

			It("ARM-based GPU instance type fails for AmazonLinux2", func() {
				ng.InstanceType = "g5g.2xlarge"
				ng.InstancesDistribution.InstanceTypes = nil
				ng.AMIFamily = api.NodeImageFamilyAmazonLinux2
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("g5g.2xlarge instance types are not supported for unmanaged nodegroups with AMIFamily AmazonLinux2"))
			})

			It("fails when instance distribution is enabled and instanceType set", func() {
				ng.InstanceType = "t3.small"

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(HaveOccurred())
				Expect(err).To(MatchError("instanceType should be \"mixed\" or unset when using the instances distribution feature"))
			})

			It("fails when the instance distribution doesn't have any instance type", func() {
				ng.InstanceType = "mixed"
				ng.InstancesDistribution.InstanceTypes = []string{}

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("at least two instance types have to be specified for mixed nodegroups"))
			})

			It("fails when the onDemandBaseCapacity is not above 0", func() {
				ng.InstancesDistribution.OnDemandBaseCapacity = newInt(-1)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("onDemandBaseCapacity should be 0 or more"))
			})

			It("fails when the spotInstancePools is not between 1 and 20", func() {
				ng.InstancesDistribution.SpotInstancePools = newInt(0)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("spotInstancePools should be between 1 and 20"))

				ng.InstancesDistribution.SpotInstancePools = newInt(21)
				err = api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(HaveOccurred())
				Expect(err).To(MatchError("spotInstancePools should be between 1 and 20"))
			})

			It("fails when the onDemandPercentageAboveBaseCapacity is not between 0 and 100", func() {
				ng.InstancesDistribution.OnDemandPercentageAboveBaseCapacity = newInt(-1)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("percentageAboveBase should be between 0 and 100"))

				ng.InstancesDistribution.OnDemandPercentageAboveBaseCapacity = newInt(101)
				err = api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("percentageAboveBase should be between 0 and 100"))
			})

			It("fails when the spotAllocationStrategy is not a supported strategy", func() {
				ng.InstancesDistribution.SpotAllocationStrategy = aws.String("unsupported-strategy")

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("spotAllocationStrategy should be one of: [lowest-price diversified capacity-optimized capacity-optimized-prioritized price-capacity-optimized]"))
			})

			It("fails when the spotAllocationStrategy is capacity-optimized and spotInstancePools is specified", func() {
				ng.InstancesDistribution.SpotAllocationStrategy = aws.String("capacity-optimized")
				ng.InstancesDistribution.SpotInstancePools = newInt(2)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("spotInstancePools cannot be specified when also specifying spotAllocationStrategy: capacity-optimized"))
			})

			It("fails when the spotAllocationStrategy is capacity-optimized-prioritized and spotInstancePools is specified", func() {
				ng.InstancesDistribution.SpotAllocationStrategy = aws.String("capacity-optimized-prioritized")
				ng.InstancesDistribution.SpotInstancePools = newInt(2)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(MatchError("spotInstancePools cannot be specified when also specifying spotAllocationStrategy: capacity-optimized-prioritized"))
			})

			It("does not fail when the spotAllocationStrategy is lowest-price and spotInstancePools is specified", func() {
				ng.InstancesDistribution.SpotAllocationStrategy = aws.String("lowest-price")
				ng.InstancesDistribution.SpotInstancePools = newInt(2)

				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	Describe("kubelet extra config", func() {
		Context("Instances distribution", func() {
			var (
				ng  *api.NodeGroup
				cfg *api.ClusterConfig
			)

			BeforeEach(func() {
				ng = newNodeGroup()
				cfg = api.NewClusterConfig()
			})

			It("Forbids overriding basic fields", func() {
				testKeys := []string{"kind", "apiVersion", "address", "clusterDomain", "authentication",
					"authorization", "serverTLSBootstrap"}

				for _, key := range testKeys {
					ng.KubeletExtraConfig = &api.InlineDocument{
						key: "should-not-be-allowed",
					}
					err := api.ValidateNodeGroup(0, ng, cfg)
					Expect(err).To(MatchError(fmt.Sprintf("cannot override \"%s\" in kubelet config, as it's critical to eksctl functionality", key)))
				}
			})

			It("Allows other kubelet options", func() {
				ng.KubeletExtraConfig = &api.InlineDocument{
					"kubeReserved": map[string]string{
						"cpu":               "300m",
						"memory":            "300Mi",
						"ephemeral-storage": "1Gi",
					},
					"kubeReservedCgroup": "/kube-reserved",
					"cgroupDriver":       "systemd",
					"featureGates": map[string]bool{
						"VolumeScheduling":         false,
						"VolumeSnapshotDataSource": true,
					},
				}
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	Describe("ebs encryption", func() {
		var (
			nodegroup = "ng1"
			volSize   = 50
			kmsKeyID  = "36c0b54e-64ed-4f2d-a1c7-96558764311e"
			disabled  = false
			enabled   = true
		)

		Context("Encrypted workers", func() {

			var (
				ng  *api.NodeGroup
				cfg *api.ClusterConfig
			)

			BeforeEach(func() {
				ng = newNodeGroup()
				cfg = api.NewClusterConfig()
			})

			It("Forbids setting volumeKmsKeyID without volumeEncrypted", func() {
				ng.Name = nodegroup
				ng.VolumeSize = &volSize
				ng.VolumeEncrypted = nil
				ng.VolumeKmsKeyID = &kmsKeyID
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(HaveOccurred())
			})

			It("Forbids setting volumeKmsKeyID with volumeEncrypted: false", func() {
				ng.Name = nodegroup
				ng.VolumeSize = &volSize
				ng.VolumeEncrypted = &disabled
				ng.VolumeKmsKeyID = &kmsKeyID
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(HaveOccurred())
			})

			It("Allows setting volumeKmsKeyID with volumeEncrypted: true", func() {
				ng.Name = nodegroup
				ng.VolumeSize = &volSize
				ng.VolumeEncrypted = &enabled
				ng.VolumeKmsKeyID = &kmsKeyID
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).NotTo(HaveOccurred())
			})

		})
	})

	Describe("FargateProfile", func() {
		Describe("Validate", func() {
			It("returns an error when the profile's name is empty", func() {
				profile := api.FargateProfile{
					Selectors: []api.FargateProfileSelector{
						{Namespace: "default"},
					},
				}
				err := profile.Validate()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("invalid Fargate profile: empty name"))
			})

			It("returns an error when the profile has a nil selectors array", func() {
				profile := api.FargateProfile{
					Name: "default",
				}
				err := profile.Validate()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("invalid Fargate profile \"default\": no profile selector"))
			})

			It("returns an error when the profile has an empty selectors array", func() {
				profile := api.FargateProfile{
					Name:      "default",
					Selectors: []api.FargateProfileSelector{},
				}
				err := profile.Validate()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("invalid Fargate profile \"default\": no profile selector"))
			})

			It("returns an error when the profile's selectors do not have any namespace defined", func() {
				profile := api.FargateProfile{
					Name: "default",
					Selectors: []api.FargateProfileSelector{
						{},
					},
				}
				err := profile.Validate()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("invalid Fargate profile \"default\": invalid profile selector at index #0: empty namespace"))
			})

			It("returns an error when the profile's name starts with eks-", func() {
				profile := api.FargateProfile{
					Name: "eks-foo",
					Selectors: []api.FargateProfileSelector{
						{Namespace: "default"},
					},
				}
				err := profile.Validate()
				Expect(err).To(HaveOccurred())
				Expect(err.Error()).To(Equal("invalid Fargate profile \"eks-foo\": name should NOT start with \"eks-\""))
			})

			It("passes when a name and at least one selector with a namespace is defined", func() {
				profile := api.FargateProfile{
					Name: "default",
					Selectors: []api.FargateProfileSelector{
						{Namespace: "default"},
					},
				}
				err := profile.Validate()
				Expect(err).NotTo(HaveOccurred())
			})

			It("passes when a name and multiple selectors with a namespace is defined", func() {
				profile := api.FargateProfile{
					Name: "default",
					Selectors: []api.FargateProfileSelector{
						{
							Namespace: "default",
						},
						{
							Namespace: "dev",
						},
					},
				}
				err := profile.Validate()
				Expect(err).NotTo(HaveOccurred())
			})
		})
	})

	Describe("Bottlerocket node groups", func() {
		It("returns an error if bottlerocket settings are used with incorrect amiFamily", func() {
			ng := &api.NodeGroup{
				NodeGroupBase: &api.NodeGroupBase{
					Bottlerocket: &api.NodeGroupBottlerocket{},
				},
			}
			err := api.ValidateNodeGroup(0, ng, api.NewClusterConfig())
			Expect(err).To(MatchError(ContainSubstring(`bottlerocket config can only be used with amiFamily "Bottlerocket"`)))
		})

		type bottlerocketEntry struct {
			ng          *api.NodeGroup
			expectedErr string
		}

		DescribeTable("field validation", func(be bottlerocketEntry) {
			if be.ng.NodeGroupBase == nil {
				be.ng.NodeGroupBase = &api.NodeGroupBase{}
			}
			be.ng.AMIFamily = api.NodeImageFamilyBottlerocket
			err := api.ValidateNodeGroup(0, be.ng, api.NewClusterConfig())
			if be.expectedErr != "" {
				Expect(err).To(MatchError(be.expectedErr))
			} else {
				Expect(err).NotTo(HaveOccurred())
			}
		},
			Entry("preBootstrapCommands", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{
						PreBootstrapCommands: []string{"/usr/bin/env true"},
					},
				},

				expectedErr: "preBootstrapCommands is not supported for Bottlerocket nodegroups (path=nodeGroups[0].preBootstrapCommands)",
			}),

			Entry("overrideBootstrapCommand", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{
						OverrideBootstrapCommand: aws.String("/usr/bin/some-command"),
					},
				},

				expectedErr: "overrideBootstrapCommand is not supported for Bottlerocket nodegroups (path=nodeGroups[0].overrideBootstrapCommand)",
			}),

			Entry("kubeletExtraConfig", bottlerocketEntry{
				ng: &api.NodeGroup{
					KubeletExtraConfig: &api.InlineDocument{
						"cgroupDriver": "systemd",
					},
				},

				expectedErr: "kubeletExtraConfig is not supported for Bottlerocket nodegroups (path=nodeGroups[0].kubeletExtraConfig)",
			}),

			Entry("overlapping settings", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{
						Bottlerocket: &api.NodeGroupBottlerocket{
							Settings: &api.InlineDocument{
								"kubernetes": map[string]interface{}{
									"node-labels": map[string]string{
										"mylabel.example.com": "value",
									},
								},
							},
						},
					},
				},

				expectedErr: "invalid Bottlerocket setting: use nodeGroups[0].labels instead (path=nodeGroups[0].bottlerocket.settings.kubernetes.node-labels)",
			}),

			Entry("both clusterDNS and cluster-dns-ip set", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{
						Bottlerocket: &api.NodeGroupBottlerocket{
							Settings: &api.InlineDocument{
								"kubernetes": map[string]interface{}{
									"cluster-dns-ip": "10.100.0.10",
								},
							},
						},
					},
					ClusterDNS: "10.100.0.10",
				},

				expectedErr: "only one of nodeGroups[0].bottlerocket.settings.kubernetes.cluster-dns-ip or nodeGroups[0].clusterDNS can be set",
			}),

			Entry("labels", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{Labels: map[string]string{"label": "label-value"}},
				},
			}),

			Entry("maxPods", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{MaxPodsPerNode: 32},
				},
			}),

			Entry("maxPods", bottlerocketEntry{
				ng: &api.NodeGroup{
					NodeGroupBase: &api.NodeGroupBase{
						ScalingConfig: &api.ScalingConfig{
							MinSize: aws.Int(5),
						},
					},
				},
			}),
		)
	})

	type kmsFieldCase struct {
		secretsEncryption *api.SecretsEncryption
		errSubstr         string
	}

	DescribeTable("KMS field validation", func(k kmsFieldCase) {
		clusterConfig := api.NewClusterConfig()
		clusterConfig.Metadata.Version = "1.15"

		clusterConfig.SecretsEncryption = k.secretsEncryption
		err := api.ValidateClusterConfig(clusterConfig)
		if k.errSubstr != "" {
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring(k.errSubstr))
		} else {
			Expect(err).NotTo(HaveOccurred())
		}
	},
		Entry("Nil secretsEncryption", kmsFieldCase{
			secretsEncryption: nil,
		}),
		Entry("Empty secretsEncryption.keyARN", kmsFieldCase{
			secretsEncryption: &api.SecretsEncryption{},
			errSubstr:         "secretsEncryption.keyARN is required",
		}),
	)

	Describe("Supported AMI Families", func() {
		var (
			ng  *api.NodeGroup
			cfg *api.ClusterConfig
		)

		BeforeEach(func() {
			ng = api.NewNodeGroup()
			cfg = api.NewClusterConfig()
		})

		It("succeeds when AMI family is in supported list", func() {
			ng.AMIFamily = "AmazonLinux2"
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).NotTo(HaveOccurred())
		})

		It("mutates the AMIFamily to the correct value when the capitalisation is incorrect", func() {
			ng.AMIFamily = "aMAZONlINUx2"
			Expect(api.ValidateNodeGroup(0, ng, cfg)).To(Succeed())
			Expect(ng.AMIFamily).To(Equal("AmazonLinux2"))

			mng := api.NewManagedNodeGroup()
			mng.AMIFamily = "bOTTLEROCKEt"
			Expect(api.ValidateManagedNodeGroup(0, mng)).To(Succeed())
			Expect(mng.AMIFamily).To(Equal("Bottlerocket"))
		})

		It("fails when the AMIFamily is not supported", func() {
			ng.AMIFamily = "SomeTrash"
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError(fmt.Sprintf("AMI Family SomeTrash is not supported - use one of: %s", strings.Join(api.SupportedAMIFamilies(), ", "))))
		})

		It("fails when the AmiFamily is not supported for managed nodes with custom AMI", func() {
			mng := api.NewManagedNodeGroup()
			mng.AMI = "ami-1234"
			mng.OverrideBootstrapCommand = aws.String("bootstrap command")

			mng.AMIFamily = api.NodeImageFamilyAmazonLinux2
			err := api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntu2004
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntuPro2004
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntu2204
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntuPro2204
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntu2404
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyUbuntuPro2404
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyBottlerocket
			mng.OverrideBootstrapCommand = nil
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())

			mng.AMIFamily = api.NodeImageFamilyAmazonLinux2023
			err = api.ValidateManagedNodeGroup(0, mng)
			Expect(err).NotTo(HaveOccurred())
		})

		It("fails when the AMIFamily is WindowsServer2004CoreContainer", func() {
			ng.AMIFamily = api.NodeImageFamilyWindowsServer2004CoreContainer
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError("AMI Family WindowsServer2004CoreContainer is deprecated. For more information, head to the Amazon documentation on Windows AMIs (https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-windows-ami.html)"))
		})

		It("fails when the AMIFamily is WindowsServer20H2CoreContainer", func() {
			ng.AMIFamily = api.NodeImageFamilyWindowsServer20H2CoreContainer
			err := api.ValidateNodeGroup(0, ng, cfg)
			Expect(err).To(MatchError("AMI Family WindowsServer20H2CoreContainer is deprecated. For more information, head to the Amazon documentation on Windows AMIs (https://docs.aws.amazon.com/eks/latest/userguide/eks-optimized-windows-ami.html)"))
		})
	})

	Describe("Windows node groups", func() {
		It("returns an error with unsupported fields", func() {
			doc := api.InlineDocument{
				"cgroupDriver": "systemd",
			}

			ngs := map[string]*api.NodeGroup{
				"KubeletExtraConfig": {KubeletExtraConfig: &doc, NodeGroupBase: &api.NodeGroupBase{}},
			}
			cfg := api.NewClusterConfig()

			for name, ng := range ngs {
				ng.AMIFamily = api.NodeImageFamilyWindowsServer2019CoreContainer
				err := api.ValidateNodeGroup(0, ng, cfg)
				Expect(err).To(HaveOccurred(), "Expected an error when provided %s", name)
			}
		})

		It("has no error with supported fields", func() {
			x := 32
			ngs := []*api.NodeGroup{
				{NodeGroupBase: &api.NodeGroupBase{Labels: map[string]string{"label": "label-value"}}},
				{NodeGroupBase: &api.NodeGroupBase{MaxPodsPerNode: x}},
				{NodeGroupBase: &api.NodeGroupBase{ScalingConfig: &api.ScalingConfig{MinSize: &x}}},
				{NodeGroupBase: &api.NodeGroupBase{PreBootstrapCommands: []string{"start /wait msiexec.exe"}}},
			}
			cfg := api.NewClusterConfig()

			for i, ng := range ngs {
				ng.AMIFamily = api.NodeImageFamilyWindowsServer2019CoreContainer
				Expect(api.ValidateNodeGroup(i, ng, cfg)).To(Succeed())
			}
		})
	})

	Describe("Karpenter", func() {
		It("returns an error when OIDC is not set", func() {
			cfg := api.NewClusterConfig()
			cfg.Karpenter = &api.Karpenter{
				Version: "0.20.0",
			}
			Expect(api.ValidateClusterConfig(cfg)).To(MatchError(ContainSubstring("failed to validate Karpenter config: iam.withOIDC must be enabled with Karpenter")))
		})

		It("returns an error when version is missing", func() {
			cfg := api.NewClusterConfig()
			cfg.Karpenter = &api.Karpenter{}
			Expect(api.ValidateClusterConfig(cfg)).To(MatchError(ContainSubstring("version field is required if installing Karpenter is enabled")))
		})

		It("returns an error when version is missing", func() {
			cfg := api.NewClusterConfig()
			cfg.IAM.WithOIDC = aws.Bool(true)
			cfg.Karpenter = &api.Karpenter{
				Version: "isitmeeeyourlookingfoorrrr",
			}
			Expect(api.ValidateClusterConfig(cfg)).To(MatchError(ContainSubstring("failed to parse Karpenter version")))
		})

		It("returns an error when the version is not supported", func() {
			cfg := api.NewClusterConfig()
			cfg.IAM.WithOIDC = aws.Bool(true)
			cfg.Karpenter = &api.Karpenter{
				Version: "v0.17.0",
			}
			Expect(api.ValidateClusterConfig(cfg)).To(MatchError(ContainSubstring("failed to validate Karpenter config: minimum supported version is v0.20.0")))
		})
	})

	type labelsTaintsEntry struct {
		labels map[string]string
		taints []api.NodeGroupTaint
		valid  bool
	}

	DescribeTable("Nodegroup label and taints validation", func(e labelsTaintsEntry) {
		ng := newNodeGroup()
		ng.Labels = e.labels
		ng.Taints = e.taints

		mng := api.NewManagedNodeGroup()
		mng.Labels = e.labels
		mng.Taints = e.taints

		err := api.ValidateNodeGroup(0, ng, api.NewClusterConfig())
		if e.valid {
			Expect(err).NotTo(HaveOccurred())
		} else {
			Expect(err).To(HaveOccurred())
		}

		err = api.ValidateManagedNodeGroup(0, mng)
		if e.valid {
			Expect(err).NotTo(HaveOccurred())
		} else {
			Expect(err).To(HaveOccurred())
		}
	},
		Entry("disallowed label", labelsTaintsEntry{
			labels: map[string]string{
				"node-role.kubernetes.io/os": "linux",
			},
		}),

		Entry("disallowed label 2", labelsTaintsEntry{
			labels: map[string]string{
				"alpha.service-controller.kubernetes.io/test": "value",
			},
		}),

		Entry("empty labels and taints", labelsTaintsEntry{
			labels: map[string]string{},
			taints: []api.NodeGroupTaint{},
			valid:  true,
		}),

		Entry("allowed labels", labelsTaintsEntry{
			labels: map[string]string{
				"kubernetes.io/hostname":           "supercomputer",
				"beta.kubernetes.io/os":            "linux",
				"kubelet.kubernetes.io/palindrome": "telebuk",
			},
			valid: true,
		}),

		Entry("valid taints", labelsTaintsEntry{
			taints: []api.NodeGroupTaint{
				{
					Key:    "key1",
					Value:  "value1",
					Effect: "NoSchedule",
				},
				{
					Key:    "key2",
					Effect: "NoSchedule",
				},
				{
					Key:    "key3",
					Effect: "PreferNoSchedule",
				},
			},
			valid: true,
		}),

		Entry("missing taint effect", labelsTaintsEntry{
			taints: []api.NodeGroupTaint{
				{
					Key:   "key1",
					Value: "value1",
				},
			},
		}),

		Entry("unsupported taint effect", labelsTaintsEntry{
			taints: []api.NodeGroupTaint{
				{
					Key:    "key2",
					Value:  "value1",
					Effect: "NoEffect",
				},
			},
		}),

		Entry("invalid value", labelsTaintsEntry{
			taints: []api.NodeGroupTaint{
				{
					Key:    "key3",
					Value:  "v@lue",
					Effect: "NoSchedule",
				},
			},
		}),
	)

	Describe("Availability Zones", func() {
		When("the config file does not specify any AZ", func() {
			It("skips validation", func() {
				Expect(api.ValidateClusterConfig(api.NewClusterConfig())).NotTo(HaveOccurred())
			})
		})

		When("the config file contains too few availability zones", func() {
			It("returns an error", func() {
				cfg := api.NewClusterConfig()
				cfg.AvailabilityZones = append(cfg.AvailabilityZones, "az-1")
				Expect(api.ValidateClusterConfig(cfg)).To(MatchError("only 1 zone(s) specified [az-1], 2 are required (can be non-unique)"))
			})
		})
	})

	Describe("Validate SecretsEncryption", func() {
		var cfg *api.ClusterConfig

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
		})

		When("a key ARN is set", func() {
			When("the key is valid", func() {
				It("does not return an error", func() {
					cfg.SecretsEncryption = &api.SecretsEncryption{
						KeyARN: "arn:aws:kms:us-west-2:000000000000:key/12345-12345",
					}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).NotTo(HaveOccurred())
				})
			})

			When("the key is invalid", func() {
				It("returns an error", func() {
					cfg.SecretsEncryption = &api.SecretsEncryption{
						KeyARN: "invalid:arn",
					}
					err := api.ValidateClusterConfig(cfg)
					Expect(err).To(MatchError(ContainSubstring("invalid ARN")))
				})
			})
		})

		When("a key ARN is not set", func() {
			It("returns an error", func() {
				cfg.SecretsEncryption = &api.SecretsEncryption{}
				err := api.ValidateClusterConfig(cfg)
				Expect(err).To(MatchError(ContainSubstring("field secretsEncryption.keyARN is required for enabling secrets encryption")))
			})
		})
	})

	Describe("Scaling config", func() {
		var (
			mng   *api.ManagedNodeGroup
			zero  int
			one   int
			two   int
			three int
		)

		BeforeEach(func() {
			mng = api.NewManagedNodeGroup()
			mng.AMIFamily = "bOTTLEROCKEt"
			zero = 0
			one = 1
			two = 2
			three = 3
		})

		When("No config is specified", func() {
			It("Should set the defaults", func() {
				Expect(api.ValidateManagedNodeGroup(0, mng)).To(Succeed())
				Expect(*mng.MinSize).To(Equal(api.DefaultNodeCount))
				Expect(*mng.MaxSize).To(Equal(api.DefaultNodeCount))
				Expect(*mng.DesiredCapacity).To(Equal(api.DefaultNodeCount))
			})
		})

		When("DesiredCapacity is zero", func() {
			It("Should set min to zero and max to 1", func() {
				mng.DesiredCapacity = &zero
				Expect(api.ValidateManagedNodeGroup(0, mng)).To(Succeed())
				Expect(*mng.MinSize).To(Equal(0))
				Expect(*mng.MaxSize).To(Equal(1))
			})
		})

		When("DesiredCapacity is greater than zero", func() {
			It("Should set min and max equal to capacity", func() {
				three := 3
				mng.DesiredCapacity = &three
				Expect(api.ValidateManagedNodeGroup(0, mng)).To(Succeed())
				Expect(*mng.MinSize).To(Equal(*mng.DesiredCapacity))
				Expect(*mng.MaxSize).To(Equal(*mng.DesiredCapacity))
			})
		})

		DescribeTable("Invalid config values", func(config api.ScalingConfig, expectedErr string) {
			mng.ScalingConfig = &config
			err := api.ValidateManagedNodeGroup(0, mng)
			Expect(err).To(HaveOccurred())
			Expect(err.Error()).To(ContainSubstring(expectedErr))
		},
			Entry("Desired capacity is lower than min",
				api.ScalingConfig{
					DesiredCapacity: &one,
					MinSize:         &two,
				},
				fmt.Sprintf("cannot use --nodes-min=%d and --nodes=%d at the same time", 2, 1),
			),
			Entry("Desired capacity is greater than max",
				api.ScalingConfig{
					DesiredCapacity: &three,
					MaxSize:         &two,
				},
				fmt.Sprintf("cannot use --nodes-max=%d and --nodes=%d at the same time", 2, 3),
			),
			Entry("Min is greater than max",
				api.ScalingConfig{
					MinSize: &two,
					MaxSize: &one,
				},
				fmt.Sprintf("cannot use --nodes-min=%d and --nodes-max=%d at the same time", 2, 1),
			),
		)
	})

	Describe("Capacity Reservation validation", func() {
		var (
			cfg *api.ClusterConfig
			ng  *api.NodeGroup
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			ng = cfg.NewNodeGroup()
			ng.Name = "ng"
		})

		When("both CapacityReservationPreference and CapacityReservationTarget are set", func() {
			It("returns an error", func() {
				ng.CapacityReservation = &api.CapacityReservation{
					CapacityReservationPreference: aws.String("open"),
					CapacityReservationTarget: &api.CapacityReservationTarget{
						CapacityReservationID:               aws.String("id"),
						CapacityReservationResourceGroupARN: aws.String("arn"),
					},
				}

				Expect(api.ValidateNodeGroup(0, ng, cfg)).To(MatchError(ContainSubstring("only one of CapacityReservationPreference or CapacityReservationTarget may be specified at a time")))
			})
		})

		When("CapacityReservationPreference is set", func() {
			When("it is set to 'open'", func() {
				It("does not fail", func() {
					ng.CapacityReservation = &api.CapacityReservation{CapacityReservationPreference: aws.String("open")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(Succeed())
				})
			})

			When("it is set to 'none'", func() {
				It("does not fail", func() {
					ng.CapacityReservation = &api.CapacityReservation{CapacityReservationPreference: aws.String("none")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(Succeed())
				})
			})

			When("it is set to something other than 'open' or 'none'", func() {
				It("returns an error", func() {
					ng.CapacityReservation = &api.CapacityReservation{CapacityReservationPreference: aws.String("err")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(MatchError(ContainSubstring(`accepted values include "open" and "none"; got "err"`)))
				})
			})
		})

		When("CapacityReservationTarget is set", func() {
			When("when both CapacityReservationID and CapacityReservationResourceGroupARN are set", func() {
				It("returns an error", func() {
					ng.CapacityReservation = &api.CapacityReservation{
						CapacityReservationTarget: &api.CapacityReservationTarget{
							CapacityReservationID:               aws.String("id"),
							CapacityReservationResourceGroupARN: aws.String("arn"),
						},
					}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(MatchError(ContainSubstring("only one of CapacityReservationID or CapacityReservationResourceGroupARN may be specified at a time")))
				})
			})
		})
	})

	Describe("Instance Market Options validation", func() {
		var (
			cfg *api.ClusterConfig
			ng  *api.NodeGroup
		)

		BeforeEach(func() {
			cfg = api.NewClusterConfig()
			ng = cfg.NewNodeGroup()
			ng.Name = "ng"
		})

		When("InstanceMarketOptions is set", func() {
			When("it is set to 'capacity-block'", func() {
				It("does not fail", func() {
					ng.CapacityReservation = &api.CapacityReservation{
						CapacityReservationTarget: &api.CapacityReservationTarget{
							CapacityReservationID: aws.String("id"),
						},
					}
					ng.InstanceMarketOptions = &api.InstanceMarketOptions{MarketType: aws.String("capacity-block")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(Succeed())
				})
			})

			When("it is set to 'capacity-block' but Capacity Reservation not set", func() {
				It("does fail", func() {
					ng.InstanceMarketOptions = &api.InstanceMarketOptions{MarketType: aws.String("capacity-block")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(MatchError(ContainSubstring(`instanceMarketOptions cannot be set without capacityReservation`)))
				})
			})

			When("it is set to 'foobar'", func() {
				It("does fail", func() {
					ng.CapacityReservation = &api.CapacityReservation{
						CapacityReservationTarget: &api.CapacityReservationTarget{
							CapacityReservationID: aws.String("id"),
						},
					}
					ng.InstanceMarketOptions = &api.InstanceMarketOptions{MarketType: aws.String("foobar")}
					Expect(api.ValidateNodeGroup(0, ng, cfg)).To(MatchError(ContainSubstring(`only accepted value is "capacity-block"; got "foobar"`)))
				})
			})
		})
	})

	DescribeTable("ToPodIdentityAssociationID", func(piaARN, expectedID, expectedErr string) {
		piaID, err := api.ToPodIdentityAssociationID(piaARN)
		if expectedErr != "" {
			Expect(err).To(MatchError(ContainSubstring(expectedErr)))
			return
		}
		Expect(err).NotTo(HaveOccurred())
		Expect(piaID).To(Equal(expectedID))
	},
		Entry("valid PIA ARN", "arn:aws:eks:us-west-2:000:podidentityassociation/cluster/a-d3dw7wfvxtoatujeg", "a-d3dw7wfvxtoatujeg", ""),
		Entry("invalid PIA ARN format", "arn:aws:eks:us-west-2:000:podidentityassociation/a-d3dw7wfvxtoatujeg", "", "unexpected pod identity association ARN format"),
		Entry("invalid PIA ARN", "a-d3dw7wfvxtoatujeg", "", "parsing ARN"),
	)

	DescribeTable("addon pod identity association", func(addons []*api.Addon, expectedErr string) {
		clusterConfig := api.NewClusterConfig()
		clusterConfig.Addons = addons
		err := api.ValidateClusterConfig(clusterConfig)
		if expectedErr != "" {
			Expect(err).To(MatchError(ContainSubstring(expectedErr)))
		} else {
			Expect(err).NotTo(HaveOccurred())
		}
	},
		Entry("wellKnownPolicies specified", []*api.Addon{
			{
				Name: api.VPCCNIAddon,
				PodIdentityAssociations: &[]api.PodIdentityAssociation{
					{
						ServiceAccountName: "aws-node",
						WellKnownPolicies:  api.WellKnownPolicies{AutoScaler: true},
					},
				},
			},
		}, fmt.Sprintf("wellKnownPolicies is not supported for addon.podIdentityAssociations; use addon.useDefaultPodIdentityAssociations instead (addon: %s)", api.VPCCNIAddon)),
		Entry("tags specified", []*api.Addon{
			{
				Name: api.VPCCNIAddon,
				PodIdentityAssociations: &[]api.PodIdentityAssociation{
					{
						ServiceAccountName: "aws-node",
						Tags:               map[string]string{},
					},
				},
			},
		}, fmt.Sprintf("tags is not supported for addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)),
		Entry("pod identity associations specified with useDefaultPodIdentityAssociations", []*api.Addon{
			{
				Name:                              api.VPCCNIAddon,
				UseDefaultPodIdentityAssociations: true,
				PodIdentityAssociations: &[]api.PodIdentityAssociation{
					{
						ServiceAccountName:   "aws-node",
						PermissionPolicyARNs: []string{"arn:aws:iam::aws:policy/AmazonEKS_CNI_Policy"},
					},
				},
			},
		}, fmt.Sprintf("cannot specify both addon.useDefaultPodIdentityAssociations and addon.podIdentityAssociations (addon: %s)", api.VPCCNIAddon)),
		Entry("IRSA fields specified with useDefaultPodIdentityAssociations", []*api.Addon{
			{
				Name:                              api.VPCCNIAddon,
				ServiceAccountRoleARN:             "role-1",
				UseDefaultPodIdentityAssociations: true,
			},
		}, fmt.Sprintf("cannot specify serviceAccountRoleARN, wellKnownPolicies, attachPolicy or attachPolicyARNs"+
			" when addon.useDefaultPodIdentityAssociations is set (addon: %s)", api.VPCCNIAddon)),

		Entry("IRSA fields specified", []*api.Addon{
			{
				Name:                  api.VPCCNIAddon,
				ServiceAccountRoleARN: "role-1",
			},
		}, ""),
	)
})

func newInt(value int) *int {
	v := value
	return &v
}

func newNodeGroup() *api.NodeGroup {
	return &api.NodeGroup{
		NodeGroupBase: &api.NodeGroupBase{},
	}
}
