Skip to content

Commit

Permalink
Add abstraction for grouping flags
Browse files Browse the repository at this point in the history
  • Loading branch information
errordeveloper committed Dec 17, 2018
1 parent 8745630 commit 17b0702
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 123 deletions.
39 changes: 23 additions & 16 deletions cmd/eksctl/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/kris-nova/logger"
"github.com/spf13/cobra"
"github.com/weaveworks/eksctl/pkg/ctl/cmdutils"
"github.com/weaveworks/eksctl/pkg/ctl/completion"
"github.com/weaveworks/eksctl/pkg/ctl/create"
"github.com/weaveworks/eksctl/pkg/ctl/delete"
Expand All @@ -14,23 +15,27 @@ import (
"github.com/weaveworks/eksctl/pkg/ctl/utils"
)

var (
rootCmd = &cobra.Command{
Use: "eksctl",
Short: "a CLI for Amazon EKS",
Run: func(c *cobra.Command, _ []string) {
if err := c.Help(); err != nil {
logger.Debug("ignoring error %q", err.Error())
}
},
}
colorValue string
)
var rootCmd = &cobra.Command{
Use: "eksctl [command]",
Short: "a CLI for Amazon EKS",
Run: func(c *cobra.Command, _ []string) {
if err := c.Help(); err != nil {
logger.Debug("ignoring error %q", err.Error())
}
},
}

func init() {
addCommands()
rootCmd.PersistentFlags().IntVarP(&logger.Level, "verbose", "v", 3, "set log level, use 0 to silence, 4 for debugging and 5 for debugging with AWS debug logging")

var colorValue string

g := cmdutils.NewGrouping()

addCommands(g)

rootCmd.PersistentFlags().BoolP("help", "h", false, "help for this command")
rootCmd.PersistentFlags().StringVarP(&colorValue, "color", "C", "true", "toggle colorized logs (true,false,fabulous)")
rootCmd.PersistentFlags().IntVarP(&logger.Level, "verbose", "v", 3, "set log level, use 0 to silence, 4 for debugging and 5 for debugging with AWS debug logging")

cobra.OnInitialize(func() {
// Control colored output
Expand All @@ -52,6 +57,8 @@ func init() {
logger.Timestamps = true
}
})

rootCmd.SetUsageFunc(g.Usage)
}

func main() {
Expand All @@ -61,9 +68,9 @@ func main() {
}
}

func addCommands() {
func addCommands(g *cmdutils.Grouping) {
rootCmd.AddCommand(versionCmd())
rootCmd.AddCommand(create.Command())
rootCmd.AddCommand(create.Command(g))
rootCmd.AddCommand(delete.Command())
rootCmd.AddCommand(get.Command())
rootCmd.AddCommand(scale.Command())
Expand Down
26 changes: 16 additions & 10 deletions pkg/ctl/cmdutils/cmdutils.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,22 @@ func GetNameArg(args []string) string {
}

// AddCommonFlagsForAWS adds common flags for api.ProviderConfig
func AddCommonFlagsForAWS(fs *pflag.FlagSet, p *api.ProviderConfig) {
fs.StringVarP(&p.Region, "region", "r", "", "AWS region")
fs.StringVarP(&p.Profile, "profile", "p", "", "AWS credentials profile to use (overrides the AWS_PROFILE environment variable)")

fs.DurationVar(&p.WaitTimeout, "aws-api-timeout", api.DefaultWaitTimeout, "")
// TODO deprecate in 0.2.0
if err := fs.MarkHidden("aws-api-timeout"); err != nil {
logger.Debug("ignoring error %q", err.Error())
}
fs.DurationVar(&p.WaitTimeout, "timeout", api.DefaultWaitTimeout, "max wait time in any polling operations")
func AddCommonFlagsForAWS(group *NamedFlagSetGroup, p *api.ProviderConfig) {
group.InFlagSet("AWS client", func(fs *pflag.FlagSet) {
fs.StringVarP(&p.Profile, "profile", "p", "", "AWS credentials profile to use (overrides the AWS_PROFILE environment variable)")

fs.DurationVar(&p.WaitTimeout, "aws-api-timeout", api.DefaultWaitTimeout, "")
// TODO deprecate in 0.2.0
if err := fs.MarkHidden("aws-api-timeout"); err != nil {
logger.Debug("ignoring error %q", err.Error())
}
fs.DurationVar(&p.WaitTimeout, "timeout", api.DefaultWaitTimeout, "max wait time in any polling operations")
})

group.InFlagSet("General", func(fs *pflag.FlagSet) {
fs.StringVarP(&p.Region, "region", "r", "", "AWS region")
})

}

// AddCommonFlagsForKubeconfig adds common flags for controlling how output kubeconfig is written
Expand Down
106 changes: 106 additions & 0 deletions pkg/ctl/cmdutils/group.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmdutils

import (
"fmt"
"strings"
"unicode"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
)

// Grouping holds a superset of all flagsets for all commands
type Grouping struct {
groups map[*cobra.Command]*NamedFlagSetGroup
}

type namedFlagSet struct {
name string
fs *pflag.FlagSet
}

// NamedFlagSetGroup holds a single group of flagsets
type NamedFlagSetGroup struct {
list []namedFlagSet
}

// NewGrouping creates an instance of Grouping
func NewGrouping() *Grouping {
return &Grouping{
make(map[*cobra.Command]*NamedFlagSetGroup),
}
}

// New creates a new group of flagsets for use with a subcommand
func (g *Grouping) New(cmd *cobra.Command) *NamedFlagSetGroup {
n := &NamedFlagSetGroup{}
g.groups[cmd] = n
return n
}

// InFlagSet returs new or existing named GlagSet in a group
func (n *NamedFlagSetGroup) InFlagSet(name string, cb func(*pflag.FlagSet)) {
for _, nfs := range n.list {
if nfs.name == name {
cb(nfs.fs)
return
}
}

nfs := namedFlagSet{
name: name,
fs: &pflag.FlagSet{},
}
cb(nfs.fs)
n.list = append(n.list, nfs)
}

// AddTo mixes all flagsets in the given group into another flagset
func (n *NamedFlagSetGroup) AddTo(cmd *cobra.Command) {
for _, nfs := range n.list {
cmd.Flags().AddFlagSet(nfs.fs)
}
}

// Usage is for use with (*cobra.Command).SetUsageFunc
func (g *Grouping) Usage(cmd *cobra.Command) error {
if cmd == nil {
return fmt.Errorf("nil command")
}

group := g.groups[cmd]

usage := []string{fmt.Sprintf("Usage: %s", cmd.UseLine())}

if cmd.HasAvailableSubCommands() {
usage = append(usage, "\nCommands:")
for _, subCommand := range cmd.Commands() {
usage = append(usage, fmt.Sprintf(" %s %-10s %s", cmd.CommandPath(), subCommand.Name(), subCommand.Short))
}
}

if len(cmd.Aliases) > 0 {
usage = append(usage, "\nAliases: "+cmd.NameAndAliases())
}

if group != nil {
for _, nfs := range group.list {
usage = append(usage, fmt.Sprintf("\n%s flags:", nfs.name))
usage = append(usage, strings.TrimRightFunc(nfs.fs.FlagUsages(), unicode.IsSpace))
}
}

usage = append(usage, "\nCommon flags:")
if len(cmd.PersistentFlags().FlagUsages()) != 0 {
usage = append(usage, strings.TrimRightFunc(cmd.PersistentFlags().FlagUsages(), unicode.IsSpace))
}
if len(cmd.InheritedFlags().FlagUsages()) != 0 {
usage = append(usage, strings.TrimRightFunc(cmd.InheritedFlags().FlagUsages(), unicode.IsSpace))
}

usage = append(usage, fmt.Sprintf("\nUse '%s [command] --help' for more information about a command.\n", cmd.CommandPath()))

cmd.Println(strings.Join(usage, "\n"))

return nil
}
120 changes: 39 additions & 81 deletions pkg/ctl/create/cluster.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,6 @@ package create
import (
"fmt"
"os"
"strings"
"unicode"

"github.com/kris-nova/logger"
"github.com/pkg/errors"
Expand Down Expand Up @@ -36,7 +34,7 @@ var (
subnets map[api.SubnetTopology]*[]string
)

func createClusterCmd() *cobra.Command {
func createClusterCmd(g *cmdutils.Grouping) *cobra.Command {
p := &api.ProviderConfig{}
cfg := api.NewClusterConfig()
ng := cfg.NewNodeGroup()
Expand All @@ -52,101 +50,61 @@ func createClusterCmd() *cobra.Command {
},
}

fs := cmd.Flags()

cmdutils.AddCommonFlagsForAWS(fs, p)
group := g.New(cmd)

exampleClusterName := utils.ClusterName("", "")

fs.StringVarP(&cfg.Metadata.Name, "name", "n", "", fmt.Sprintf("EKS cluster name (generated if unspecified, e.g. %q)", exampleClusterName))

fs.StringToStringVarP(&cfg.Metadata.Tags, "tags", "", map[string]string{}, `A list of KV pairs used to tag the AWS resources (e.g. "Owner=John Doe,Team=Some Team")`)

fs.StringVarP(&ng.InstanceType, "node-type", "t", defaultNodeType, "node instance type")
fs.IntVarP(&ng.DesiredCapacity, "nodes", "N", api.DefaultNodeCount, "total number of nodes (desired capacity of ASG)")

// TODO: https://github.com/weaveworks/eksctl/issues/28
fs.IntVarP(&ng.MinSize, "nodes-min", "m", 0, "minimum nodes in ASG (leave unset for a static nodegroup)")
fs.IntVarP(&ng.MaxSize, "nodes-max", "M", 0, "maximum nodes in ASG (leave unset for a static nodegroup)")

fs.IntVarP(&ng.VolumeSize, "node-volume-size", "", 0, "Node volume size (in GB)")
fs.IntVar(&ng.MaxPodsPerNode, "max-pods-per-node", 0, "maximum number of pods per node (set automatically if unspecified)")
fs.StringSliceVar(&availabilityZones, "zones", nil, "(auto-select if unspecified)")

fs.BoolVar(&ng.AllowSSH, "ssh-access", false, "control SSH access for nodes")
fs.StringVar(&ng.SSHPublicKeyPath, "ssh-public-key", defaultSSHPublicKey, "SSH public key to use for nodes (import from local path, or use existing EC2 key pair)")

fs.BoolVar(&writeKubeconfig, "write-kubeconfig", true, "toggle writing of kubeconfig")
cmdutils.AddCommonFlagsForKubeconfig(fs, &kubeconfigPath, &setContext, &autoKubeconfigPath, exampleClusterName)
group.InFlagSet("General", func(fs *pflag.FlagSet) {
fs.StringVarP(&cfg.Metadata.Name, "name", "n", "", fmt.Sprintf("EKS cluster name (generated if unspecified, e.g. %q)", exampleClusterName))
fs.StringToStringVarP(&cfg.Metadata.Tags, "tags", "", map[string]string{}, `A list of KV pairs used to tag the AWS resources (e.g. "Owner=John Doe,Team=Some Team")`)
fs.StringSliceVar(&availabilityZones, "zones", nil, "(auto-select if unspecified)")
})

fs.BoolVar(&cfg.Addons.WithIAM.PolicyAmazonEC2ContainerRegistryPowerUser, "full-ecr-access", false, "enable full access to ECR")
fs.BoolVar(&cfg.Addons.WithIAM.PolicyAutoScaling, "asg-access", false, "enable iam policy dependency for cluster-autoscaler")
fs.BoolVar(&cfg.Addons.Storage, "storage-class", true, "if true (default) then a default StorageClass of type gp2 provisioned by EBS will be created")
group.InFlagSet("Initial nodegroup", func(fs *pflag.FlagSet) {
fs.StringVarP(&ng.InstanceType, "node-type", "t", defaultNodeType, "node instance type")
fs.IntVarP(&ng.DesiredCapacity, "nodes", "N", api.DefaultNodeCount, "total number of nodes (desired capacity of ASG)")

fs.StringVar(&ng.AMI, "node-ami", ami.ResolverStatic, "Advanced use cases only. If 'static' is supplied (default) then eksctl will use static AMIs; if 'auto' is supplied then eksctl will automatically set the AMI based on region/instance type; if any other value is supplied it will override the AMI to use for the nodes. Use with extreme care.")
fs.StringVar(&ng.AMIFamily, "node-ami-family", ami.ImageFamilyAmazonLinux2, "Advanced use cases only. If 'AmazonLinux2' is supplied (default), then eksctl will use the offical AWS EKS AMIs (Amazon Linux 2); if 'Ubuntu1804' is supplied, then eksctl will use the offical Canonical EKS AMIs (Ubuntu 18.04).")
// TODO: https://github.com/weaveworks/eksctl/issues/28
fs.IntVarP(&ng.MinSize, "nodes-min", "m", 0, "minimum nodes in ASG (leave unset for a static nodegroup)")
fs.IntVarP(&ng.MaxSize, "nodes-max", "M", 0, "maximum nodes in ASG (leave unset for a static nodegroup)")

fs.StringVar(&kopsClusterNameForVPC, "vpc-from-kops-cluster", "", "re-use VPC from a given kops cluster")
fs.IntVarP(&ng.VolumeSize, "node-volume-size", "", 0, "Node volume size (in GB)")
fs.IntVar(&ng.MaxPodsPerNode, "max-pods-per-node", 0, "maximum number of pods per node (set automatically if unspecified)")

fs.IPNetVar(cfg.VPC.CIDR, "vpc-cidr", api.DefaultCIDR(), "global CIDR to use for VPC")
fs.BoolVar(&ng.AllowSSH, "ssh-access", false, "control SSH access for nodes")
fs.StringVar(&ng.SSHPublicKeyPath, "ssh-public-key", defaultSSHPublicKey, "SSH public key to use for nodes (import from local path, or use existing EC2 key pair)")

subnets = map[api.SubnetTopology]*[]string{
api.SubnetTopologyPrivate: fs.StringSlice("vpc-private-subnets", nil, "re-use private subnets of an existing VPC"),
api.SubnetTopologyPublic: fs.StringSlice("vpc-public-subnets", nil, "re-use public subnets of an existing VPC"),
}
fs.StringVar(&ng.AMI, "node-ami", ami.ResolverStatic, "Advanced use cases only. If 'static' is supplied (default) then eksctl will use static AMIs; if 'auto' is supplied then eksctl will automatically set the AMI based on region/instance type; if any other value is supplied it will override the AMI to use for the nodes. Use with extreme care.")
fs.StringVar(&ng.AMIFamily, "node-ami-family", ami.ImageFamilyAmazonLinux2, "Advanced use cases only. If 'AmazonLinux2' is supplied (default), then eksctl will use the offical AWS EKS AMIs (Amazon Linux 2); if 'Ubuntu1804' is supplied, then eksctl will use the offical Canonical EKS AMIs (Ubuntu 18.04).")

fs.BoolVarP(&ng.PrivateNetworking, "node-private-networking", "P", false, "whether to make initial nodegroup networking private")

groupFlagsInUsage(cmd)
fs.BoolVarP(&ng.PrivateNetworking, "node-private-networking", "P", false, "whether to make initial nodegroup networking private")
})

return cmd
}
group.InFlagSet("Cluster add-ons", func(fs *pflag.FlagSet) {
fs.BoolVar(&cfg.Addons.WithIAM.PolicyAmazonEC2ContainerRegistryPowerUser, "full-ecr-access", false, "enable full access to ECR")
fs.BoolVar(&cfg.Addons.WithIAM.PolicyAutoScaling, "asg-access", false, "enable iam policy dependency for cluster-autoscaler")
fs.BoolVar(&cfg.Addons.Storage, "storage-class", true, "if true (default) then a default StorageClass of type gp2 provisioned by EBS will be created")
})

func groupFlagsInUsage(cmd *cobra.Command) {
// Group flags by their categories determined by name prefixes
groupToPatterns := map[string][]string{
"Node": {"node", "storage-class", "ssh", "max-pods-per-node", "full-ecr-access", "asg-access"},
"Networking": {"vpc", "zones",},
"Stack": {"region", "tags"},
"Other": {},
}
groups := []string{}
for k := range groupToPatterns {
groups = append(groups, k)
}
groupToFlagSet := make(map[string]*pflag.FlagSet)
for _, g := range groups {
groupToFlagSet[g] = pflag.NewFlagSet(g, /* Unused. Can be anythng. */ pflag.ContinueOnError)
}
cmd.LocalFlags().VisitAll(func(f *pflag.Flag) {
for _, g := range groups {
for _, p := range groupToPatterns[g] {
if strings.HasPrefix(f.Name, p) {
groupToFlagSet[g].AddFlag(f)
return
}
}
group.InFlagSet("VPC networking", func(fs *pflag.FlagSet) {
fs.StringVar(&kopsClusterNameForVPC, "vpc-from-kops-cluster", "", "re-use VPC from a given kops cluster")
fs.IPNetVar(cfg.VPC.CIDR, "vpc-cidr", api.DefaultCIDR(), "global CIDR to use for VPC")
subnets = map[api.SubnetTopology]*[]string{
api.SubnetTopologyPrivate: fs.StringSlice("vpc-private-subnets", nil, "re-use private subnets of an existing VPC"),
api.SubnetTopologyPublic: fs.StringSlice("vpc-public-subnets", nil, "re-use public subnets of an existing VPC"),
}
groupToFlagSet["Other"].AddFlag(f)
})

// The usage template is based on the one bundled into cobra
// https://github.com/spf13/cobra/blob/1e58aa3361fd650121dceeedc399e7189c05674a/command.go#L397
origFlagUsages := `
cmdutils.AddCommonFlagsForAWS(group, p)

Flags:
{{.LocalFlags.FlagUsages | trimTrailingWhitespaces}}`
group.InFlagSet("Output kubeconfig", func(fs *pflag.FlagSet) {
fs.BoolVar(&writeKubeconfig, "write-kubeconfig", true, "toggle writing of kubeconfig")
cmdutils.AddCommonFlagsForKubeconfig(fs, &kubeconfigPath, &setContext, &autoKubeconfigPath, exampleClusterName)
})

altFlagUsages := ``
for _, g := range groups {
set := groupToFlagSet[g]
altFlagUsages += fmt.Sprintf(`
group.AddTo(cmd)

%s Flags:
%s`, g, strings.TrimRightFunc(set.FlagUsages(), unicode.IsSpace))
}

cmd.SetUsageTemplate(strings.Replace(cmd.UsageTemplate(), origFlagUsages, altFlagUsages, 1))
return cmd
}

func doCreateCluster(p *api.ProviderConfig, cfg *api.ClusterConfig, ng *api.NodeGroup, nameArg string) error {
Expand Down
5 changes: 3 additions & 2 deletions pkg/ctl/create/create.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ package create
import (
"github.com/kris-nova/logger"
"github.com/spf13/cobra"
"github.com/weaveworks/eksctl/pkg/ctl/cmdutils"
)

// Command will create the `create` commands
func Command() *cobra.Command {
func Command(g *cmdutils.Grouping) *cobra.Command {
cmd := &cobra.Command{
Use: "create",
Short: "Create resource(s)",
Expand All @@ -17,7 +18,7 @@ func Command() *cobra.Command {
},
}

cmd.AddCommand(createClusterCmd())
cmd.AddCommand(createClusterCmd(g))

return cmd
}
Loading

0 comments on commit 17b0702

Please sign in to comment.