diff --git a/README.md b/README.md index 9defff15d1..205cb1dde9 100644 --- a/README.md +++ b/README.md @@ -342,20 +342,14 @@ Get the list of old nodegroups: old_nodegroups="$(eksctl get ng --cluster= --output=json | jq -r '.[].Name')" ``` -Edit config file to add new nodegroups. +Edit config file to add new nodegroups. You can remove old nodegroups from the config file now +or do it later. -If you have removed the definition of old nodegroups from the config file, then you can run: +To create all of new nodegroups defined in the config file, run: ``` eksctl create nodegroup --config-file= ``` -Alternatively, you can keep the old definitions in the config file and pass `--only` with a list of the new nodegroup -names that you want to add (you can use glob expressions, or list specific names - see ['Managing -nodegroup'](#managing-nodegroups) for details): -``` -eksctl create nodegroup --config-file= --only= -``` - Once you have new nodegroups in place, you can delete old ones: ``` for ng in $old_nodegroups ; do eksctl delete nodegroup --cluster= --name=$ng ; done diff --git a/pkg/cfn/manager/nodegroup.go b/pkg/cfn/manager/nodegroup.go index 99e3115b03..a274ab88a3 100644 --- a/pkg/cfn/manager/nodegroup.go +++ b/pkg/cfn/manager/nodegroup.go @@ -81,6 +81,20 @@ func (c *StackCollection) DescribeNodeGroupStacks() ([]*Stack, error) { return nodeGroupStacks, nil } +// ListNodeGroupStacks calls DescribeNodeGroupStacks and returns only nodegroup names +func (c *StackCollection) ListNodeGroupStacks() ([]string, error) { + stacks, err := c.DescribeNodeGroupStacks() + if err != nil { + return nil, err + } + + names := []string{} + for _, s := range stacks { + names = append(names, getNodeGroupName(s)) + } + return names, nil +} + // DescribeNodeGroupStacksAndResources calls DescribeNodeGroupStacks and fetches all resources, // then returns it in a map by nodegroup name func (c *StackCollection) DescribeNodeGroupStacksAndResources() (map[string]StackInfo, error) { diff --git a/pkg/cfn/manager/tasks.go b/pkg/cfn/manager/tasks.go index 546363a331..5d18fed5c8 100644 --- a/pkg/cfn/manager/tasks.go +++ b/pkg/cfn/manager/tasks.go @@ -3,6 +3,8 @@ package manager import ( "sync" + "k8s.io/apimachinery/pkg/util/sets" + "github.com/kris-nova/logger" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha4" @@ -60,18 +62,18 @@ func (c *StackCollection) RunSingleTask(t Task) []error { // the stacks (a cluster and one or more nodegroups); any errors // will be returned as a slice as soon as one of the tasks or group // of tasks is completed -func (c *StackCollection) CreateClusterWithNodeGroups() []error { +func (c *StackCollection) CreateClusterWithNodeGroups(onlySubset sets.String) []error { if errs := c.RunSingleTask(Task{c.CreateCluster, nil}); len(errs) > 0 { return errs } - return c.CreateAllNodeGroups() + return c.CreateAllNodeGroups(onlySubset) } // CreateAllNodeGroups runs all tasks required to create the node groups; // any errors will be returned as a slice as soon as one of the tasks // or group of tasks is completed -func (c *StackCollection) CreateAllNodeGroups() []error { +func (c *StackCollection) CreateAllNodeGroups(onlySubset sets.String) []error { errs := []error{} appendErr := func(err error) { errs = append(errs, err) @@ -79,9 +81,13 @@ func (c *StackCollection) CreateAllNodeGroups() []error { createAllNodeGroups := []Task{} for i := range c.spec.NodeGroups { + ng := c.spec.NodeGroups[i] + if onlySubset != nil && !onlySubset.Has(ng.Name) { + continue + } t := Task{ Call: c.CreateNodeGroup, - Data: c.spec.NodeGroups[i], + Data: ng, } createAllNodeGroups = append(createAllNodeGroups, t) } diff --git a/pkg/ctl/create/cluster.go b/pkg/ctl/create/cluster.go index 5f5257727d..5620aaf732 100644 --- a/pkg/ctl/create/cluster.go +++ b/pkg/ctl/create/cluster.go @@ -10,7 +10,6 @@ import ( "github.com/spf13/cobra" "github.com/spf13/pflag" - "github.com/weaveworks/eksctl/pkg/ami" api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha4" "github.com/weaveworks/eksctl/pkg/authconfigmap" "github.com/weaveworks/eksctl/pkg/ctl/cmdutils" @@ -103,89 +102,6 @@ func createClusterCmd(g *cmdutils.Grouping) *cobra.Command { return cmd } -// When passing the --without-nodegroup option, don't create nodegroups -func skipNodeGroupsIfRequested(cfg *api.ClusterConfig) { - if withoutNodeGroup { - cfg.NodeGroups = nil - logger.Warning("cluster will be created without an initial nodegroup") - } -} - -// checkEachNodeGroup iterates over each nodegroup and calls check function -// (this is need to avoid common goroutine-for-loop pitfall) -func checkEachNodeGroup(cfg *api.ClusterConfig, check func(i int, ng *api.NodeGroup) error) error { - for i := range cfg.NodeGroups { - if err := check(i, cfg.NodeGroups[i]); err != nil { - return err - } - } - return nil -} - -func newNodeGroupChecker(i int, ng *api.NodeGroup) error { - if err := api.ValidateNodeGroup(i, ng); err != nil { - return err - } - - // apply defaults - if ng.InstanceType == "" { - ng.InstanceType = api.DefaultNodeType - } - if ng.AMIFamily == "" { - ng.AMIFamily = ami.ImageFamilyAmazonLinux2 - } - if ng.AMI == "" { - ng.AMI = ami.ResolverStatic - } - - if ng.SecurityGroups == nil { - ng.SecurityGroups = &api.NodeGroupSGs{ - AttachIDs: []string{}, - } - } - if ng.SecurityGroups.WithLocal == nil { - ng.SecurityGroups.WithLocal = api.NewBoolTrue() - } - if ng.SecurityGroups.WithShared == nil { - ng.SecurityGroups.WithShared = api.NewBoolTrue() - } - - if ng.AllowSSH { - if ng.SSHPublicKeyPath == "" { - ng.SSHPublicKeyPath = defaultSSHPublicKey - } - } - - if ng.VolumeSize > 0 { - if ng.VolumeType == "" { - ng.VolumeType = api.DefaultNodeVolumeType - } - } - - if ng.IAM == nil { - ng.IAM = &api.NodeGroupIAM{} - } - if ng.IAM.WithAddonPolicies.ImageBuilder == nil { - ng.IAM.WithAddonPolicies.ImageBuilder = api.NewBoolFalse() - } - if ng.IAM.WithAddonPolicies.AutoScaler == nil { - ng.IAM.WithAddonPolicies.AutoScaler = api.NewBoolFalse() - } - if ng.IAM.WithAddonPolicies.ExternalDNS == nil { - ng.IAM.WithAddonPolicies.ExternalDNS = api.NewBoolFalse() - } - - return nil -} - -func checkSubnetsGiven(cfg *api.ClusterConfig) bool { - return cfg.VPC.Subnets != nil && len(cfg.VPC.Subnets.Private)+len(cfg.VPC.Subnets.Public) != 0 -} - -func checkSubnetsGivenAsFlags() bool { - return len(*subnets[api.SubnetTopologyPrivate])+len(*subnets[api.SubnetTopologyPublic]) != 0 -} - func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg string, cmd *cobra.Command) error { meta := cfg.Metadata @@ -195,6 +111,8 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri return err } + ngFilter := NewNodeGroupFilter() + if clusterConfigFile != "" { if err := eks.LoadConfigFromFile(clusterConfigFile, cfg); err != nil { return err @@ -258,7 +176,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri skipNodeGroupsIfRequested(cfg) - if err := checkEachNodeGroup(cfg, newNodeGroupChecker); err != nil { + if err := CheckEachNodeGroup(ngFilter, cfg, NewNodeGroupChecker); err != nil { return err } } else { @@ -276,7 +194,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri skipNodeGroupsIfRequested(cfg) - err := checkEachNodeGroup(cfg, func(i int, ng *api.NodeGroup) error { + err := CheckEachNodeGroup(ngFilter, cfg, func(i int, ng *api.NodeGroup) error { if ng.AllowSSH && ng.SSHPublicKeyPath == "" { return fmt.Errorf("--ssh-public-key must be non-empty string") } @@ -387,7 +305,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri return err } - if err := checkEachNodeGroup(cfg, canUseForPrivateNodeGroups); err != nil { + if err := CheckEachNodeGroup(ngFilter, cfg, canUseForPrivateNodeGroups); err != nil { return err } @@ -414,7 +332,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri return err } - if err := checkEachNodeGroup(cfg, canUseForPrivateNodeGroups); err != nil { + if err := CheckEachNodeGroup(ngFilter, cfg, canUseForPrivateNodeGroups); err != nil { return err } @@ -427,7 +345,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri return err } - err := checkEachNodeGroup(cfg, func(_ int, ng *api.NodeGroup) error { + err := CheckEachNodeGroup(ngFilter, cfg, func(_ int, ng *api.NodeGroup) error { // resolve AMI if err := ctl.EnsureAMI(meta.Version, ng); err != nil { return err @@ -462,15 +380,16 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri } { // core action + ngSubset := ngFilter.MatchAll(cfg) stackManager := ctl.NewStackManager(cfg) - if len(cfg.NodeGroups) == 1 { + if ngCount := ngSubset.Len(); ngCount == 1 && clusterConfigFile == "" { logger.Info("will create 2 separate CloudFormation stacks for cluster itself and the initial nodegroup") } else { - logger.Info("will create a CloudFormation stack for cluster itself and %d nodegroup stack(s)", len(cfg.NodeGroups)) - + ngFilter.LogInfo(cfg) + logger.Info("will create a CloudFormation stack for cluster itself and %d nodegroup stack(s)", ngCount) } logger.Info("if you encounter any issues, check CloudFormation console or try 'eksctl utils describe-stacks --region=%s --name=%s'", meta.Region, meta.Name) - errs := stackManager.CreateClusterWithNodeGroups() + errs := stackManager.CreateClusterWithNodeGroups(ngSubset) // read any errors (it only gets non-nil errors) if len(errs) > 0 { logger.Info("%d error(s) occurred and cluster hasn't been created properly, you may wish to check CloudFormation console", len(errs)) @@ -515,7 +434,7 @@ func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg stri return err } - err = checkEachNodeGroup(cfg, func(_ int, ng *api.NodeGroup) error { + err = CheckEachNodeGroup(ngFilter, cfg, func(_ int, ng *api.NodeGroup) error { // authorise nodes to join if err = authconfigmap.AddNodeGroup(clientSet, ng); err != nil { return err diff --git a/pkg/ctl/create/create_suite_test.go b/pkg/ctl/create/create_suite_test.go new file mode 100644 index 0000000000..a0a54323d2 --- /dev/null +++ b/pkg/ctl/create/create_suite_test.go @@ -0,0 +1,11 @@ +package create_test + +import ( + "testing" + + "github.com/weaveworks/eksctl/pkg/testutils" +) + +func TestSuite(t *testing.T) { + testutils.RegisterAndRun(t) +} diff --git a/pkg/ctl/create/nodegroup.go b/pkg/ctl/create/nodegroup.go index 44ce233242..4b5b0af34c 100644 --- a/pkg/ctl/create/nodegroup.go +++ b/pkg/ctl/create/nodegroup.go @@ -3,9 +3,7 @@ package create import ( "fmt" "os" - "strings" - "github.com/gobwas/glob" "github.com/kris-nova/logger" "github.com/pkg/errors" "github.com/spf13/cobra" @@ -20,8 +18,8 @@ import ( ) var ( - updateAuthConfigMap bool - nodeGroupFilter = "" + updateAuthConfigMap bool + nodeGroupOnlyFilters []string ) func createNodeGroupCmd(g *cmdutils.Grouping) *cobra.Command { @@ -52,8 +50,10 @@ func createNodeGroupCmd(g *cmdutils.Grouping) *cobra.Command { cmdutils.AddRegionFlag(fs, p) cmdutils.AddVersionFlag(fs, cfg.Metadata, `for nodegroups "auto" and "latest" can be used to automatically inherit version from the control plane or force latest`) fs.StringVarP(&clusterConfigFile, "config-file", "f", "", "load configuration from a file") - fs.StringVarP(&nodeGroupFilter, "only", "", "", + + fs.StringSliceVar(&nodeGroupOnlyFilters, "only", nil, "select a subset of nodegroups via comma-separted list of globs, e.g.: 'ng-*,nodegroup?,N*group'") + cmdutils.AddUpdateAuthConfigMap(&updateAuthConfigMap, fs, "Remove nodegroup IAM role from aws-auth configmap") }) @@ -73,70 +73,6 @@ func createNodeGroupCmd(g *cmdutils.Grouping) *cobra.Command { return cmd } -func filterNodeGroups(cfg *api.ClusterConfig) error { - if nodeGroupFilter == "" { - // no filter supplied - return nil - } - globstrs := strings.Split(nodeGroupFilter, ",") - globs := make([]glob.Glob, len(globstrs)) - for idx, g := range globstrs { - globs[idx] = glob.MustCompile(g) - } - nodegroups := cfg.NodeGroups - filtered := make([]*api.NodeGroup, 0) - for _, ng := range nodegroups { - for _, g := range globs { - if g.Match(ng.Name) { - filtered = append(filtered, ng) - break - } - } - } - if len(filtered) == 0 { - return fmt.Errorf("No nodegroups match filter specification: %s", nodeGroupFilter) - } - cfg.NodeGroups = filtered - return nil -} - -func checkVersion(ctl *eks.ClusterProvider, meta *api.ClusterMeta) error { - switch meta.Version { - case "auto": - break - case "": - meta.Version = "auto" - case "latest": - meta.Version = api.LatestVersion - logger.Info("will use version latest version (%s) for new nodegroup(s)", meta.Version) - default: - validVersion := false - for _, v := range api.SupportedVersions() { - if meta.Version == v { - validVersion = true - } - } - if !validVersion { - return fmt.Errorf("invalid version %s, supported values: auto, latest, %s", meta.Version, strings.Join(api.SupportedVersions(), ", ")) - } - } - - if v := ctl.ControlPlaneVersion(); v == "" { - return fmt.Errorf("unable to get control plane version") - } else if meta.Version == "auto" { - meta.Version = v - logger.Info("will use version %s for new nodegroup(s) based on control plane version", meta.Version) - } else if meta.Version != v { - hint := "--version=auto" - if clusterConfigFile != "" { - hint = "metadata.version: auto" - } - logger.Warning("will use version %s for new nodegroup(s), while control plane version is %s; to automatically inherit the version use %q", meta.Version, v, hint) - } - - return nil -} - func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg string, cmd *cobra.Command) error { meta := cfg.Metadata @@ -146,6 +82,8 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s return err } + ngFilter := NewNodeGroupFilter() + if clusterConfigFile != "" { if err := eks.LoadConfigFromFile(clusterConfigFile, cfg); err != nil { return err @@ -162,11 +100,6 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s p.Region = meta.Region - // Limit nodegroups to set specified on command line via globs - if err := filterNodeGroups(cfg); err != nil { - return err - } - incompatibleFlags := []string{ "name", "cluster", @@ -198,7 +131,11 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s } } - if err := checkEachNodeGroup(cfg, newNodeGroupChecker); err != nil { + if err := ngFilter.ApplyOnlyFilter(nodeGroupOnlyFilters, cfg); err != nil { + return err + } + + if err := CheckEachNodeGroup(ngFilter, cfg, NewNodeGroupChecker); err != nil { return err } } else { @@ -208,11 +145,17 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s return errors.New("--cluster must be set") } - if nodeGroupFilter != "" { - return errors.New("cannot use --only unless a config file is specified via --config-file/-f") + incompatibleFlags := []string{ + "only", } - err := checkEachNodeGroup(cfg, func(i int, ng *api.NodeGroup) error { + for _, f := range incompatibleFlags { + if cmd.Flag(f).Changed { + return fmt.Errorf("cannot use --%s unless a config file is specified via --config-file/-f", f) + } + } + + err := CheckEachNodeGroup(ngFilter, cfg, func(i int, ng *api.NodeGroup) error { if ng.AllowSSH && ng.SSHPublicKeyPath == "" { return fmt.Errorf("--ssh-public-key must be non-empty string") } @@ -254,7 +197,13 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s return errors.Wrapf(err, "getting VPC configuration for cluster %q", cfg.Metadata.Name) } - err := checkEachNodeGroup(cfg, func(_ int, ng *api.NodeGroup) error { + stackManager := ctl.NewStackManager(cfg) + + if err := ngFilter.ApplyExistingFilter(stackManager); err != nil { + return err + } + + err := CheckEachNodeGroup(ngFilter, cfg, func(_ int, ng *api.NodeGroup) error { // resolve AMI if err := ctl.EnsureAMI(meta.Version, ng); err != nil { return err @@ -282,15 +231,20 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s return err } - stackManager := ctl.NewStackManager(cfg) - if err := ctl.ValidateClusterForCompatibility(cfg, stackManager); err != nil { return errors.Wrap(err, "cluster compatibility check failed") } + ngSubset := ngFilter.MatchAll(cfg) + ngCount := ngSubset.Len() + { - logger.Info("will create a CloudFormation stack for each of %d nodegroups in cluster %q", len(cfg.NodeGroups), cfg.Metadata.Name) - errs := stackManager.CreateAllNodeGroups() + ngFilter.LogInfo(cfg) + if ngCount > 0 { + logger.Info("will create a CloudFormation stack for each of %d nodegroups in cluster %q", ngCount, cfg.Metadata.Name) + } + + errs := stackManager.CreateAllNodeGroups(ngSubset) if len(errs) > 0 { logger.Info("%d error(s) occurred and nodegroups haven't been created properly, you may wish to check CloudFormation console", len(errs)) logger.Info("to cleanup resources, run 'eksctl delete nodegroup --region=%s --cluster=%s --name=' for each of the failed nodegroup", cfg.Metadata.Region, cfg.Metadata.Name) @@ -309,7 +263,7 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s return err } - err = checkEachNodeGroup(cfg, func(_ int, ng *api.NodeGroup) error { + err = CheckEachNodeGroup(ngFilter, cfg, func(_ int, ng *api.NodeGroup) error { if updateAuthConfigMap { // authorise nodes to join if err = authconfigmap.AddNodeGroup(clientSet, ng); err != nil { @@ -333,7 +287,7 @@ func doCreateNodeGroups(p *api.ProviderConfig, cfg *api.ClusterConfig, nameArg s if err != nil { return err } - logger.Success("created nodegroups in cluster %q", cfg.Metadata.Name) + logger.Success("created %d nodegroup(s) in cluster %q", ngCount, cfg.Metadata.Name) } if err := ctl.ValidateExistingNodeGroupsForCompatibility(cfg, stackManager); err != nil { diff --git a/pkg/ctl/create/utils.go b/pkg/ctl/create/utils.go new file mode 100644 index 0000000000..ddecb0548b --- /dev/null +++ b/pkg/ctl/create/utils.go @@ -0,0 +1,247 @@ +package create + +import ( + "fmt" + "strings" + + "github.com/gobwas/glob" + "github.com/kris-nova/logger" + "github.com/pkg/errors" + + "k8s.io/apimachinery/pkg/util/sets" + + "github.com/weaveworks/eksctl/pkg/ami" + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha4" + "github.com/weaveworks/eksctl/pkg/cfn/manager" + "github.com/weaveworks/eksctl/pkg/eks" +) + +// NodeGroupFilter holds filter configuration +type NodeGroupFilter struct { + IgnoreAllExisting bool + + existing sets.String + only []glob.Glob + onlySpec string +} + +// NewNodeGroupFilter create new NodeGroupFilter instance +func NewNodeGroupFilter() *NodeGroupFilter { + return &NodeGroupFilter{ + IgnoreAllExisting: true, + + existing: sets.NewString(), + } +} + +// ApplyOnlyFilter parses given globs for exclusive filtering +func (f *NodeGroupFilter) ApplyOnlyFilter(globExprs []string, cfg *api.ClusterConfig) error { + for _, expr := range globExprs { + compiledExpr, err := glob.Compile(expr) + if err != nil { + return errors.Wrapf(err, "parsing glob filter %q", expr) + } + f.only = append(f.only, compiledExpr) + } + f.onlySpec = strings.Join(globExprs, ",") + return f.onlyFilterMatchesAnything(cfg) +} + +func (f *NodeGroupFilter) onlyFilterMatchesAnything(cfg *api.ClusterConfig) error { + if len(f.only) == 0 { + return nil + } + for i := range cfg.NodeGroups { + for _, g := range f.only { + if g.Match(cfg.NodeGroups[i].Name) { + return nil + } + } + } + return fmt.Errorf("no nodegroups match filter specification: %q", f.onlySpec) +} + +// ApplyExistingFilter uses stackManager to list existing nodegroup stacks and configures +// the filter accordingly +func (f *NodeGroupFilter) ApplyExistingFilter(stackManager *manager.StackCollection) error { + if !f.IgnoreAllExisting { + return nil + } + + existing, err := stackManager.ListNodeGroupStacks() + if err != nil { + return err + } + + f.existing.Insert(existing...) + + return nil +} + +// Match checks given nodegroup against the filter +func (f *NodeGroupFilter) Match(ng *api.NodeGroup) bool { + if f.IgnoreAllExisting && f.existing.Has(ng.Name) { + return false + } + + for _, g := range f.only { + if g.Match(ng.Name) { + return true // return first match + } + } + + // if no globs were given, match everything, + // if false - we haven't matched anything so far + return len(f.only) == 0 +} + +// MatchAll checks all nodegroups against the filter and returns all of +// matching names as set +func (f *NodeGroupFilter) MatchAll(cfg *api.ClusterConfig) sets.String { + names := sets.NewString() + for _, ng := range cfg.NodeGroups { + if f.Match(ng) { + names.Insert(ng.Name) + } + } + return names +} + +// LogInfo prints out a user-friendly message about how filter was applied +func (f *NodeGroupFilter) LogInfo(cfg *api.ClusterConfig) { + count := f.MatchAll(cfg).Len() + filteredOutCount := len(cfg.NodeGroups) - count + if filteredOutCount > 0 { + reasons := []string{} + if f.onlySpec != "" { + reasons = append(reasons, fmt.Sprintf("--only=%q was given", f.onlySpec)) + } + if existingCount := f.existing.Len(); existingCount > 0 { + reasons = append(reasons, fmt.Sprintf("%d nodegroup(s) (%s) already exist", existingCount, strings.Join(f.existing.List(), ", "))) + } + logger.Info("%d nodegroup(s) were filtered out: %s", filteredOutCount, strings.Join(reasons, ", ")) + } +} + +// CheckEachNodeGroup iterates over each nodegroup and calls check function +// (this is needed to avoid common goroutine-for-loop pitfall) +func CheckEachNodeGroup(f *NodeGroupFilter, cfg *api.ClusterConfig, check func(i int, ng *api.NodeGroup) error) error { + for i, ng := range cfg.NodeGroups { + if f.Match(ng) { + if err := check(i, ng); err != nil { + return err + } + } + } + return nil +} + +// NewNodeGroupChecker validates a new nodegroup and applies defaults +func NewNodeGroupChecker(i int, ng *api.NodeGroup) error { + if err := api.ValidateNodeGroup(i, ng); err != nil { + return err + } + + // apply defaults + if ng.InstanceType == "" { + ng.InstanceType = api.DefaultNodeType + } + if ng.AMIFamily == "" { + ng.AMIFamily = ami.ImageFamilyAmazonLinux2 + } + if ng.AMI == "" { + ng.AMI = ami.ResolverStatic + } + + if ng.SecurityGroups == nil { + ng.SecurityGroups = &api.NodeGroupSGs{ + AttachIDs: []string{}, + } + } + if ng.SecurityGroups.WithLocal == nil { + ng.SecurityGroups.WithLocal = api.NewBoolTrue() + } + if ng.SecurityGroups.WithShared == nil { + ng.SecurityGroups.WithShared = api.NewBoolTrue() + } + + if ng.AllowSSH { + if ng.SSHPublicKeyPath == "" { + ng.SSHPublicKeyPath = defaultSSHPublicKey + } + } + + if ng.VolumeSize > 0 { + if ng.VolumeType == "" { + ng.VolumeType = api.DefaultNodeVolumeType + } + } + + if ng.IAM == nil { + ng.IAM = &api.NodeGroupIAM{} + } + if ng.IAM.WithAddonPolicies.ImageBuilder == nil { + ng.IAM.WithAddonPolicies.ImageBuilder = api.NewBoolFalse() + } + if ng.IAM.WithAddonPolicies.AutoScaler == nil { + ng.IAM.WithAddonPolicies.AutoScaler = api.NewBoolFalse() + } + if ng.IAM.WithAddonPolicies.ExternalDNS == nil { + ng.IAM.WithAddonPolicies.ExternalDNS = api.NewBoolFalse() + } + + return nil +} + +// When passing the --without-nodegroup option, don't create nodegroups +func skipNodeGroupsIfRequested(cfg *api.ClusterConfig) { + if withoutNodeGroup { + cfg.NodeGroups = nil + logger.Warning("cluster will be created without an initial nodegroup") + } +} + +func checkSubnetsGiven(cfg *api.ClusterConfig) bool { + return cfg.VPC.Subnets != nil && len(cfg.VPC.Subnets.Private)+len(cfg.VPC.Subnets.Public) != 0 +} + +func checkSubnetsGivenAsFlags() bool { + return len(*subnets[api.SubnetTopologyPrivate])+len(*subnets[api.SubnetTopologyPublic]) != 0 +} + +func checkVersion(ctl *eks.ClusterProvider, meta *api.ClusterMeta) error { + switch meta.Version { + case "auto": + break + case "": + meta.Version = "auto" + case "latest": + meta.Version = api.LatestVersion + logger.Info("will use version latest version (%s) for new nodegroup(s)", meta.Version) + default: + validVersion := false + for _, v := range api.SupportedVersions() { + if meta.Version == v { + validVersion = true + } + } + if !validVersion { + return fmt.Errorf("invalid version %s, supported values: auto, latest, %s", meta.Version, strings.Join(api.SupportedVersions(), ", ")) + } + } + + if v := ctl.ControlPlaneVersion(); v == "" { + return fmt.Errorf("unable to get control plane version") + } else if meta.Version == "auto" { + meta.Version = v + logger.Info("will use version %s for new nodegroup(s) based on control plane version", meta.Version) + } else if meta.Version != v { + hint := "--version=auto" + if clusterConfigFile != "" { + hint = "metadata.version: auto" + } + logger.Warning("will use version %s for new nodegroup(s), while control plane version is %s; to automatically inherit the version use %q", meta.Version, v, hint) + } + + return nil +} diff --git a/pkg/ctl/create/utils_test.go b/pkg/ctl/create/utils_test.go new file mode 100644 index 0000000000..d9c22570ad --- /dev/null +++ b/pkg/ctl/create/utils_test.go @@ -0,0 +1,351 @@ +package create_test + +import ( + "bytes" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + api "github.com/weaveworks/eksctl/pkg/apis/eksctl.io/v1alpha4" + . "github.com/weaveworks/eksctl/pkg/ctl/create" + "github.com/weaveworks/eksctl/pkg/printers" +) + +var _ = Describe("create utils", func() { + + newClusterConfig := func() *api.ClusterConfig { + cfg := api.NewClusterConfig() + + cfg.Metadata.Name = "test-3x3-ngs" + cfg.Metadata.Region = "eu-central-1" + + return cfg + } + addGroupA := func(cfg *api.ClusterConfig) { + var ng *api.NodeGroup + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng1a" + ng.VolumeSize = 768 + ng.VolumeType = "io1" + ng.IAM.AttachPolicyARNs = []string{"foo"} + ng.Labels = map[string]string{"group": "a", "seq": "1"} + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng2a" + ng.IAM.AttachPolicyARNs = []string{"bar"} + ng.Labels = map[string]string{"group": "a", "seq": "2"} + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng3a" + ng.ClusterDNS = "1.2.3.4" + ng.InstanceType = "m3.large" + ng.AllowSSH = true + ng.Labels = map[string]string{"group": "a", "seq": "3"} + } + + addGroupB := func(cfg *api.ClusterConfig) { + var ng *api.NodeGroup + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng1b" + ng.AllowSSH = true + ng.Labels = map[string]string{"group": "b", "seq": "1"} + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng2b" + ng.ClusterDNS = "4.2.8.14" + ng.InstanceType = "m5.xlarge" + ng.SecurityGroups.AttachIDs = []string{"sg-1", "sg-2"} + ng.SecurityGroups.WithLocal = api.NewBoolFalse() + ng.Labels = map[string]string{"group": "b", "seq": "1"} + + ng = cfg.NewNodeGroup() + ng.Name = "test-ng3b" + ng.VolumeSize = 192 + ng.SecurityGroups.AttachIDs = []string{"sg-1", "sg-2"} + ng.SecurityGroups.WithLocal = api.NewBoolFalse() + ng.Labels = map[string]string{"group": "b", "seq": "1"} + } + + expected := ` + { + "kind": "ClusterConfig", + "apiVersion": "eksctl.io/v1alpha4", + "metadata": { + "name": "test-3x3-ngs", + "region": "eu-central-1", + "version": "1.11" + }, + "iam": {}, + "vpc": { + "cidr": "192.168.0.0/16" + }, + "nodeGroups": [ + { + "name": "test-ng1a", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m5.large", + "privateNetworking": false, + "securityGroups": { + "withShared": true, + "withLocal": true + }, + "volumeSize": 768, + "volumeType": "io1", + "labels": { + "group": "a", + "seq": "1" + }, + "allowSSH": false, + "iam": { + "attachPolicyARNs": [ + "foo" + ], + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + } + }, + { + "name": "test-ng2a", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m5.large", + "privateNetworking": false, + "securityGroups": { + "withShared": true, + "withLocal": true + }, + "volumeSize": 0, + "volumeType": "gp2", + "labels": { + "group": "a", + "seq": "2" + }, + "allowSSH": false, + "iam": { + "attachPolicyARNs": [ + "bar" + ], + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + } + }, + { + "name": "test-ng3a", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m3.large", + "privateNetworking": false, + "securityGroups": { + "withShared": true, + "withLocal": true + }, + "volumeSize": 0, + "volumeType": "gp2", + "labels": { + "group": "a", + "seq": "3" + }, + "allowSSH": true, + "sshPublicKeyPath": "~/.ssh/id_rsa.pub", + "iam": { + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + }, + "clusterDNS": "1.2.3.4" + }, + { + "name": "test-ng1b", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m5.large", + "privateNetworking": false, + "securityGroups": { + "withShared": true, + "withLocal": true + }, + "volumeSize": 0, + "volumeType": "gp2", + "labels": { + "group": "b", + "seq": "1" + }, + "allowSSH": true, + "sshPublicKeyPath": "~/.ssh/id_rsa.pub", + "iam": { + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + } + }, + { + "name": "test-ng2b", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m5.xlarge", + "privateNetworking": false, + "securityGroups": { + "attachIDs": [ + "sg-1", + "sg-2" + ], + "withShared": true, + "withLocal": false + }, + "volumeSize": 0, + "volumeType": "gp2", + "labels": { + "group": "b", + "seq": "1" + }, + "allowSSH": false, + "iam": { + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + }, + "clusterDNS": "4.2.8.14" + }, + { + "name": "test-ng3b", + "ami": "static", + "amiFamily": "AmazonLinux2", + "instanceType": "m5.large", + "privateNetworking": false, + "securityGroups": { + "attachIDs": [ + "sg-1", + "sg-2" + ], + "withShared": true, + "withLocal": false + }, + "volumeSize": 192, + "volumeType": "gp2", + "labels": { + "group": "b", + "seq": "1" + }, + "allowSSH": false, + "iam": { + "withAddonPolicies": { + "imageBuilder": false, + "autoScaler": false, + "externalDNS": false, + "appMesh": false, + "ebs": false + } + } + } + ] + } + ` + + Context("CheckEachNodeGroup", func() { + + It("should iterate over unique nodegroups", func() { + cfg := newClusterConfig() + addGroupA(cfg) + f := NewNodeGroupFilter() + + names := []string{} + CheckEachNodeGroup(f, cfg, func(i int, nodeGroup *api.NodeGroup) error { + Expect(nodeGroup).To(Equal(cfg.NodeGroups[i])) + names = append(names, nodeGroup.Name) + return nil + }) + Expect(names).To(Equal([]string{"test-ng1a", "test-ng2a", "test-ng3a"})) + + names = []string{} + cfg.NodeGroups[0].Name = "ng-x0" + cfg.NodeGroups[1].Name = "ng-x1" + cfg.NodeGroups[2].Name = "ng-x2" + CheckEachNodeGroup(f, cfg, func(i int, nodeGroup *api.NodeGroup) error { + Expect(nodeGroup).To(Equal(cfg.NodeGroups[i])) + names = append(names, nodeGroup.Name) + return nil + }) + Expect(names).To(Equal([]string{"ng-x0", "ng-x1", "ng-x2"})) + }) + + It("should iterate over unique nodegroups and filter some out", func() { + cfg := newClusterConfig() + addGroupA(cfg) + addGroupB(cfg) + f := NewNodeGroupFilter() + + names := []string{} + CheckEachNodeGroup(f, cfg, func(i int, nodeGroup *api.NodeGroup) error { + Expect(nodeGroup).To(Equal(cfg.NodeGroups[i])) + names = append(names, nodeGroup.Name) + return nil + }) + Expect(names).To(Equal([]string{"test-ng1a", "test-ng2a", "test-ng3a", "test-ng1b", "test-ng2b", "test-ng3b"})) + + names = []string{} + + err := f.ApplyOnlyFilter([]string{"t?xyz?", "ab*z123?"}, cfg) + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(`no nodegroups match filter specification: "t?xyz?,ab*z123?"`)) + + err = f.ApplyOnlyFilter([]string{"test-ng1?", "te*-ng3?"}, cfg) + Expect(err).ToNot(HaveOccurred()) + CheckEachNodeGroup(f, cfg, func(i int, nodeGroup *api.NodeGroup) error { + Expect(nodeGroup).To(Equal(cfg.NodeGroups[i])) + names = append(names, nodeGroup.Name) + return nil + }) + Expect(names).To(Equal([]string{"test-ng1a", "test-ng3a", "test-ng1b", "test-ng3b"})) + }) + + It("should iterate over unique nodegroups and apply defaults with NewNodeGroupChecker", func() { + cfg := newClusterConfig() + addGroupA(cfg) + addGroupB(cfg) + f := NewNodeGroupFilter() + + printer := printers.NewJSONPrinter() + + names := []string{} + CheckEachNodeGroup(f, cfg, NewNodeGroupChecker) + + CheckEachNodeGroup(f, cfg, func(i int, nodeGroup *api.NodeGroup) error { + Expect(nodeGroup).To(Equal(cfg.NodeGroups[i])) + names = append(names, nodeGroup.Name) + return nil + }) + Expect(names).To(Equal([]string{"test-ng1a", "test-ng2a", "test-ng3a", "test-ng1b", "test-ng2b", "test-ng3b"})) + + w := &bytes.Buffer{} + + printer.PrintObj(cfg, w) + + Expect(w.Bytes()).To(MatchJSON(expected)) + }) + }) +})