From 4a2d040e273a5700c6b0ceecc6a8ef702e08b42d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Fri, 1 Oct 2021 15:03:06 +0200 Subject: [PATCH] Refactor command initialization, better completion, Apple Silicon support (#116) --- README.md | 11 + cmd/completion.go | 189 ---------- cmd/completion/completion.go | 222 +++++++++++ cmd/context.go | 109 +----- cmd/filesystemlayout.go | 133 +++---- cmd/firewall.go | 105 ++---- cmd/firmware.go | 234 +++--------- cmd/health.go | 28 +- cmd/helper.go | 173 +++------ cmd/image.go | 102 +++--- cmd/login.go | 91 ++--- cmd/machine.go | 608 ++++++++++++------------------- cmd/must.go | 9 + cmd/network.go | 233 ++++++------ cmd/{ => output}/detailer.go | 12 +- cmd/{ => output}/printer.go | 154 +++++++- cmd/{ => output}/printer_test.go | 2 +- cmd/partition.go | 128 +++---- cmd/project.go | 104 +++--- cmd/root.go | 214 ++++------- cmd/size.go | 113 +++--- cmd/switch.go | 73 ++-- cmd/update.go | 16 +- cmd/version.go | 54 ++- cmd/whoami.go | 75 ++-- docker-make.yml | 2 + pkg/api/auth.go | 36 ++ pkg/api/context.go | 70 ++++ {cmd => pkg/api}/issue.go | 4 +- pkg/api/version.go | 10 + 30 files changed, 1506 insertions(+), 1808 deletions(-) delete mode 100644 cmd/completion.go create mode 100644 cmd/completion/completion.go create mode 100644 cmd/must.go rename cmd/{ => output}/detailer.go (95%) rename cmd/{ => output}/printer.go (91%) rename cmd/{ => output}/printer_test.go (99%) create mode 100644 pkg/api/auth.go create mode 100644 pkg/api/context.go rename {cmd => pkg/api}/issue.go (99%) create mode 100644 pkg/api/version.go diff --git a/README.md b/README.md index 4fb79187..5833bb8d 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ Download locations: - [metalctl-linux-amd64](https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-linux-amd64) - [metalctl-darwin-amd64](https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-darwin-amd64) +- [metalctl-darwin-arm64](https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-darwin-arm64) - [metalctl-windows-amd64](https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-windows-amd64) ### Installation on Linux @@ -20,12 +21,22 @@ sudo mv metalctl-linux-amd64 /usr/local/bin/metalctl ### Installation on MacOS +For x86 based Macs: + ```bash curl -LO https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-darwin-amd64 chmod +x metalctl-darwin-amd64 sudo mv metalctl-darwin-amd64 /usr/local/bin/metalctl ``` +For Apple Silicon (M1) based Macs: + +```bash +curl -LO https://github.com/metal-stack/metalctl/releases/download/v0.8.3/metalctl-darwin-arm64 +chmod +x metalctl-darwin-arm64 +sudo mv metalctl-darwin-arm64 /usr/local/bin/metalctl +``` + ### Installation on Windows ```bash diff --git a/cmd/completion.go b/cmd/completion.go deleted file mode 100644 index c0bc79c0..00000000 --- a/cmd/completion.go +++ /dev/null @@ -1,189 +0,0 @@ -package cmd - -import ( - metalgo "github.com/metal-stack/metal-go" - "github.com/spf13/cobra" -) - -func imageListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.ImageList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, i := range resp.Image { - names = append(names, *i.ID) - } - return names, cobra.ShellCompDirectiveNoFileComp -} - -func partitionListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.PartitionList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, p := range resp.Partition { - names = append(names, *p.ID) - } - return names, cobra.ShellCompDirectiveNoFileComp -} - -func sizeListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.SizeList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, s := range resp.Size { - names = append(names, *s.ID) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func filesystemLayoutListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.FilesystemLayoutList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, s := range resp { - names = append(names, *s.ID+"\t"+s.Description) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func machineListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.MachineList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, m := range resp.Machines { - name := *m.ID - if m.Allocation != nil && *m.Allocation.Hostname != "" { - name = name + "\t" + *m.Allocation.Hostname - } - names = append(names, name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func firewallListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.FirewallList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, m := range resp.Firewalls { - name := *m.ID - if m.Allocation != nil && *m.Allocation.Hostname != "" { - name = name + "\t" + *m.Allocation.Hostname - } - names = append(names, name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func networkListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.NetworkList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, n := range resp.Networks { - names = append(names, *n.ID+"\t"+n.Name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} - -func ipListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.IPList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, i := range resp.IPs { - names = append(names, *i.Ipaddress+"\t"+i.Name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func projectListCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.ProjectList() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for _, p := range resp.Project { - names = append(names, p.Meta.ID+"\t"+p.TenantID+"/"+p.Name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func contextListCompletion() ([]string, cobra.ShellCompDirective) { - ctxs, err := getContexts() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - var names []string - for name := range ctxs.Contexts { - names = append(names, name) - } - return names, cobra.ShellCompDirectiveNoFileComp -} -func outputFormatListCompletion() ([]string, cobra.ShellCompDirective) { - return []string{"table", "wide", "markdown", "json", "yaml", "template"}, cobra.ShellCompDirectiveNoFileComp -} -func outputOrderListCompletion() ([]string, cobra.ShellCompDirective) { - return []string{"size", "id", "status", "event", "when", "partition", "project"}, cobra.ShellCompDirectiveNoFileComp -} - -var machineListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return machineListCompletion(driver) -} -var firewallListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return firewallListCompletion(driver) -} -var filesystemLayoutListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return filesystemLayoutListCompletion(driver) -} -var imageListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return imageListCompletion(driver) -} -var networkListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return networkListCompletion(driver) -} -var ipListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return ipListCompletion(driver) -} -var partitionListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return partitionListCompletion(driver) -} -var projectListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return projectListCompletion(driver) -} -var sizeListCompletionFunc = func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return sizeListCompletion(driver) -} diff --git a/cmd/completion/completion.go b/cmd/completion/completion.go new file mode 100644 index 00000000..790bee52 --- /dev/null +++ b/cmd/completion/completion.go @@ -0,0 +1,222 @@ +package completion + +import ( + metalgo "github.com/metal-stack/metal-go" + "github.com/metal-stack/metalctl/pkg/api" + "github.com/spf13/cobra" +) + +type Completion struct { + driver *metalgo.Driver +} + +func NewCompletion(driver *metalgo.Driver) *Completion { + return &Completion{ + driver: driver, + } +} + +func (c *Completion) ImageListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.ImageList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, i := range resp.Image { + names = append(names, *i.ID) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) PartitionListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.PartitionList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, p := range resp.Partition { + names = append(names, *p.ID) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) SizeListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.SizeList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, s := range resp.Size { + names = append(names, *s.ID) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) FilesystemLayoutListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.FilesystemLayoutList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, s := range resp { + names = append(names, *s.ID+"\t"+s.Description) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) MachineListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.MachineList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, m := range resp.Machines { + name := *m.ID + if m.Allocation != nil && *m.Allocation.Hostname != "" { + name = name + "\t" + *m.Allocation.Hostname + } + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) FirewallListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.FirewallList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, m := range resp.Firewalls { + name := *m.ID + if m.Allocation != nil && *m.Allocation.Hostname != "" { + name = name + "\t" + *m.Allocation.Hostname + } + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) NetworkListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.NetworkList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, n := range resp.Networks { + names = append(names, *n.ID+"\t"+n.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) IpListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.IPList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, i := range resp.IPs { + names = append(names, *i.Ipaddress+"\t"+i.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) ProjectListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.ProjectList() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, p := range resp.Project { + names = append(names, p.Meta.ID+"\t"+p.TenantID+"/"+p.Name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) ContextListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + ctxs, err := api.GetContexts() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for name := range ctxs.Contexts { + names = append(names, name) + } + return names, cobra.ShellCompDirectiveNoFileComp +} +func OutputFormatListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"table", "wide", "markdown", "json", "yaml", "template"}, cobra.ShellCompDirectiveNoFileComp +} +func OutputOrderListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{"size", "id", "status", "event", "when", "partition", "project"}, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) FirmwareKindCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return []string{string(metalgo.Bmc), string(metalgo.Bios)}, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) FirmwareVendorCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.ListFirmwares("", "", "") + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var vendors []string + for _, vv := range resp.Firmwares.Revisions { + for v := range vv.VendorRevisions { + vendors = append(vendors, v) + } + } + return vendors, cobra.ShellCompDirectiveNoFileComp +} + +func (c *Completion) FirmwareBoardCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + resp, err := c.driver.ListFirmwares("", "", "") + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var boards []string + for _, vv := range resp.Firmwares.Revisions { + for _, bb := range vv.VendorRevisions { + for b := range bb.BoardRevisions { + boards = append(boards, b) + } + } + } + return boards, cobra.ShellCompDirectiveNoFileComp +} +func (c *Completion) FirmwareRevisionCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + return c.firmwareRevisions("", "") +} +func (c *Completion) FirmwareBiosRevisionCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 1 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return c.firmwareRevisions(args[0], metalgo.Bios) +} +func (c *Completion) FirmwareBmcRevisionCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) != 1 { + return nil, cobra.ShellCompDirectiveNoFileComp + } + return c.firmwareRevisions(args[0], metalgo.Bmc) +} + +func (c *Completion) firmwareRevisions(machineID string, kind metalgo.FirmwareKind) ([]string, cobra.ShellCompDirective) { + vendor := "" + board := "" + if machineID != "" { + m, err := c.driver.MachineIPMIGet(machineID) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + board = m.Machine.Ipmi.Fru.BoardPartNumber + vendor = m.Machine.Ipmi.Fru.BoardMfg + } + resp, err := c.driver.ListFirmwares(kind, vendor, board) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var revisions []string + for _, vv := range resp.Firmwares.Revisions { + for _, bb := range vv.VendorRevisions { + for _, rr := range bb.BoardRevisions { + revisions = append(revisions, rr...) + } + } + } + return revisions, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/context.go b/cmd/context.go index 68f08ddb..0d6e289c 100644 --- a/cmd/context.go +++ b/cmd/context.go @@ -2,45 +2,20 @@ package cmd import ( "fmt" - "os" "github.com/fatih/color" + "github.com/metal-stack/metalctl/cmd/output" + "github.com/metal-stack/metalctl/pkg/api" "github.com/spf13/cobra" - "github.com/spf13/viper" - "gopkg.in/yaml.v3" ) -// Contexts contains all configuration contexts of metalctl -type Contexts struct { - CurrentContext string `yaml:"current"` - PreviousContext string `yaml:"previous"` - Contexts map[string]Context -} - -// Context configure metalctl behaviour -type Context struct { - ApiURL string `yaml:"url"` - IssuerURL string `yaml:"issuer_url"` - IssuerType string `yaml:"issuer_type"` - CustomScopes string `yaml:"custom_scopes"` - ClientID string `yaml:"client_id"` - ClientSecret string `yaml:"client_secret"` - HMAC *string `yaml:"hmac"` -} - -var ( - contextCmd = &cobra.Command{ - Use: "context ", - Aliases: []string{"ctx"}, - Short: "manage metalctl context", - Long: "context defines the backend to which metalctl talks to. You can switch back and forth with \"-\"", - ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - if len(args) != 0 { - return nil, cobra.ShellCompDirectiveNoFileComp - } - return contextListCompletion() - }, - +func newContextCmd(c *config) *cobra.Command { + contextCmd := &cobra.Command{ + Use: "context ", + Aliases: []string{"ctx"}, + Short: "manage metalctl context", + Long: "context defines the backend to which metalctl talks to. You can switch back and forth with \"-\"", + ValidArgsFunction: c.comp.ContextListCompletion, Example: ` ~/.metalctl/config.yaml --- @@ -64,14 +39,14 @@ contexts: return contextSet(args) } if len(args) == 0 { - return contextList() + return c.contextList() } return nil }, PreRun: bindPFlags, } - contextShortCmd = &cobra.Command{ + contextShortCmd := &cobra.Command{ Use: "short", Short: "only show the default context name", RunE: func(cmd *cobra.Command, args []string) error { @@ -79,19 +54,12 @@ contexts: }, PreRun: bindPFlags, } - - defaultCtx = Context{ - ApiURL: "http://localhost:8080/metal", - IssuerURL: "http://localhost:8080/", - } -) - -func init() { contextCmd.AddCommand(contextShortCmd) + return contextCmd } func contextShort() error { - ctxs, err := getContexts() + ctxs, err := api.GetContexts() if err != nil { return err } @@ -106,7 +74,7 @@ func contextSet(args []string) error { if args[0] == "-" { return previous() } - ctxs, err := getContexts() + ctxs, err := api.GetContexts() if err != nil { return err } @@ -121,11 +89,11 @@ func contextSet(args []string) error { } ctxs.PreviousContext = ctxs.CurrentContext ctxs.CurrentContext = nextCtx - return writeContexts(ctxs) + return api.WriteContexts(ctxs) } func previous() error { - ctxs, err := getContexts() + ctxs, err := api.GetContexts() if err != nil { return err } @@ -136,50 +104,13 @@ func previous() error { curr := ctxs.CurrentContext ctxs.PreviousContext = curr ctxs.CurrentContext = prev - return writeContexts(ctxs) -} - -func contextList() error { - ctxs, err := getContexts() - if err != nil { - return err - } - return printer.Print(ctxs) -} - -func mustDefaultContext() Context { - ctxs, err := getContexts() - if err != nil { - return defaultCtx - } - ctx, ok := ctxs.Contexts[ctxs.CurrentContext] - if !ok { - return defaultCtx - } - return ctx + return api.WriteContexts(ctxs) } -func getContexts() (*Contexts, error) { - var ctxs Contexts - cfgFile := viper.GetViper().ConfigFileUsed() - c, err := os.ReadFile(cfgFile) - if err != nil { - return nil, fmt.Errorf("unable to read config, please create a config.yaml in either: /etc/metalctl/, $HOME/.metalctl/ or in the current directory, see metalctl ctx -h for examples") - } - err = yaml.Unmarshal(c, &ctxs) - return &ctxs, err -} - -func writeContexts(ctxs *Contexts) error { - c, err := yaml.Marshal(ctxs) +func (c *config) contextList() error { + ctxs, err := api.GetContexts() if err != nil { return err } - cfgFile := viper.GetViper().ConfigFileUsed() - err = os.WriteFile(cfgFile, c, 0600) - if err != nil { - return err - } - fmt.Printf("%s switched context to \"%s\"\n", color.GreenString("✔"), color.GreenString(ctxs.CurrentContext)) - return nil + return output.New().Print(ctxs) } diff --git a/cmd/filesystemlayout.go b/cmd/filesystemlayout.go index 2947442d..6b3cc6d0 100644 --- a/cmd/filesystemlayout.go +++ b/cmd/filesystemlayout.go @@ -3,78 +3,74 @@ package cmd import ( "errors" "fmt" - "log" "net/http" - metalgo "github.com/metal-stack/metal-go" fsmodel "github.com/metal-stack/metal-go/api/client/filesystemlayout" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var ( - filesystemLayoutCmd = &cobra.Command{ +func newFilesystemLayoutCmd(c *config) *cobra.Command { + filesystemLayoutCmd := &cobra.Command{ Use: "filesystemlayout", Aliases: []string{"fsl"}, Short: "manage filesystemlayouts", Long: "a filesystemlayout is a specification how the disks in a machine are partitioned, formatted and mounted.", } - filesystemListCmd = &cobra.Command{ + filesystemListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all filesystems", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemList(driver) + return c.filesystemList() }, PreRun: bindPFlags, } - filesystemDescribeCmd = &cobra.Command{ + filesystemDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a filesystem", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemDescribe(driver, args) + return c.filesystemDescribe(args) }, - ValidArgsFunction: filesystemLayoutListCompletionFunc, + ValidArgsFunction: c.comp.FilesystemLayoutListCompletion, } - filesystemApplyCmd = &cobra.Command{ + filesystemApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a filesystem", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemApply(driver) + return c.filesystemApply() }, PreRun: bindPFlags, } - filesystemDeleteCmd = &cobra.Command{ + filesystemDeleteCmd := &cobra.Command{ Use: "delete ", Short: "delete a filesystem", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemDelete(driver, args) + return c.filesystemDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: filesystemLayoutListCompletionFunc, + ValidArgsFunction: c.comp.FilesystemLayoutListCompletion, } - filesystemTryCmd = &cobra.Command{ + filesystemTryCmd := &cobra.Command{ Use: "try", Short: "try to detect a filesystem by given size and image", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemTry(driver) + return c.filesystemTry() }, PreRun: bindPFlags, } - filesystemMatchCmd = &cobra.Command{ + filesystemMatchCmd := &cobra.Command{ Use: "match", Short: "check if a machine satisfies all disk requirements of a given filesystemlayout", RunE: func(cmd *cobra.Command, args []string) error { - return filesystemMatch(driver) + return c.filesystemMatch() }, PreRun: bindPFlags, } -) - -func init() { filesystemApplyCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin. Example: @@ -84,56 +80,21 @@ Example: # cat default.yaml | metalctl filesystem apply -f - ## or via file # metalctl filesystem apply -f default.yaml`) - err := filesystemApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(filesystemApplyCmd.MarkFlagRequired("file")) filesystemTryCmd.Flags().StringP("size", "", "", "size to try") filesystemTryCmd.Flags().StringP("image", "", "", "image to try") - err = filesystemTryCmd.MarkFlagRequired("size") - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemTryCmd.MarkFlagRequired("image") - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemTryCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemTryCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return imageListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(filesystemTryCmd.MarkFlagRequired("size")) + must(filesystemTryCmd.MarkFlagRequired("image")) + must(filesystemTryCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(filesystemTryCmd.RegisterFlagCompletionFunc("image", c.comp.ImageListCompletion)) filesystemMatchCmd.Flags().StringP("machine", "", "", "machine id to check for match [required]") filesystemMatchCmd.Flags().StringP("filesystemlayout", "", "", "filesystemlayout id to check against [required]") - err = filesystemMatchCmd.MarkFlagRequired("machine") - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemMatchCmd.MarkFlagRequired("filesystemlayout") - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemMatchCmd.RegisterFlagCompletionFunc("machine", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return machineListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = filesystemMatchCmd.RegisterFlagCompletionFunc("filesystemlayout", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return filesystemLayoutListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(filesystemMatchCmd.MarkFlagRequired("machine")) + must(filesystemMatchCmd.MarkFlagRequired("filesystemlayout")) + must(filesystemMatchCmd.RegisterFlagCompletionFunc("machine", c.comp.MachineListCompletion)) + must(filesystemMatchCmd.RegisterFlagCompletionFunc("filesystemlayout", c.comp.FilesystemLayoutListCompletion)) filesystemLayoutCmd.AddCommand(filesystemListCmd) filesystemLayoutCmd.AddCommand(filesystemDescribeCmd) @@ -141,30 +102,32 @@ Example: filesystemLayoutCmd.AddCommand(filesystemApplyCmd) filesystemLayoutCmd.AddCommand(filesystemTryCmd) filesystemLayoutCmd.AddCommand(filesystemMatchCmd) + + return filesystemLayoutCmd } -func filesystemList(driver *metalgo.Driver) error { - resp, err := driver.FilesystemLayoutList() +func (c *config) filesystemList() error { + resp, err := c.driver.FilesystemLayoutList() if err != nil { return err } - return printer.Print(resp) + return output.New().Print(resp) } -func filesystemDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) filesystemDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no filesystem ID given") } filesystemID := args[0] - resp, err := driver.FilesystemLayoutGet(filesystemID) + resp, err := c.driver.FilesystemLayoutGet(filesystemID) if err != nil { return err } - return detailer.Detail(resp) + return output.NewDetailer().Detail(resp) } // TODO: General apply method would be useful as these are quite a lot of lines and it's getting erroneous -func filesystemApply(driver *metalgo.Driver) error { +func (c *config) filesystemApply() error { var iars []models.V1FilesystemLayoutCreateRequest var iar models.V1FilesystemLayoutCreateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -179,7 +142,7 @@ func filesystemApply(driver *metalgo.Driver) error { } var response []*models.V1FilesystemLayoutResponse for _, iar := range iars { - p, err := driver.FilesystemLayoutGet(*iar.ID) + p, err := c.driver.FilesystemLayoutGet(*iar.ID) if err != nil { var r *fsmodel.GetFilesystemLayoutDefault if !errors.As(err, &r) { @@ -190,7 +153,7 @@ func filesystemApply(driver *metalgo.Driver) error { } } if p == nil { - resp, err := driver.FilesystemLayoutCreate(iar) + resp, err := c.driver.FilesystemLayoutCreate(iar) if err != nil { return err } @@ -198,28 +161,28 @@ func filesystemApply(driver *metalgo.Driver) error { continue } - resp, err := driver.FilesystemLayoutUpdate(models.V1FilesystemLayoutUpdateRequest(iar)) + resp, err := c.driver.FilesystemLayoutUpdate(models.V1FilesystemLayoutUpdateRequest(iar)) if err != nil { return err } response = append(response, resp) } - return printer.Print(response) + return output.New().Print(response) } -func filesystemDelete(driver *metalgo.Driver, args []string) error { +func (c *config) filesystemDelete(args []string) error { if len(args) < 1 { return fmt.Errorf("no filesystem ID given") } filesystemID := args[0] - resp, err := driver.FilesystemLayoutDelete(filesystemID) + resp, err := c.driver.FilesystemLayoutDelete(filesystemID) if err != nil { return err } - return detailer.Detail(resp) + return output.NewDetailer().Detail(resp) } -func filesystemTry(driver *metalgo.Driver) error { +func (c *config) filesystemTry() error { size := viper.GetString("size") image := viper.GetString("image") try := models.V1FilesystemLayoutTryRequest{ @@ -227,13 +190,13 @@ func filesystemTry(driver *metalgo.Driver) error { Image: &image, } - resp, err := driver.FilesystemLayoutTry(try) + resp, err := c.driver.FilesystemLayoutTry(try) if err != nil { return err } - return printer.Print(resp) + return output.New().Print(resp) } -func filesystemMatch(driver *metalgo.Driver) error { +func (c *config) filesystemMatch() error { machine := viper.GetString("machine") fsl := viper.GetString("filesystemlayout") match := models.V1FilesystemLayoutMatchRequest{ @@ -241,9 +204,9 @@ func filesystemMatch(driver *metalgo.Driver) error { Filesystemlayout: &fsl, } - resp, err := driver.FilesystemLayoutMatch(match) + resp, err := c.driver.FilesystemLayoutMatch(match) if err != nil { return err } - return printer.Print(resp) + return output.New().Print(resp) } diff --git a/cmd/firewall.go b/cmd/firewall.go index 9597664a..e9c4da73 100644 --- a/cmd/firewall.go +++ b/cmd/firewall.go @@ -2,80 +2,78 @@ package cmd import ( "fmt" - "log" metalgo "github.com/metal-stack/metal-go" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" ) -var ( - firewallCmd = &cobra.Command{ +func newFirewallCmd(c *config) *cobra.Command { + firewallCmd := &cobra.Command{ Use: "firewall", Short: "manage firewalls", Long: "metal firewalls are bare metal firewalls.", } - firewallCreateCmd = &cobra.Command{ + firewallCreateCmd := &cobra.Command{ Use: "create", Short: "create a firewall", Long: `create a new firewall connected to the given networks.`, RunE: func(cmd *cobra.Command, args []string) error { - return firewallCreate(driver) + return c.firewallCreate() }, PreRun: bindPFlags, } - firewallListCmd = &cobra.Command{ + firewallListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all firewalls", Long: "list all firewalls with almost all properties in tabular form.", RunE: func(cmd *cobra.Command, args []string) error { - return firewallList(driver) + return c.firewallList() }, PreRun: bindPFlags, } - firewallDescribeCmd = &cobra.Command{ + firewallDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a firewall", Long: "describe a firewall in a very detailed form with all properties.", RunE: func(cmd *cobra.Command, args []string) error { - return firewallDescribe(driver, args) + return c.firewallDescribe(args) }, PreRun: bindPFlags, - ValidArgsFunction: firewallListCompletionFunc, + ValidArgsFunction: c.comp.FirewallListCompletion, } - firewallDestroyCmd = &cobra.Command{ + firewallDestroyCmd := &cobra.Command{ Use: "destroy ", Aliases: []string{"delete", "rm"}, Short: "destroy a firewall", Long: `destroy a firewall and destroy all data stored on the local disks. Once destroyed it is back for usage by other projects. A destroyed firewall can not restored anymore`, RunE: func(cmd *cobra.Command, args []string) error { - return firewallDestroy(driver, args) + return c.firewallDestroy(args) }, PreRun: bindPFlags, - ValidArgsFunction: firewallListCompletionFunc, + ValidArgsFunction: c.comp.FirewallListCompletion, } - firewallReserveCmd = &cobra.Command{ + firewallReserveCmd := &cobra.Command{ Use: "reserve ", Short: "reserve a firewall", Long: `reserve a firewall for exclusive usage, this firewall will no longer be picked by other allocations. This is useful for maintenance of the firewall or testing. After the reservation is not needed anymore, the reservation should be removed with --remove.`, RunE: func(cmd *cobra.Command, args []string) error { - return machineReserve(driver, args) + return c.machineReserve(args) }, PreRun: bindPFlags, - ValidArgsFunction: firewallListCompletionFunc, + ValidArgsFunction: c.comp.FirewallListCompletion, } -) -func init() { - addMachineCreateFlags(firewallCreateCmd, "firewall") + c.addMachineCreateFlags(firewallCreateCmd, "firewall") firewallListCmd.Flags().StringVarP(&filterOpts.ID, "id", "", "", "ID to filter [optional]") firewallListCmd.Flags().StringVarP(&filterOpts.Partition, "partition", "", "", "partition to filter [optional]") @@ -86,46 +84,23 @@ func init() { firewallListCmd.Flags().StringVarP(&filterOpts.Hostname, "hostname", "", "", "allocation hostname to filter [optional]") firewallListCmd.Flags().StringVarP(&filterOpts.Mac, "mac", "", "", "mac to filter [optional]") firewallListCmd.Flags().StringSliceVar(&filterOpts.Tags, "tags", []string{}, "tags to filter, use it like: --tags \"tag1,tag2\" or --tags \"tag3\".") - err := firewallListCmd.RegisterFlagCompletionFunc("partition", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firewallListCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firewallListCmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return projectListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firewallListCmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firewallListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firewallListCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return imageListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(firewallListCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion)) + must(firewallListCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(firewallListCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + must(firewallListCmd.RegisterFlagCompletionFunc("id", c.comp.FirewallListCompletion)) + must(firewallListCmd.RegisterFlagCompletionFunc("image", c.comp.ImageListCompletion)) firewallCmd.AddCommand(firewallCreateCmd) firewallCmd.AddCommand(firewallListCmd) firewallCmd.AddCommand(firewallDestroyCmd) firewallCmd.AddCommand(firewallDescribeCmd) firewallCmd.AddCommand(firewallReserveCmd) + + return firewallCmd } -func firewallCreate(driver *metalgo.Driver) error { - mcr, err := machineCreateRequest() +func (c *config) firewallCreate() error { + mcr, err := c.machineCreateRequest() if err != nil { return fmt.Errorf("firewall create error:%w", err) } @@ -133,14 +108,14 @@ func firewallCreate(driver *metalgo.Driver) error { fcr := &metalgo.FirewallCreateRequest{ MachineCreateRequest: *mcr, } - resp, err := driver.FirewallCreate(fcr) + resp, err := c.driver.FirewallCreate(fcr) if err != nil { return err } - return printer.Print(resp.Firewall) + return output.New().Print(resp.Firewall) } -func firewallList(driver *metalgo.Driver) error { +func (c *config) firewallList() error { var resp *metalgo.FirewallListResponse var err error if atLeastOneViperStringFlagGiven("id", "partition", "size", "name", "project", "image", "hostname") || @@ -176,37 +151,37 @@ func firewallList(driver *metalgo.Driver) error { if len(filterOpts.Tags) > 0 { ffr.Tags = filterOpts.Tags } - resp, err = driver.FirewallFind(ffr) + resp, err = c.driver.FirewallFind(ffr) } else { - resp, err = driver.FirewallList() + resp, err = c.driver.FirewallList() } if err != nil { return err } - return printer.Print(resp.Firewalls) + return output.New().Print(resp.Firewalls) } -func firewallDescribe(driver *metalgo.Driver, args []string) error { - firewallID, err := getMachineID(args) +func (c *config) firewallDescribe(args []string) error { + firewallID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.FirewallGet(firewallID) + resp, err := c.driver.FirewallGet(firewallID) if err != nil { return err } - return detailer.Detail(resp.Firewall) + return output.NewDetailer().Detail(resp.Firewall) } -func firewallDestroy(driver *metalgo.Driver, args []string) error { - firewallID, err := getMachineID(args) +func (c *config) firewallDestroy(args []string) error { + firewallID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineDelete(firewallID) + resp, err := c.driver.MachineDelete(firewallID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } diff --git a/cmd/firmware.go b/cmd/firmware.go index 6eab6f8a..30878b44 100644 --- a/cmd/firmware.go +++ b/cmd/firmware.go @@ -2,9 +2,9 @@ package cmd import ( "fmt" - "log" metalgo "github.com/metal-stack/metal-go" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -16,142 +16,87 @@ const ( remove ) -var ( - firmwareCmd = &cobra.Command{ +func newFirmwareCmd(c *config) *cobra.Command { + firmwareCmd := &cobra.Command{ Use: "firmware", Short: "manage firmwares", Long: "list, upload and remove firmwares.", } - firmwareListCmd = &cobra.Command{ + firmwareListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list firmwares", Long: "lists all available firmwares matching the given criteria.", RunE: func(cmd *cobra.Command, args []string) error { - return firmwareList(driver) + return c.firmwareList() }, PreRun: bindPFlags, } - firmwareUploadCmd = &cobra.Command{ + firmwareUploadCmd := &cobra.Command{ Use: "upload", Short: "upload a firmware", } - firmwareUploadBiosCmd = &cobra.Command{ + firmwareUploadBiosCmd := &cobra.Command{ Use: "bios ", Short: "upload a BIOS firmware", Long: "the given BIOS firmware file will be uploaded and tagged as given revision.", RunE: func(cmd *cobra.Command, args []string) error { - return firmwareUploadBios(driver, args) + return c.firmwareUploadBios(args) }, PreRun: bindPFlags, } - firmwareUploadBmcCmd = &cobra.Command{ + firmwareUploadBmcCmd := &cobra.Command{ Use: "bmc ", Short: "upload a BMC firmware", Long: "the given BMC firmware file will be uploaded and tagged as given revision.", RunE: func(cmd *cobra.Command, args []string) error { - return firmwareUploadBmc(driver, args) + return c.firmwareUploadBmc(args) }, PreRun: bindPFlags, } - firmwareRemoveCmd = &cobra.Command{ + firmwareRemoveCmd := &cobra.Command{ Use: "remove", Aliases: []string{"rm", "delete", "del"}, Short: "remove a firmware", Long: "removes the specified firmware.", RunE: func(cmd *cobra.Command, args []string) error { - return firmwareRemove(driver, args) + return c.firmwareRemove(args) }, PreRun: bindPFlags, } -) -func init() { firmwareListCmd.Flags().StringP("kind", "", "", "the firmware kind [bmc|bios]") firmwareListCmd.Flags().StringP("vendor", "", "", "the vendor") firmwareListCmd.Flags().StringP("board", "", "", "the board type") firmwareListCmd.Flags().StringP("machineid", "", "", "the machine id (ignores vendor and board flags)") - err := firmwareListCmd.RegisterFlagCompletionFunc("kind", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareKindCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareListCmd.RegisterFlagCompletionFunc("vendor", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareVendorCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareListCmd.RegisterFlagCompletionFunc("board", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareBoardCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(firmwareListCmd.RegisterFlagCompletionFunc("kind", c.comp.FirmwareKindCompletion)) + must(firmwareListCmd.RegisterFlagCompletionFunc("vendor", c.comp.FirmwareVendorCompletion)) + must(firmwareListCmd.RegisterFlagCompletionFunc("board", c.comp.FirmwareBoardCompletion)) firmwareCmd.AddCommand(firmwareListCmd) firmwareUploadBiosCmd.Flags().StringP("vendor", "", "", "the vendor (required)") firmwareUploadBiosCmd.Flags().StringP("board", "", "", "the board type (required)") firmwareUploadBiosCmd.Flags().StringP("revision", "", "", "the BIOS firmware revision (required)") - err = firmwareUploadBiosCmd.MarkFlagRequired("vendor") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBiosCmd.MarkFlagRequired("board") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBiosCmd.MarkFlagRequired("revision") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBiosCmd.RegisterFlagCompletionFunc("vendor", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareVendorCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBiosCmd.RegisterFlagCompletionFunc("board", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareBoardCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(firmwareUploadBiosCmd.MarkFlagRequired("vendor")) + must(firmwareUploadBiosCmd.MarkFlagRequired("board")) + must(firmwareUploadBiosCmd.MarkFlagRequired("revision")) + must(firmwareUploadBiosCmd.RegisterFlagCompletionFunc("vendor", c.comp.FirmwareVendorCompletion)) + must(firmwareUploadBiosCmd.RegisterFlagCompletionFunc("board", c.comp.FirmwareBoardCompletion)) firmwareUploadCmd.AddCommand(firmwareUploadBiosCmd) firmwareUploadBmcCmd.Flags().StringP("vendor", "", "", "the vendor (required)") firmwareUploadBmcCmd.Flags().StringP("board", "", "", "the board type (required)") firmwareUploadBmcCmd.Flags().StringP("revision", "", "", "the BMC firmware revision (required)") - err = firmwareUploadBmcCmd.MarkFlagRequired("vendor") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBmcCmd.MarkFlagRequired("board") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBmcCmd.MarkFlagRequired("revision") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBmcCmd.RegisterFlagCompletionFunc("vendor", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareVendorCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareUploadBmcCmd.RegisterFlagCompletionFunc("board", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareBoardCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(firmwareUploadBmcCmd.MarkFlagRequired("vendor")) + must(firmwareUploadBmcCmd.MarkFlagRequired("board")) + must(firmwareUploadBmcCmd.MarkFlagRequired("revision")) + must(firmwareUploadBmcCmd.RegisterFlagCompletionFunc("vendor", c.comp.FirmwareVendorCompletion)) + must(firmwareUploadBmcCmd.RegisterFlagCompletionFunc("board", c.comp.FirmwareBoardCompletion)) firmwareUploadCmd.AddCommand(firmwareUploadBmcCmd) firmwareCmd.AddCommand(firmwareUploadCmd) @@ -160,50 +105,20 @@ func init() { firmwareRemoveCmd.Flags().StringP("vendor", "", "", "the vendor (required)") firmwareRemoveCmd.Flags().StringP("board", "", "", "the board type (required)") firmwareRemoveCmd.Flags().StringP("revision", "", "", "the firmware revision (required)") - err = firmwareRemoveCmd.MarkFlagRequired("kind") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.MarkFlagRequired("vendor") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.MarkFlagRequired("board") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.MarkFlagRequired("revision") - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.RegisterFlagCompletionFunc("kind", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareKindCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.RegisterFlagCompletionFunc("vendor", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareVendorCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.RegisterFlagCompletionFunc("board", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareBoardCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = firmwareRemoveCmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareRevisionCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(firmwareRemoveCmd.MarkFlagRequired("kind")) + must(firmwareRemoveCmd.MarkFlagRequired("vendor")) + must(firmwareRemoveCmd.MarkFlagRequired("board")) + must(firmwareRemoveCmd.MarkFlagRequired("revision")) + must(firmwareRemoveCmd.RegisterFlagCompletionFunc("kind", c.comp.FirmwareKindCompletion)) + must(firmwareRemoveCmd.RegisterFlagCompletionFunc("vendor", c.comp.FirmwareVendorCompletion)) + must(firmwareRemoveCmd.RegisterFlagCompletionFunc("board", c.comp.FirmwareBoardCompletion)) + must(firmwareRemoveCmd.RegisterFlagCompletionFunc("revision", c.comp.FirmwareRevisionCompletion)) firmwareCmd.AddCommand(firmwareRemoveCmd) + + return firmwareCmd } -func firmwareList(driver *metalgo.Driver) error { +func (c *config) firmwareList() error { var err error var resp *metalgo.FirmwaresResponse @@ -214,30 +129,30 @@ func firmwareList(driver *metalgo.Driver) error { case "": vendor := viper.GetString("vendor") board := viper.GetString("board") - resp, err = driver.ListFirmwares(kind, vendor, board) + resp, err = c.driver.ListFirmwares(kind, vendor, board) default: - resp, err = driver.MachineListFirmwares(kind, id) + resp, err = c.driver.MachineListFirmwares(kind, id) } if err != nil { return err } - return printer.Print(resp.Firmwares) + return output.New().Print(resp.Firmwares) } -func firmwareUploadBios(driver *metalgo.Driver, args []string) error { - return manageFirmware(upload, driver, metalgo.Bios, args) +func (c *config) firmwareUploadBios(args []string) error { + return c.manageFirmware(upload, metalgo.Bios, args) } -func firmwareUploadBmc(driver *metalgo.Driver, args []string) error { - return manageFirmware(upload, driver, metalgo.Bmc, args) +func (c *config) firmwareUploadBmc(args []string) error { + return c.manageFirmware(upload, metalgo.Bmc, args) } -func firmwareRemove(driver *metalgo.Driver, args []string) error { - return manageFirmware(remove, driver, metalgo.Bios, args) +func (c *config) firmwareRemove(args []string) error { + return c.manageFirmware(remove, metalgo.Bios, args) } -func manageFirmware(task firmwareTask, driver *metalgo.Driver, kind metalgo.FirmwareKind, args []string) error { +func (c *config) manageFirmware(task firmwareTask, kind metalgo.FirmwareKind, args []string) error { revision := viper.GetString("revision") vendor := viper.GetString("vendor") board := viper.GetString("board") @@ -248,62 +163,9 @@ func manageFirmware(task firmwareTask, driver *metalgo.Driver, kind metalgo.Firm if len(args) < 1 { return fmt.Errorf("no firmware file given") } - _, err = driver.UploadFirmware(kind, vendor, board, revision, args[0]) + _, err = c.driver.UploadFirmware(kind, vendor, board, revision, args[0]) case remove: - _, err = driver.RemoveFirmware(kind, vendor, board, revision) + _, err = c.driver.RemoveFirmware(kind, vendor, board, revision) } return err } - -func firmwareKindCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - return []string{string(metalgo.Bmc), string(metalgo.Bios)}, cobra.ShellCompDirectiveNoFileComp -} - -func firmwareVendorCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.ListFirmwares("", "", "") - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - var vendors []string - for _, vv := range resp.Firmwares.Revisions { - for v := range vv.VendorRevisions { - vendors = append(vendors, v) - } - } - return vendors, cobra.ShellCompDirectiveNoFileComp -} - -func firmwareBoardCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.ListFirmwares("", "", "") - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - var boards []string - for _, vv := range resp.Firmwares.Revisions { - for _, bb := range vv.VendorRevisions { - for b := range bb.BoardRevisions { - boards = append(boards, b) - } - } - } - return boards, cobra.ShellCompDirectiveNoFileComp -} - -func firmwareRevisionCompletion(driver *metalgo.Driver) ([]string, cobra.ShellCompDirective) { - resp, err := driver.ListFirmwares("", "", "") - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - - var revisions []string - for _, vv := range resp.Firmwares.Revisions { - for _, bb := range vv.VendorRevisions { - for _, rr := range bb.BoardRevisions { - revisions = append(revisions, rr...) - } - } - } - return revisions, cobra.ShellCompDirectiveNoFileComp -} diff --git a/cmd/health.go b/cmd/health.go index ebb5a8fb..a206b4d6 100644 --- a/cmd/health.go +++ b/cmd/health.go @@ -1,19 +1,25 @@ package cmd import ( + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" ) -var healthCmd = &cobra.Command{ - Use: "health", - Short: "shows the server health", - RunE: func(cmd *cobra.Command, args []string) error { - resp, err := driver.HealthGet() - if err != nil { - return err - } +func newHealthCmd(c *config) *cobra.Command { - return detailer.Detail(resp.Health) - }, - PreRun: bindPFlags, + healthCmd := &cobra.Command{ + Use: "health", + Short: "shows the server health", + RunE: func(cmd *cobra.Command, args []string) error { + resp, err := c.driver.HealthGet() + if err != nil { + return err + } + + return output.NewDetailer().Detail(resp.Health) + }, + PreRun: bindPFlags, + } + + return healthCmd } diff --git a/cmd/helper.go b/cmd/helper.go index 924f8fdc..4393b0f9 100644 --- a/cmd/helper.go +++ b/cmd/helper.go @@ -2,25 +2,20 @@ package cmd import ( "bufio" - "bytes" - "encoding/json" "errors" "fmt" "io" - "math" - "net" "os" - "sort" + "os/user" + "path/filepath" "strings" - "time" "github.com/metal-stack/metal-lib/auth" + "github.com/metal-stack/metalctl/pkg/api" metalgo "github.com/metal-stack/metal-go" "gopkg.in/yaml.v3" - "github.com/metal-stack/metal-go/api/models" - "github.com/spf13/viper" ) @@ -92,60 +87,6 @@ func viperInt64(flag string) *int64 { return &value } -func sortIPs(v1ips []*models.V1IPResponse) []*models.V1IPResponse { - - v1ipmap := make(map[string]*models.V1IPResponse) - var ips []string - for _, v1ip := range v1ips { - v1ipmap[*v1ip.Ipaddress] = v1ip - ips = append(ips, *v1ip.Ipaddress) - } - - realIPs := make([]net.IP, 0, len(ips)) - - for _, ip := range ips { - realIPs = append(realIPs, net.ParseIP(ip)) - } - - sort.Slice(realIPs, func(i, j int) bool { - return bytes.Compare(realIPs[i], realIPs[j]) < 0 - }) - - var result []*models.V1IPResponse - for _, ip := range realIPs { - result = append(result, v1ipmap[ip.String()]) - } - return result -} - -//nolint:unparam -func truncate(input string, maxlength int) string { - elipsis := "..." - il := len(input) - el := len(elipsis) - if il <= maxlength { - return input - } - if maxlength <= el { - return input[:maxlength] - } - startlength := ((maxlength - el) / 2) - el/2 - - output := input[:startlength] + elipsis - missing := maxlength - len(output) - output = output + input[il-missing:] - return output -} - -func truncateEnd(input string, maxlength int) string { - elipsis := "..." - length := len(input) + len(elipsis) - if length <= maxlength { - return input - } - return input[:maxlength] + elipsis -} - func parseNetworks(values []string) ([]metalgo.MachineAllocationNetwork, error) { nets := []metalgo.MachineAllocationNetwork{} for _, netWithFlag := range values { @@ -203,67 +144,6 @@ func splitNetwork(value string) (string, bool, error) { // return string(result) // } -func humanizeDuration(duration time.Duration) string { - days := int64(duration.Hours() / 24) - hours := int64(math.Mod(duration.Hours(), 24)) - minutes := int64(math.Mod(duration.Minutes(), 60)) - seconds := int64(math.Mod(duration.Seconds(), 60)) - - chunks := []struct { - singularName string - amount int64 - }{ - {"d", days}, - {"h", hours}, - {"m", minutes}, - {"s", seconds}, - } - - parts := []string{} - - for _, chunk := range chunks { - switch chunk.amount { - case 0: - continue - default: - parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.singularName)) - } - } - - if len(parts) == 0 { - return "0s" - } - if len(parts) > 2 { - parts = parts[:2] - } - return strings.Join(parts, " ") -} - -// strValue returns the value of a string pointer of not nil, otherwise empty string -func strValue(strPtr *string) string { - if strPtr != nil { - return *strPtr - } - return "" -} - -// genericObject transforms the input to a struct which has fields with the same name as in the json struct. -// this is handy for template rendering as the output of -o json|yaml can be used as the input for the template -func genericObject(input interface{}) map[string]interface{} { - b, err := json.Marshal(input) - if err != nil { - fmt.Printf("unable to marshall input:%v", err) - os.Exit(1) - } - var result interface{} - err = json.Unmarshal(b, &result) - if err != nil { - fmt.Printf("unable to unmarshal input:%v", err) - os.Exit(1) - } - return result.(map[string]interface{}) -} - func labelsFromTags(tags []string) map[string]string { labels := make(map[string]string) for _, tag := range tags { @@ -313,7 +193,7 @@ const cloudContext = "cloudctl" // getAuthContext reads AuthContext from given kubeconfig func getAuthContext(kubeconfig string) (*auth.AuthContext, error) { - cs, err := getContexts() + cs, err := api.GetContexts() if err != nil { return nil, err } @@ -364,3 +244,48 @@ func Prompt(msg, compare string) error { } return nil } +func searchSSHKey() (string, error) { + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) + } + homeDir := currentUser.HomeDir + defaultDir := filepath.Join(homeDir, "/.ssh/") + var key string + for _, k := range defaultSSHKeys { + possibleKey := filepath.Join(defaultDir, k) + _, err := os.ReadFile(possibleKey) + if err == nil { + fmt.Printf("using SSH identity: %s. Another identity can be specified with --sshidentity/-p\n", + possibleKey) + key = possibleKey + break + } + } + + if key == "" { + return "", fmt.Errorf("failure to locate a SSH identity in default location (%s). "+ + "Another identity can be specified with --sshidentity/-p\n", defaultDir) + } + return key, nil +} + +func readFromFile(filePath string) (string, error) { + currentUser, err := user.Current() + if err != nil { + return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) + } + homeDir := currentUser.HomeDir + + if filePath == "~" { + filePath = homeDir + } else if strings.HasPrefix(filePath, "~/") { + filePath = filepath.Join(homeDir, filePath[2:]) + } + + content, err := os.ReadFile(filePath) + if err != nil { + return "", fmt.Errorf("unable to read from given file %s error:%w", filePath, err) + } + return strings.TrimSpace(string(content)), nil +} diff --git a/cmd/image.go b/cmd/image.go index 86832b48..0175cedd 100644 --- a/cmd/image.go +++ b/cmd/image.go @@ -3,86 +3,84 @@ package cmd import ( "errors" "fmt" - "log" "net/http" metalgo "github.com/metal-stack/metal-go" imagemodel "github.com/metal-stack/metal-go/api/client/image" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - imageCmd = &cobra.Command{ +func newImageCmd(c *config) *cobra.Command { + imageCmd := &cobra.Command{ Use: "image", Short: "manage images", Long: "os images available to be installed on machines.", } - imageListCmd = &cobra.Command{ + imageListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all images", RunE: func(cmd *cobra.Command, args []string) error { - return imageList(driver) + return c.imageList() }, } - imageDescribeCmd = &cobra.Command{ + imageDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageDescribe(driver, args) + return c.imageDescribe(args) }, - ValidArgsFunction: imageListCompletionFunc, + ValidArgsFunction: c.comp.ImageListCompletion, } - imageCreateCmd = &cobra.Command{ + imageCreateCmd := &cobra.Command{ Use: "create", Short: "create a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageCreate(driver) + return c.imageCreate() }, PreRun: bindPFlags, } - imageUpdateCmd = &cobra.Command{ + imageUpdateCmd := &cobra.Command{ Use: "update", Short: "update a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageUpdate(driver) + return c.imageUpdate() }, PreRun: bindPFlags, } - imageApplyCmd = &cobra.Command{ + imageApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageApply(driver) + return c.imageApply() }, PreRun: bindPFlags, } - imageDeleteCmd = &cobra.Command{ + imageDeleteCmd := &cobra.Command{ Use: "delete ", Aliases: []string{"rm"}, Short: "delete a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageDelete(driver, args) + return c.imageDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: imageListCompletionFunc, + ValidArgsFunction: c.comp.ImageListCompletion, } - imageEditCmd = &cobra.Command{ + imageEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a image", RunE: func(cmd *cobra.Command, args []string) error { - return imageEdit(driver, args) + return c.imageEdit(args) }, PreRun: bindPFlags, - ValidArgsFunction: imageListCompletionFunc, + ValidArgsFunction: c.comp.ImageListCompletion, } -) -func init() { imageCreateCmd.Flags().StringP("id", "", "", "ID of the image. [required]") imageCreateCmd.Flags().StringP("url", "", "", "url of the image. [required]") imageCreateCmd.Flags().StringP("name", "n", "", "Name of the image. [optional]") @@ -98,10 +96,7 @@ Example: # cat ubuntu.yaml | metalctl image update -f - ## or via file # metalctl image update -f ubuntu.yaml`) - err := imageUpdateCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(imageUpdateCmd.MarkFlagRequired("file")) imageApplyCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin. Example: @@ -112,10 +107,7 @@ Example: # cat ubuntu.yaml | metalctl image apply -f - ## or via file # metalctl image apply -f ubuntu.yaml`) - err = imageApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(imageApplyCmd.MarkFlagRequired("file")) imageCmd.AddCommand(imageListCmd) imageCmd.AddCommand(imageDescribeCmd) @@ -124,29 +116,31 @@ Example: imageCmd.AddCommand(imageDeleteCmd) imageCmd.AddCommand(imageApplyCmd) imageCmd.AddCommand(imageEditCmd) + + return imageCmd } -func imageList(driver *metalgo.Driver) error { - resp, err := driver.ImageList() +func (c *config) imageList() error { + resp, err := c.driver.ImageList() if err != nil { return err } - return printer.Print(resp.Image) + return output.New().Print(resp.Image) } -func imageDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) imageDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no image ID given") } imageID := args[0] - resp, err := driver.ImageGet(imageID) + resp, err := c.driver.ImageGet(imageID) if err != nil { return err } - return detailer.Detail(resp.Image) + return output.NewDetailer().Detail(resp.Image) } -func imageCreate(driver *metalgo.Driver) error { +func (c *config) imageCreate() error { var icr metalgo.ImageCreateRequest if viper.GetString("file") != "" { var iars []metalgo.ImageCreateRequest @@ -170,22 +164,22 @@ func imageCreate(driver *metalgo.Driver) error { Features: viper.GetStringSlice("features"), } } - resp, err := driver.ImageCreate(icr) + resp, err := c.driver.ImageCreate(icr) if err != nil { return err } - return detailer.Detail(resp.Image) + return output.NewDetailer().Detail(resp.Image) } -func imageUpdate(driver *metalgo.Driver) error { +func (c *config) imageUpdate() error { iar, err := readImageCreateRequests(viper.GetString("file")) if err != nil { return err } - resp, err := driver.ImageUpdate(iar) + resp, err := c.driver.ImageUpdate(iar) if err != nil { return err } - return detailer.Detail(resp.Image) + return output.NewDetailer().Detail(resp.Image) } func readImageCreateRequests(filename string) (metalgo.ImageCreateRequest, error) { @@ -201,7 +195,7 @@ func readImageCreateRequests(filename string) (metalgo.ImageCreateRequest, error } // TODO: General apply method would be useful as these are quite a lot of lines and it's getting erroneous -func imageApply(driver *metalgo.Driver) error { +func (c *config) imageApply() error { var iars []metalgo.ImageCreateRequest var iar metalgo.ImageCreateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -216,7 +210,7 @@ func imageApply(driver *metalgo.Driver) error { } var response []*models.V1ImageResponse for _, iar := range iars { - image, err := driver.ImageGet(iar.ID) + image, err := c.driver.ImageGet(iar.ID) if err != nil { var r *imagemodel.FindImageDefault if !errors.As(err, &r) { @@ -227,7 +221,7 @@ func imageApply(driver *metalgo.Driver) error { } } if image.Image == nil { - resp, err := driver.ImageCreate(iar) + resp, err := c.driver.ImageCreate(iar) if err != nil { return err } @@ -235,7 +229,7 @@ func imageApply(driver *metalgo.Driver) error { continue } if image.Image.ID != nil { - resp, err := driver.ImageUpdate(iar) + resp, err := c.driver.ImageUpdate(iar) if err != nil { return err } @@ -243,29 +237,29 @@ func imageApply(driver *metalgo.Driver) error { continue } } - return detailer.Detail(response) + return output.NewDetailer().Detail(response) } -func imageDelete(driver *metalgo.Driver, args []string) error { +func (c *config) imageDelete(args []string) error { if len(args) < 1 { return fmt.Errorf("no image ID given") } imageID := args[0] - resp, err := driver.ImageDelete(imageID) + resp, err := c.driver.ImageDelete(imageID) if err != nil { return err } - return detailer.Detail(resp.Image) + return output.NewDetailer().Detail(resp.Image) } -func imageEdit(driver *metalgo.Driver, args []string) error { +func (c *config) imageEdit(args []string) error { if len(args) < 1 { return fmt.Errorf("no image ID given") } imageID := args[0] getFunc := func(id string) ([]byte, error) { - resp, err := driver.ImageGet(imageID) + resp, err := c.driver.ImageGet(imageID) if err != nil { return nil, err } @@ -281,11 +275,11 @@ func imageEdit(driver *metalgo.Driver, args []string) error { return err } fmt.Printf("new image classification:%s\n", *iar.Classification) - uresp, err := driver.ImageUpdate(iar) + uresp, err := c.driver.ImageUpdate(iar) if err != nil { return err } - return detailer.Detail(uresp.Image) + return output.NewDetailer().Detail(uresp.Image) } return edit(imageID, getFunc, updateFunc) diff --git a/cmd/login.go b/cmd/login.go index 9b6a8c96..ce8981f5 100644 --- a/cmd/login.go +++ b/cmd/login.go @@ -7,65 +7,66 @@ import ( "strings" "github.com/metal-stack/metal-lib/auth" + "github.com/metal-stack/metalctl/pkg/api" "github.com/spf13/cobra" "github.com/spf13/viper" ) -var loginCmd = &cobra.Command{ - Use: "login", - Short: "login user and receive token", - Long: "login and receive token that will be used to authenticate commands.", - RunE: func(cmd *cobra.Command, args []string) error { +func newLoginCmd() *cobra.Command { - var console io.Writer - var handler auth.TokenHandlerFunc - if viper.GetBool("printOnly") { - // do not print to console - handler = printTokenHandler - } else { - cs, err := getContexts() - if err != nil { - return err + loginCmd := &cobra.Command{ + Use: "login", + Short: "login user and receive token", + Long: "login and receive token that will be used to authenticate commands.", + RunE: func(cmd *cobra.Command, args []string) error { + + var console io.Writer + var handler auth.TokenHandlerFunc + if viper.GetBool("print-only") { + // do not print to console + handler = printTokenHandler + } else { + cs, err := api.GetContexts() + if err != nil { + return err + } + console = os.Stdout + handler = auth.NewUpdateKubeConfigHandler(viper.GetString("kubeconfig"), console, auth.WithContextName(formatContextName(cloudContext, cs.CurrentContext))) } - console = os.Stdout - handler = auth.NewUpdateKubeConfigHandler(viper.GetString("kubeconfig"), console, auth.WithContextName(formatContextName(cloudContext, cs.CurrentContext))) - } - scopes := auth.DexScopes - if ctx.IssuerType == "generic" { - scopes = auth.GenericScopes - } else if ctx.CustomScopes != "" { - cs := strings.Split(ctx.CustomScopes, ",") - for i := range cs { - cs[i] = strings.TrimSpace(cs[i]) + ctx := api.MustDefaultContext() + scopes := auth.DexScopes + if ctx.IssuerType == "generic" { + scopes = auth.GenericScopes + } else if ctx.CustomScopes != "" { + cs := strings.Split(ctx.CustomScopes, ",") + for i := range cs { + cs[i] = strings.TrimSpace(cs[i]) + } + scopes = cs } - scopes = cs - } - config := auth.Config{ - ClientID: ctx.ClientID, - ClientSecret: ctx.ClientSecret, - IssuerURL: ctx.IssuerURL, - Scopes: scopes, - TokenHandler: handler, - Console: console, - Debug: viper.GetBool("debug"), - } + config := auth.Config{ + ClientID: ctx.ClientID, + ClientSecret: ctx.ClientSecret, + IssuerURL: ctx.IssuerURL, + Scopes: scopes, + TokenHandler: handler, + Console: console, + Debug: viper.GetBool("debug"), + } - fmt.Println() + fmt.Println() - return auth.OIDCFlow(config) - }, - PreRun: bindPFlags, + return auth.OIDCFlow(config) + }, + PreRun: bindPFlags, + } + loginCmd.Flags().Bool("print-only", false, "If true, the token is printed to stdout") + return loginCmd } func printTokenHandler(tokenInfo auth.TokenInfo) error { - fmt.Println(tokenInfo.IDToken) return nil } - -func init() { - - loginCmd.Flags().Bool("printOnly", false, "If true, the token is printed to stdout") -} diff --git a/cmd/machine.go b/cmd/machine.go index d68e1fad..82839fa0 100644 --- a/cmd/machine.go +++ b/cmd/machine.go @@ -13,6 +13,8 @@ import ( metalgo "github.com/metal-stack/metal-go" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" + "github.com/metal-stack/metalctl/pkg/api" "github.com/spf13/cobra" "github.com/spf13/viper" ) @@ -35,16 +37,17 @@ const ( forceFlag = "yes-i-really-mean-it" ) -var ( - filterOpts = &FilterOpts{} +var filterOpts = &FilterOpts{} - machineCmd = &cobra.Command{ +func newMachineCmd(c *config) *cobra.Command { + + machineCmd := &cobra.Command{ Use: "machine", Short: "manage machines", Long: "metal machines are bare metal servers.", } - machineCreateCmd = &cobra.Command{ + machineCreateCmd := &cobra.Command{ Use: "create", Short: "create a machine", Long: `create a new machine with the given operating system, the size and a project.`, @@ -85,271 +88,269 @@ Once created the machine installation can not be modified anymore. `, RunE: func(cmd *cobra.Command, args []string) error { - return machineCreate(driver) + return c.machineCreate() }, PreRun: bindPFlags, } - machineListCmd = &cobra.Command{ + machineListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all machines", Long: "list all machines with almost all properties in tabular form.", RunE: func(cmd *cobra.Command, args []string) error { - return machineList(driver) + return c.machineList() }, PreRun: bindPFlags, } - machineDescribeCmd = &cobra.Command{ + machineDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a machine", Long: "describe a machine in a very detailed form with all properties.", RunE: func(cmd *cobra.Command, args []string) error { - return machineDescribe(driver, args) + return c.machineDescribe(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineConsolePasswordCmd = &cobra.Command{ + machineConsolePasswordCmd := &cobra.Command{ Use: "consolepassword ", Short: "fetch the consolepassword for a machine", RunE: func(cmd *cobra.Command, args []string) error { - return machineConsolePassword(driver, args) + return c.machineConsolePassword(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineDestroyCmd = &cobra.Command{ + machineDestroyCmd := &cobra.Command{ Use: "destroy ", Aliases: []string{"delete", "rm"}, Short: "destroy a machine", Long: `destroy a machine and destroy all data stored on the local disks. Once destroyed it is back for usage by other projects. A destroyed machine can not restored anymore`, RunE: func(cmd *cobra.Command, args []string) error { - return machineDestroy(driver, args) + return c.machineDestroy(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machinePowerCmd = &cobra.Command{ + machinePowerCmd := &cobra.Command{ Use: "power", Short: "manage machine power", } - machinePowerOnCmd = &cobra.Command{ + machinePowerOnCmd := &cobra.Command{ Use: "on ", Short: "power on a machine", Long: "set the machine to power on state, if the machine already was on nothing happens.", RunE: func(cmd *cobra.Command, args []string) error { - return machinePowerOn(driver, args) + return c.machinePowerOn(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machinePowerOffCmd = &cobra.Command{ + machinePowerOffCmd := &cobra.Command{ Use: "off ", Short: "power off a machine", Long: `set the machine to power off state, if the machine already was off nothing happens. It will usually take some time to power off the machine, depending on the machine type. Power on will therefore not work if the machine is in the powering off phase.`, RunE: func(cmd *cobra.Command, args []string) error { - return machinePowerOff(driver, args) + return c.machinePowerOff(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machinePowerResetCmd = &cobra.Command{ + machinePowerResetCmd := &cobra.Command{ Use: "reset ", Short: "power reset a machine", Long: "(hard) reset the machine power.", RunE: func(cmd *cobra.Command, args []string) error { - return machinePowerReset(driver, args) + return c.machinePowerReset(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machinePowerCycleCmd = &cobra.Command{ + machinePowerCycleCmd := &cobra.Command{ Use: "cycle ", Short: "power cycle a machine", Long: "(soft) cycle the machine power.", RunE: func(cmd *cobra.Command, args []string) error { - return machinePowerCycle(driver, args) + return c.machinePowerCycle(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineUpdateCmd = &cobra.Command{ + machineUpdateCmd := &cobra.Command{ Use: "update", Aliases: []string{"firmware-update"}, Short: "update a machine firmware", } - machineUpdateBiosCmd = &cobra.Command{ + machineUpdateBiosCmd := &cobra.Command{ Use: "bios ", Short: "update a machine BIOS", Long: "the machine BIOS will be updated to given revision. If revision flag is not specified an update plan will be printed instead.", RunE: func(cmd *cobra.Command, args []string) error { - return machineUpdateBios(driver, args) + return c.machineUpdateBios(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineUpdateBmcCmd = &cobra.Command{ + machineUpdateBmcCmd := &cobra.Command{ Use: "bmc ", Short: "update a machine BMC", Long: "the machine BMC will be updated to given revision. If revision flag is not specified an update plan will be printed instead.", RunE: func(cmd *cobra.Command, args []string) error { - return machineUpdateBmc(driver, args) + return c.machineUpdateBmc(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineBootBiosCmd = &cobra.Command{ + machineBootBiosCmd := &cobra.Command{ Use: "bios ", Short: "boot a machine into BIOS", Long: "the machine will boot into bios.", RunE: func(cmd *cobra.Command, args []string) error { - return machineBootBios(driver, args) + return c.machineBootBios(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineBootPxeCmd = &cobra.Command{ + machineBootPxeCmd := &cobra.Command{ Use: "pxe ", Short: "boot a machine from PXE", Long: "the machine will boot from PXE.", RunE: func(cmd *cobra.Command, args []string) error { - return machineBootPxe(driver, args) + return c.machineBootPxe(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineBootDiskCmd = &cobra.Command{ + machineBootDiskCmd := &cobra.Command{ Use: "disk ", Short: "boot a machine from disk", Long: "the machine will boot from disk.", RunE: func(cmd *cobra.Command, args []string) error { - return machineBootDisk(driver, args) + return c.machineBootDisk(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineIdentifyCmd = &cobra.Command{ + machineIdentifyCmd := &cobra.Command{ Use: "identify", Short: "manage machine chassis identify LED power", } - machineIdentifyOnCmd = &cobra.Command{ + machineIdentifyOnCmd := &cobra.Command{ Use: "on ", Short: "power on the machine chassis identify LED", Long: `set the machine chassis identify LED to on state`, RunE: func(cmd *cobra.Command, args []string) error { - return machineIdentifyOn(driver, args) + return c.machineIdentifyOn(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineIdentifyOffCmd = &cobra.Command{ + machineIdentifyOffCmd := &cobra.Command{ Use: "off ", Short: "power off the machine chassis identify LED", Long: `set the machine chassis identify LED to off state`, RunE: func(cmd *cobra.Command, args []string) error { - return machineIdentifyOff(driver, args) + return c.machineIdentifyOff(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineReserveCmd = &cobra.Command{ + machineReserveCmd := &cobra.Command{ Use: "reserve ", Short: "reserve a machine", Long: `reserve a machine for exclusive usage, this machine will no longer be picked by other allocations. This is useful for maintenance of the machine or testing. After the reservation is not needed anymore, the reservation should be removed with --remove.`, RunE: func(cmd *cobra.Command, args []string) error { - return machineReserve(driver, args) + return c.machineReserve(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineLockCmd = &cobra.Command{ + machineLockCmd := &cobra.Command{ Use: "lock ", Short: "lock a machine", Long: `when a machine is locked, it can not be destroyed, to destroy a machine you must first remove the lock from that machine with --remove`, RunE: func(cmd *cobra.Command, args []string) error { - return machineLock(driver, args) + return c.machineLock(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineReinstallCmd = &cobra.Command{ + machineReinstallCmd := &cobra.Command{ Use: "reinstall ", Short: "reinstalls an already allocated machine", Long: `reinstalls an already allocated machine. If it is not yet allocated, nothing happens, otherwise only the machine's primary disk is wiped and the new image will subsequently be installed on that device`, RunE: func(cmd *cobra.Command, args []string) error { - return machineReinstall(driver, args) + return c.machineReinstall(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineConsoleCmd = &cobra.Command{ + machineConsoleCmd := &cobra.Command{ Use: "console ", Short: `console access to a machine, machine must be created with a ssh public key, authentication is done with your private key. In case the machine did not register properly a direct ipmi console access is available via the --ipmi flag. This is only for administrative access.`, RunE: func(cmd *cobra.Command, args []string) error { - return machineConsole(driver, args) + return c.machineConsole(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } - machineIpmiCmd = &cobra.Command{ + machineIpmiCmd := &cobra.Command{ Use: "ipmi []", Short: `display ipmi details of the machine, if no machine ID is given all ipmi addresses are returned.`, RunE: func(cmd *cobra.Command, args []string) error { - return machineIpmi(driver, args) + return c.machineIpmi(args) }, PreRun: bindPFlags, } - machineIssuesCmd = &cobra.Command{ + machineIssuesCmd := &cobra.Command{ Use: "issues", Short: `display machines which are in a potential bad state`, RunE: func(cmd *cobra.Command, args []string) error { - return machineIssues(driver) + return c.machineIssues() }, PreRun: bindPFlags, } - machineLogsCmd = &cobra.Command{ + machineLogsCmd := &cobra.Command{ Use: "logs ", Aliases: []string{"log"}, Short: `display machine provisioning logs`, RunE: func(cmd *cobra.Command, args []string) error { - return machineLogs(driver, args) + return c.machineLogs(args) }, PreRun: bindPFlags, - ValidArgsFunction: machineListCompletionFunc, + ValidArgsFunction: c.comp.MachineListCompletion, } -) -func init() { - addMachineCreateFlags(machineCreateCmd, "machine") + c.addMachineCreateFlags(machineCreateCmd, "machine") machineCmd.AddCommand(machineCreateCmd) machineListCmd.Flags().StringVarP(&filterOpts.ID, "id", "", "", "ID to filter [optional]") @@ -362,36 +363,11 @@ func init() { machineListCmd.Flags().StringVarP(&filterOpts.Mac, "mac", "", "", "mac to filter [optional]") machineListCmd.Flags().StringSliceVar(&filterOpts.Tags, "tags", []string{}, "tags to filter, use it like: --tags \"tag1,tag2\" or --tags \"tag3\".") - err := machineListCmd.RegisterFlagCompletionFunc("partition", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineListCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineListCmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return projectListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineListCmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return machineListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineListCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return imageListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(machineListCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion)) + must(machineListCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(machineListCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + must(machineListCmd.RegisterFlagCompletionFunc("id", c.comp.MachineListCompletion)) + must(machineListCmd.RegisterFlagCompletionFunc("image", c.comp.ImageListCompletion)) machineIpmiCmd.Flags().StringVarP(&filterOpts.ID, "id", "", "", "ID to filter [optional]") machineIpmiCmd.Flags().StringVarP(&filterOpts.Partition, "partition", "", "", "partition to filter [optional]") @@ -403,30 +379,10 @@ func init() { machineIpmiCmd.Flags().StringVarP(&filterOpts.Mac, "mac", "", "", "mac to filter [optional]") machineIpmiCmd.Flags().StringSliceVar(&filterOpts.Tags, "tags", []string{}, "tags to filter, use it like: --tags \"tag1,tag2\" or --tags \"tag3\".") - err = machineIpmiCmd.RegisterFlagCompletionFunc("partition", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIpmiCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIpmiCmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return projectListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIpmiCmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return machineListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(machineIpmiCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion)) + must(machineIpmiCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(machineIpmiCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + must(machineIpmiCmd.RegisterFlagCompletionFunc("id", c.comp.MachineListCompletion)) machineConsolePasswordCmd.Flags().StringP("reason", "", "", "a short description why access to the consolepassword is required") @@ -437,22 +393,12 @@ func init() { machineUpdateBiosCmd.Flags().StringP("revision", "", "", "the BIOS revision") machineUpdateBiosCmd.Flags().StringP("description", "", "", "the reason why the BIOS should be updated") - err = machineUpdateBiosCmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareRevisionCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(machineUpdateBiosCmd.RegisterFlagCompletionFunc("revision", c.comp.FirmwareBiosRevisionCompletion)) machineUpdateCmd.AddCommand(machineUpdateBiosCmd) machineUpdateBmcCmd.Flags().StringP("revision", "", "", "the BMC revision") machineUpdateBmcCmd.Flags().StringP("description", "", "", "the reason why the BMC should be updated") - err = machineUpdateBmcCmd.RegisterFlagCompletionFunc("revision", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return firmwareRevisionCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(machineUpdateBmcCmd.RegisterFlagCompletionFunc("revision", c.comp.FirmwareBmcRevisionCompletion)) machineUpdateCmd.AddCommand(machineUpdateBmcCmd) machineCmd.AddCommand(machineUpdateCmd) @@ -483,10 +429,7 @@ func init() { machineReinstallCmd.Flags().StringP("image", "", "", "id of the image to get installed. [required]") machineReinstallCmd.Flags().StringP("description", "d", "", "description of the reinstallation. [optional]") - err = machineReinstallCmd.MarkFlagRequired("image") - if err != nil { - log.Fatal(err.Error()) - } + must(machineReinstallCmd.MarkFlagRequired("image")) machineCmd.AddCommand(machineReinstallCmd) machineIssuesCmd.Flags().StringSliceP("only", "", []string{}, "issue types to include [optional]") @@ -501,56 +444,26 @@ func init() { machineIssuesCmd.Flags().StringVarP(&filterOpts.Mac, "mac", "", "", "mac to filter [optional]") machineIssuesCmd.Flags().StringSliceVar(&filterOpts.Tags, "tags", []string{}, "tags to filter, use it like: --tags \"tag1,tag2\" or --tags \"tag3\".") - err = machineIssuesCmd.RegisterFlagCompletionFunc("partition", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return projectListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return machineListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return imageListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("omit", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + must(machineIssuesCmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion)) + must(machineIssuesCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(machineIssuesCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + must(machineIssuesCmd.RegisterFlagCompletionFunc("id", c.comp.MachineListCompletion)) + must(machineIssuesCmd.RegisterFlagCompletionFunc("image", c.comp.ImageListCompletion)) + must(machineIssuesCmd.RegisterFlagCompletionFunc("omit", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var shortNames []string - for _, i := range AllIssues { + for _, i := range api.AllIssues { shortNames = append(shortNames, i.ShortName+"\t"+i.Description) } return shortNames, cobra.ShellCompDirectiveNoFileComp - }) - if err != nil { - log.Fatal(err.Error()) - } - err = machineIssuesCmd.RegisterFlagCompletionFunc("only", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + })) + must(machineIssuesCmd.RegisterFlagCompletionFunc("only", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { var shortNames []string - for _, i := range AllIssues { + for _, i := range api.AllIssues { shortNames = append(shortNames, i.ShortName+"\t"+i.Description) } return shortNames, cobra.ShellCompDirectiveNoFileComp - }) - if err != nil { - log.Fatal(err.Error()) - } + })) + machineConsoleCmd.Flags().StringP("sshidentity", "p", "", "SSH key file, if not given the default ssh key will be used if present [optional].") machineConsoleCmd.Flags().BoolP("ipmi", "", false, "use ipmitool with direct network access (admin only).") machineConsoleCmd.Flags().StringP("ipmiuser", "", "", "overwrite ipmi user (admin only).") @@ -561,9 +474,11 @@ func init() { machineCmd.AddCommand(machineLogsCmd) machineDestroyCmd.Flags().Bool("remove-from-database", false, "remove given machine from the database, is only required for maintenance reasons [optional] (admin only).") + + return machineCmd } -func addMachineCreateFlags(cmd *cobra.Command, name string) { +func (c *config) addMachineCreateFlags(cmd *cobra.Command, name string) { cmd.Flags().StringP("description", "d", "", "Description of the "+name+" to create. [optional]") cmd.Flags().StringP("partition", "S", "", "partition/datacenter where the "+name+" is created. [required, except for reserved machines]") cmd.Flags().StringP("hostname", "H", "", "Hostname of the "+name+". [required]") @@ -617,83 +532,34 @@ MODE can be omitted or one of: log.Fatal(fmt.Errorf("illegal name: %s. Must be one of (machine, firewall)", name)) } - err := cmd.MarkFlagRequired("hostname") - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.MarkFlagRequired("image") - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.MarkFlagRequired("project") - if err != nil { - log.Fatal(err.Error()) - } + must(cmd.MarkFlagRequired("hostname")) + must(cmd.MarkFlagRequired("image")) + must(cmd.MarkFlagRequired("project")) // Completion for arguments - err = cmd.RegisterFlagCompletionFunc("networks", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return networkListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("ips", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return ipListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("partition", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return sizeListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("project", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return projectListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return machineListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("image", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return imageListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = cmd.RegisterFlagCompletionFunc("filesystemlayout", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return filesystemLayoutListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } + must(cmd.RegisterFlagCompletionFunc("networks", c.comp.NetworkListCompletion)) + must(cmd.RegisterFlagCompletionFunc("ips", c.comp.IpListCompletion)) + must(cmd.RegisterFlagCompletionFunc("partition", c.comp.PartitionListCompletion)) + must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + must(cmd.RegisterFlagCompletionFunc("id", c.comp.MachineListCompletion)) + must(cmd.RegisterFlagCompletionFunc("image", c.comp.ImageListCompletion)) + must(cmd.RegisterFlagCompletionFunc("filesystemlayout", c.comp.FilesystemLayoutListCompletion)) } -func machineCreate(driver *metalgo.Driver) error { - mcr, err := machineCreateRequest() +func (c *config) machineCreate() error { + mcr, err := c.machineCreateRequest() if err != nil { return fmt.Errorf("machine create error:%w", err) } - resp, err := driver.MachineCreate(mcr) + resp, err := c.driver.MachineCreate(mcr) if err != nil { return fmt.Errorf("machine create error:%w", err) } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineCreateRequest() (*metalgo.MachineCreateRequest, error) { +func (c *config) machineCreateRequest() (*metalgo.MachineCreateRequest, error) { sshPublicKeyArgument := viper.GetString("sshpublickey") if strings.HasPrefix(sshPublicKeyArgument, "@") { @@ -761,7 +627,7 @@ func machineCreateRequest() (*metalgo.MachineCreateRequest, error) { return mcr, nil } -func machineList(driver *metalgo.Driver) error { +func (c *config) machineList() error { var resp *metalgo.MachineListResponse var err error if atLeastOneViperStringFlagGiven("id", "partition", "size", "name", "project", "image", "hostname", "mac") || @@ -797,35 +663,35 @@ func machineList(driver *metalgo.Driver) error { if len(filterOpts.Tags) > 0 { mfr.Tags = filterOpts.Tags } - resp, err = driver.MachineFind(mfr) + resp, err = c.driver.MachineFind(mfr) } else { - resp, err = driver.MachineList() + resp, err = c.driver.MachineList() } if err != nil { return err } - return printer.Print(resp.Machines) + return output.New().Print(resp.Machines) } -func machineDescribe(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineDescribe(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineGet(machineID) + resp, err := c.driver.MachineGet(machineID) if err != nil { return err } - return detailer.Detail(resp.Machine) + return output.NewDetailer().Detail(resp.Machine) } -func machineConsolePassword(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineConsolePassword(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } reason := viper.GetString("reason") - resp, err := driver.MachineConsolePassword(machineID, reason) + resp, err := c.driver.MachineConsolePassword(machineID, reason) if err != nil { return err } @@ -833,8 +699,8 @@ func machineConsolePassword(driver *metalgo.Driver, args []string) error { return nil } -func machineDestroy(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineDestroy(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } @@ -843,74 +709,74 @@ func machineDestroy(driver *metalgo.Driver, args []string) error { if !viper.GetBool(forceFlag) { return fmt.Errorf("remove-from-database is set but you forgot to add --%s", forceFlag) } - resp, err := driver.MachineDeleteFromDatabase(machineID) + resp, err := c.driver.MachineDeleteFromDatabase(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } - resp, err := driver.MachineDelete(machineID) + resp, err := c.driver.MachineDelete(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machinePowerOn(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machinePowerOn(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachinePowerOn(machineID) + resp, err := c.driver.MachinePowerOn(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machinePowerOff(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machinePowerOff(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachinePowerOff(machineID) + resp, err := c.driver.MachinePowerOff(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machinePowerReset(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machinePowerReset(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachinePowerReset(machineID) + resp, err := c.driver.MachinePowerReset(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machinePowerCycle(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machinePowerCycle(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachinePowerCycle(machineID) + resp, err := c.driver.MachinePowerCycle(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineUpdateBios(driver *metalgo.Driver, args []string) error { - m, vendor, board, err := firmwareData(args) +func (c *config) machineUpdateBios(args []string) error { + m, vendor, board, err := c.firmwareData(args) if err != nil { return err } @@ -920,11 +786,11 @@ func machineUpdateBios(driver *metalgo.Driver, args []string) error { currentVersion = *m.Bios.Version } - return machineUpdateFirmware(driver, metalgo.Bios, *m.ID, vendor, board, revision, currentVersion) + return c.machineUpdateFirmware(metalgo.Bios, *m.ID, vendor, board, revision, currentVersion) } -func machineUpdateBmc(driver *metalgo.Driver, args []string) error { - m, vendor, board, err := firmwareData(args) +func (c *config) machineUpdateBmc(args []string) error { + m, vendor, board, err := c.firmwareData(args) if err != nil { return err } @@ -934,11 +800,11 @@ func machineUpdateBmc(driver *metalgo.Driver, args []string) error { currentVersion = *m.Ipmi.Bmcversion } - return machineUpdateFirmware(driver, metalgo.Bmc, *m.ID, vendor, board, revision, currentVersion) + return c.machineUpdateFirmware(metalgo.Bmc, *m.ID, vendor, board, revision, currentVersion) } -func firmwareData(args []string) (*models.V1MachineIPMIResponse, string, string, error) { - m, err := getMachine(args) +func (c *config) firmwareData(args []string) (*models.V1MachineIPMIResponse, string, string, error) { + m, err := c.getMachine(args) if err != nil { return nil, "", "", err } @@ -954,8 +820,8 @@ func firmwareData(args []string) (*models.V1MachineIPMIResponse, string, string, return m, vendor, board, nil } -func machineUpdateFirmware(driver *metalgo.Driver, kind metalgo.FirmwareKind, machineID, vendor, board, revision, currentVersion string) error { - f, err := driver.ListFirmwares(kind, "", "") +func (c *config) machineUpdateFirmware(kind metalgo.FirmwareKind, machineID, vendor, board, revision, currentVersion string) error { + f, err := c.driver.ListFirmwares(kind, "", "") if err != nil { return err } @@ -1021,82 +887,82 @@ func machineUpdateFirmware(driver *metalgo.Driver, kind metalgo.FirmwareKind, ma description = "unknown" } - resp, err := driver.MachineUpdateFirmware(kind, machineID, revision, description) + resp, err := c.driver.MachineUpdateFirmware(kind, machineID, revision, description) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineBootBios(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineBootBios(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineBootBios(machineID) + resp, err := c.driver.MachineBootBios(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineBootDisk(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineBootDisk(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineBootDisk(machineID) + resp, err := c.driver.MachineBootDisk(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineBootPxe(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineBootPxe(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineBootPxe(machineID) + resp, err := c.driver.MachineBootPxe(machineID) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineIdentifyOn(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineIdentifyOn(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } description := viper.GetString("description") - resp, err := driver.ChassisIdentifyLEDPowerOn(machineID, description) + resp, err := c.driver.ChassisIdentifyLEDPowerOn(machineID, description) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineIdentifyOff(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineIdentifyOff(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } description := viper.GetString("description") - resp, err := driver.ChassisIdentifyLEDPowerOff(machineID, description) + resp, err := c.driver.ChassisIdentifyLEDPowerOff(machineID, description) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineReserve(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineReserve(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } @@ -1105,21 +971,21 @@ func machineReserve(driver *metalgo.Driver, args []string) error { var resp *metalgo.MachineStateResponse if remove { - resp, err = driver.MachineUnReserve(machineID) + resp, err = c.driver.MachineUnReserve(machineID) if err != nil { return err } } else { - resp, err = driver.MachineReserve(machineID, description) + resp, err = c.driver.MachineReserve(machineID, description) if err != nil { return err } } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineLock(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineLock(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } @@ -1128,21 +994,21 @@ func machineLock(driver *metalgo.Driver, args []string) error { var resp *metalgo.MachineStateResponse if remove { - resp, err = driver.MachineUnLock(machineID) + resp, err = c.driver.MachineUnLock(machineID) if err != nil { return err } } else { - resp, err = driver.MachineLock(machineID, description) + resp, err = c.driver.MachineLock(machineID, description) if err != nil { return err } } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineReinstall(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineReinstall(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } @@ -1150,30 +1016,30 @@ func machineReinstall(driver *metalgo.Driver, args []string) error { description := viper.GetString("description") var resp *metalgo.MachineGetResponse - resp, err = driver.MachineReinstall(machineID, imageID, description) + resp, err = c.driver.MachineReinstall(machineID, imageID, description) if err != nil { return err } - return printer.Print(resp.Machine) + return output.New().Print(resp.Machine) } -func machineLogs(driver *metalgo.Driver, args []string) error { +func (c *config) machineLogs(args []string) error { // FIXME add ipmi sel as well - machineID, err := getMachineID(args) + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineGet(machineID) + resp, err := c.driver.MachineGet(machineID) if err != nil { return err } machine := resp.Machine - return printer.Print(machine.Events.Log) + return output.New().Print(machine.Events.Log) } -func machineConsole(driver *metalgo.Driver, args []string) error { - machineID, err := getMachineID(args) +func (c *config) machineConsole(args []string) error { + machineID, err := c.getMachineID(args) if err != nil { return err } @@ -1184,7 +1050,7 @@ func machineConsole(driver *metalgo.Driver, args []string) error { return fmt.Errorf("unable to locate ipmitool in path") } - resp, err := driver.MachineIPMIGet(machineID) + resp, err := c.driver.MachineIPMIGet(machineID) if err != nil { return err } @@ -1234,7 +1100,7 @@ func machineConsole(driver *metalgo.Driver, args []string) error { return fmt.Errorf("machine console error:%w", err) } } - parsedurl, err := url.Parse(driverURL) + parsedurl, err := url.Parse(c.driverURL) if err != nil { return err } @@ -1253,20 +1119,20 @@ func machineConsole(driver *metalgo.Driver, args []string) error { return nil } -func machineIpmi(driver *metalgo.Driver, args []string) error { +func (c *config) machineIpmi(args []string) error { if len(args) == 1 { - machineID, err := getMachineID(args) + machineID, err := c.getMachineID(args) if err != nil { return err } - resp, err := driver.MachineIPMIGet(machineID) + resp, err := c.driver.MachineIPMIGet(machineID) if err != nil { return err } hidden := "" resp.Machine.Ipmi.Password = &hidden - return detailer.Detail(resp.Machine) + return output.NewDetailer().Detail(resp.Machine) } mfr := &metalgo.MachineFindRequest{} @@ -1300,14 +1166,14 @@ func machineIpmi(driver *metalgo.Driver, args []string) error { if len(filterOpts.Tags) > 0 { mfr.Tags = filterOpts.Tags } - resp, err := driver.MachineIPMIList(mfr) + resp, err := c.driver.MachineIPMIList(mfr) if err != nil { return err } - return printer.Print(resp.Machines) + return output.New().Print(resp.Machines) } -func machineIssues(driver *metalgo.Driver) error { +func (c *config) machineIssues() error { mfr := &metalgo.MachineFindRequest{} if filterOpts.ID != "" { mfr.ID = &filterOpts.ID @@ -1339,7 +1205,7 @@ func machineIssues(driver *metalgo.Driver) error { if len(filterOpts.Tags) > 0 { mfr.Tags = filterOpts.Tags } - resp, err := driver.MachineIPMIList(mfr) + resp, err := c.driver.MachineIPMIList(mfr) if err != nil { return err } @@ -1348,11 +1214,11 @@ func machineIssues(driver *metalgo.Driver) error { omit := viper.GetStringSlice("omit") var ( - res = MachineIssues{} + res = api.MachineIssues{} asnMap = map[int64][]models.V1MachineIPMIResponse{} bmcIPMap = map[string][]models.V1MachineIPMIResponse{} - conditionalAppend = func(issues Issues, issue Issue) Issues { + conditionalAppend = func(issues api.Issues, issue api.Issue) api.Issues { for _, o := range omit { if issue.ShortName == o { return issues @@ -1373,28 +1239,28 @@ func machineIssues(driver *metalgo.Driver) error { ) for _, m := range resp.Machines { - var issues Issues + var issues api.Issues if m.Partition == nil { - issues = conditionalAppend(issues, IssueNoPartition) + issues = conditionalAppend(issues, api.IssueNoPartition) } if m.Liveliness != nil { switch *m.Liveliness { case "Alive": case "Dead": - issues = conditionalAppend(issues, IssueLivelinessDead) + issues = conditionalAppend(issues, api.IssueLivelinessDead) case "Unknown": - issues = conditionalAppend(issues, IssueLivelinessUnknown) + issues = conditionalAppend(issues, api.IssueLivelinessUnknown) default: - issues = conditionalAppend(issues, IssueLivelinessNotAvailable) + issues = conditionalAppend(issues, api.IssueLivelinessNotAvailable) } } else { - issues = conditionalAppend(issues, IssueLivelinessNotAvailable) + issues = conditionalAppend(issues, api.IssueLivelinessNotAvailable) } if m.Allocation == nil && len(m.Events.Log) > 0 && *m.Events.Log[0].Event == "Phoned Home" { - issues = conditionalAppend(issues, IssueFailedMachineReclaim) + issues = conditionalAppend(issues, api.IssueFailedMachineReclaim) } if m.Events.IncompleteProvisioningCycles != nil && @@ -1403,17 +1269,17 @@ func machineIssues(driver *metalgo.Driver) error { if m.Events != nil && len(m.Events.Log) > 0 && *m.Events.Log[0].Event == "Waiting" { // Machine which are waiting are not considered to have issues } else { - issues = conditionalAppend(issues, IssueIncompleteCycles) + issues = conditionalAppend(issues, api.IssueIncompleteCycles) } } if m.Ipmi != nil { if m.Ipmi.Mac == nil || *m.Ipmi.Mac == "" { - issues = conditionalAppend(issues, IssueBMCWithoutMAC) + issues = conditionalAppend(issues, api.IssueBMCWithoutMAC) } if m.Ipmi.Address == nil || *m.Ipmi.Address == "" { - issues = conditionalAppend(issues, IssueBMCWithoutIP) + issues = conditionalAppend(issues, api.IssueBMCWithoutIP) } else { entries := bmcIPMap[*m.Ipmi.Address] entries = append(entries, *m) @@ -1451,7 +1317,7 @@ func machineIssues(driver *metalgo.Driver) error { } if len(issues) > 0 { - res[*m.ID] = MachineWithIssues{ + res[*m.ID] = api.MachineWithIssues{ Machine: *m, Issues: issues, } @@ -1460,7 +1326,7 @@ func machineIssues(driver *metalgo.Driver) error { includeASN := true for _, o := range omit { - if o == IssueASNUniqueness.ShortName { + if o == api.IssueASNUniqueness.ShortName { includeASN = false break } @@ -1483,11 +1349,11 @@ func machineIssues(driver *metalgo.Driver) error { mWithIssues, ok := res[*m.ID] if !ok { - mWithIssues = MachineWithIssues{ + mWithIssues = api.MachineWithIssues{ Machine: m, } } - issue := IssueASNUniqueness + issue := api.IssueASNUniqueness issue.Description = fmt.Sprintf("ASN (%d) not unique, shared with %s", asn, sharedIDs) mWithIssues.Issues = append(mWithIssues.Issues, issue) res[*m.ID] = mWithIssues @@ -1497,7 +1363,7 @@ func machineIssues(driver *metalgo.Driver) error { includeDistinctBMC := true for _, o := range omit { - if o == IssueNonDistinctBMCIP.ShortName { + if o == api.IssueNonDistinctBMCIP.ShortName { includeDistinctBMC = false break } @@ -1520,11 +1386,11 @@ func machineIssues(driver *metalgo.Driver) error { mWithIssues, ok := res[*m.ID] if !ok { - mWithIssues = MachineWithIssues{ + mWithIssues = api.MachineWithIssues{ Machine: m, } } - issue := IssueNonDistinctBMCIP + issue := api.IssueNonDistinctBMCIP issue.Description = fmt.Sprintf("BMC IP (%s) not unique, shared with %s", ip, sharedIDs) mWithIssues.Issues = append(mWithIssues.Issues, issue) res[*m.ID] = mWithIssues @@ -1532,29 +1398,29 @@ func machineIssues(driver *metalgo.Driver) error { } } - return printer.Print(res) + return output.New().Print(res) } -func getMachineID(args []string) (string, error) { +func (c *config) getMachineID(args []string) (string, error) { if len(args) < 1 { return "", fmt.Errorf("no machine ID given") } machineID := args[0] - _, err := driver.MachineGet(machineID) + _, err := c.driver.MachineGet(machineID) if err != nil { return "", err } return machineID, nil } -func getMachine(args []string) (*models.V1MachineIPMIResponse, error) { +func (c *config) getMachine(args []string) (*models.V1MachineIPMIResponse, error) { if len(args) < 1 { return nil, fmt.Errorf("no machine ID given") } machineID := args[0] - m, err := driver.MachineIPMIGet(machineID) + m, err := c.driver.MachineIPMIGet(machineID) if err != nil { return nil, err } diff --git a/cmd/must.go b/cmd/must.go new file mode 100644 index 00000000..516d0925 --- /dev/null +++ b/cmd/must.go @@ -0,0 +1,9 @@ +package cmd + +import "log" + +func must(err error) { + if err != nil { + log.Fatal(err.Error()) + } +} diff --git a/cmd/network.go b/cmd/network.go index 9f72993c..6076dbc1 100644 --- a/cmd/network.go +++ b/cmd/network.go @@ -3,7 +3,6 @@ package cmd import ( "errors" "fmt" - "log" "net/http" "strings" @@ -12,165 +11,166 @@ import ( networkmodel "github.com/metal-stack/metal-go/api/client/network" "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/metal-lib/pkg/tag" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - networkCmd = &cobra.Command{ +func newNetworkCmd(c *config) *cobra.Command { + networkCmd := &cobra.Command{ Use: "network", Short: "manage networks", Long: "networks for metal.", } - networkListCmd = &cobra.Command{ + networkListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all networks", RunE: func(cmd *cobra.Command, args []string) error { - return networkList(driver) + return c.networkList() }, PreRun: bindPFlags, } - networkCreateCmd = &cobra.Command{ + networkCreateCmd := &cobra.Command{ Use: "create", Short: "create a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkCreate(driver) + return c.networkCreate() }, PreRun: bindPFlags, } - networkDescribeCmd = &cobra.Command{ + networkDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkDescribe(driver, args) + return c.networkDescribe(args) }, PreRun: bindPFlags, - ValidArgsFunction: networkListCompletionFunc, + ValidArgsFunction: c.comp.NetworkListCompletion, } - networkAllocateCmd = &cobra.Command{ + networkAllocateCmd := &cobra.Command{ Use: "allocate", Short: "allocate a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkAllocate(driver) + return c.networkAllocate() }, PreRun: bindPFlags, } - networkFreeCmd = &cobra.Command{ + networkFreeCmd := &cobra.Command{ Use: "free ", Short: "free a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkFree(driver, args) + return c.networkFree(args) }, PreRun: bindPFlags, - ValidArgsFunction: networkListCompletionFunc, + ValidArgsFunction: c.comp.NetworkListCompletion, } - networkDeleteCmd = &cobra.Command{ + networkDeleteCmd := &cobra.Command{ Use: "delete ", Short: "delete a network", Aliases: []string{"destroy", "rm", "remove"}, RunE: func(cmd *cobra.Command, args []string) error { - return networkDelete(driver, args) + return c.networkDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: networkListCompletionFunc, + ValidArgsFunction: c.comp.NetworkListCompletion, } - networkPrefixCmd = &cobra.Command{ + networkPrefixCmd := &cobra.Command{ Use: "prefix", Short: "prefix management of a network", } - networkPrefixAddCmd = &cobra.Command{ + networkPrefixAddCmd := &cobra.Command{ Use: "add ", Short: "add a prefix to a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkPrefixAdd(driver, args) + return c.networkPrefixAdd(args) }, PreRun: bindPFlags, - ValidArgsFunction: networkListCompletionFunc, + ValidArgsFunction: c.comp.NetworkListCompletion, } - networkPrefixRemoveCmd = &cobra.Command{ + networkPrefixRemoveCmd := &cobra.Command{ Use: "remove ", Short: "remove a prefix from a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkPrefixRemove(driver, args) + return c.networkPrefixRemove(args) }, PreRun: bindPFlags, - ValidArgsFunction: networkListCompletionFunc, + ValidArgsFunction: c.comp.NetworkListCompletion, } - networkIPCmd = &cobra.Command{ + networkIPCmd := &cobra.Command{ Use: "ip", Short: "manage IPs", } - networkIPListCmd = &cobra.Command{ + networkIPListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "manage IPs", RunE: func(cmd *cobra.Command, args []string) error { - return ipList(driver) + return c.ipList() }, PreRun: bindPFlags, } - networkIPAllocateCmd = &cobra.Command{ + networkIPAllocateCmd := &cobra.Command{ Use: "allocate", Short: "allocate an IP, if non given the next free is allocated, otherwise the given IP is checked for availability.", RunE: func(cmd *cobra.Command, args []string) error { - return ipAllocate(driver, args) + return c.ipAllocate(args) }, PreRun: bindPFlags, } - networkIPFreeCmd = &cobra.Command{ + networkIPFreeCmd := &cobra.Command{ Use: "free ", Short: "free an IP", RunE: func(cmd *cobra.Command, args []string) error { - return ipFree(driver, args) + return c.ipFree(args) }, PreRun: bindPFlags, - ValidArgsFunction: ipListCompletionFunc, + ValidArgsFunction: c.comp.IpListCompletion, } - networkIPApplyCmd = &cobra.Command{ + networkIPApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update an IP", RunE: func(cmd *cobra.Command, args []string) error { - return ipApply(driver) + return c.ipApply() }, PreRun: bindPFlags, } - networkIPEditCmd = &cobra.Command{ + networkIPEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a ip", RunE: func(cmd *cobra.Command, args []string) error { - return ipEdit(driver, args) + return c.ipEdit(args) }, PreRun: bindPFlags, - ValidArgsFunction: ipListCompletionFunc, + ValidArgsFunction: c.comp.IpListCompletion, } - networkApplyCmd = &cobra.Command{ + networkApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a network", RunE: func(cmd *cobra.Command, args []string) error { - return networkApply(driver) + return c.networkApply() }, PreRun: bindPFlags, } - networkIPIssuesCmd = &cobra.Command{ + networkIPIssuesCmd := &cobra.Command{ Use: "issues", Short: `display ips which are in a potential bad state`, RunE: func(cmd *cobra.Command, args []string) error { - return ipIssues(driver) + return c.ipIssues() }, PreRun: bindPFlags, } -) -func init() { + // TODO add completions for project, partition, + networkCreateCmd.Flags().StringP("id", "", "", "id of the network to create. [optional]") networkCreateCmd.Flags().StringP("description", "d", "", "description of the network to create. [optional]") networkCreateCmd.Flags().StringP("name", "n", "", "name of the network to create. [optional]") @@ -192,18 +192,9 @@ func init() { networkAllocateCmd.Flags().BoolP("dmz", "", false, "use this private network as dmz. [optional]") networkAllocateCmd.Flags().BoolP("shared", "", false, "shared allows usage of this private network from other networks") - err := networkAllocateCmd.MarkFlagRequired("name") - if err != nil { - log.Fatal(err.Error()) - } - err = networkAllocateCmd.MarkFlagRequired("project") - if err != nil { - log.Fatal(err.Error()) - } - err = networkAllocateCmd.MarkFlagRequired("partition") - if err != nil { - log.Fatal(err.Error()) - } + must(networkAllocateCmd.MarkFlagRequired("name")) + must(networkAllocateCmd.MarkFlagRequired("project")) + must(networkAllocateCmd.MarkFlagRequired("partition")) networkIPAllocateCmd.Flags().StringP("description", "d", "", "description of the IP to allocate. [optional]") networkIPAllocateCmd.Flags().StringP("name", "n", "", "name of the IP to allocate. [optional]") @@ -213,10 +204,7 @@ func init() { networkIPAllocateCmd.Flags().StringSliceP("tags", "", nil, "tags to attach to the IP.") networkIPApplyCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin.`) - err = networkIPApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(networkIPApplyCmd.MarkFlagRequired("file")) networkIPListCmd.Flags().StringP("ipaddress", "", "", "ipaddress to filter [optional]") networkIPListCmd.Flags().StringP("project", "", "", "project to filter [optional]") @@ -248,10 +236,7 @@ Example: # cat internet.yaml | metalctl network apply -f - ## or via file # metalctl network apply -f internet.yaml`) - err = networkApplyCmd.MarkFlagRequired("file") - if err != nil { - panic(err) - } + must(networkApplyCmd.MarkFlagRequired("file")) networkListCmd.Flags().StringP("id", "", "", "ID to filter [optional]") networkListCmd.Flags().StringP("name", "", "", "name to filter [optional]") @@ -274,9 +259,11 @@ Example: networkCmd.AddCommand(networkPrefixCmd) networkCmd.AddCommand(networkApplyCmd) networkCmd.AddCommand(networkDeleteCmd) + + return networkCmd } -func networkList(driver *metalgo.Driver) error { +func (c *config) networkList() error { var resp *metalgo.NetworkListResponse var err error if atLeastOneViperStringFlagGiven("id", "name", "partition", "project", "parent") || @@ -296,17 +283,17 @@ func networkList(driver *metalgo.Driver) error { DestinationPrefixes: viperStringSlice("destination-prefixes"), ParentNetworkID: viperString("parent"), } - resp, err = driver.NetworkFind(nfr) + resp, err = c.driver.NetworkFind(nfr) } else { - resp, err = driver.NetworkList() + resp, err = c.driver.NetworkList() } if err != nil { return fmt.Errorf("network list error:%w", err) } - return printer.Print(resp.Networks) + return output.New().Print(resp.Networks) } -func networkAllocate(driver *metalgo.Driver) error { +func (c *config) networkAllocate() error { var ncrs []metalgo.NetworkAllocateRequest var ncr metalgo.NetworkAllocateRequest if viper.GetString("file") != "" { @@ -341,50 +328,50 @@ func networkAllocate(driver *metalgo.Driver) error { Nat: nat, } } - resp, err := driver.NetworkAllocate(&ncr) + resp, err := c.driver.NetworkAllocate(&ncr) if err != nil { return fmt.Errorf("network allocate error:%w", err) } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func networkFree(driver *metalgo.Driver, args []string) error { +func (c *config) networkFree(args []string) error { if len(args) < 1 { return fmt.Errorf("no network given") } nw := args[0] - resp, err := driver.NetworkFree(nw) + resp, err := c.driver.NetworkFree(nw) if err != nil { return fmt.Errorf("network allocate error:%w", err) } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func networkDelete(driver *metalgo.Driver, args []string) error { +func (c *config) networkDelete(args []string) error { if len(args) < 1 { return fmt.Errorf("no network given") } nw := args[0] - resp, err := driver.NetworkDelete(nw) + resp, err := c.driver.NetworkDelete(nw) if err != nil { return fmt.Errorf("network delete error:%w", err) } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func networkDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) networkDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no network given") } nw := args[0] - resp, err := driver.NetworkGet(nw) + resp, err := c.driver.NetworkGet(nw) if err != nil { return fmt.Errorf("network describe error:%w", err) } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func networkCreate(driver *metalgo.Driver) error { +func (c *config) networkCreate() error { var ncrs []metalgo.NetworkCreateRequest var ncr metalgo.NetworkCreateRequest if viper.GetString("file") != "" { @@ -422,15 +409,15 @@ func networkCreate(driver *metalgo.Driver) error { ncr.ID = &id } } - resp, err := driver.NetworkCreate(&ncr) + resp, err := c.driver.NetworkCreate(&ncr) if err != nil { return fmt.Errorf("network create error:%w", err) } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } // TODO: General apply method would be useful as these are quite a lot of lines and it's getting erroneous -func networkApply(driver *metalgo.Driver) error { +func (c *config) networkApply() error { var iars []metalgo.NetworkCreateRequest var iar metalgo.NetworkCreateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -448,7 +435,7 @@ func networkApply(driver *metalgo.Driver) error { for _, nar := range iars { nar := nar if nar.ID == nil { - resp, err := driver.NetworkCreate(&nar) + resp, err := c.driver.NetworkCreate(&nar) if err != nil { return err } @@ -456,7 +443,7 @@ func networkApply(driver *metalgo.Driver) error { continue } - resp, err := driver.NetworkGet(*nar.ID) + resp, err := c.driver.NetworkGet(*nar.ID) if err != nil { var r *networkmodel.FindNetworkDefault if !errors.As(err, &r) { @@ -467,7 +454,7 @@ func networkApply(driver *metalgo.Driver) error { } } if resp.Network == nil { - resp, err := driver.NetworkCreate(&nar) + resp, err := c.driver.NetworkCreate(&nar) if err != nil { return err } @@ -475,17 +462,17 @@ func networkApply(driver *metalgo.Driver) error { continue } - detailResp, err := driver.NetworkUpdate(&nar) + detailResp, err := c.driver.NetworkUpdate(&nar) if err != nil { return err } response = append(response, detailResp.Network) } - return detailer.Detail(response) + return output.NewDetailer().Detail(response) } -func networkPrefixAdd(driver *metalgo.Driver, args []string) error { - networkID, err := getNetworkID(args) +func (c *config) networkPrefixAdd(args []string) error { + networkID, err := c.getNetworkID(args) if err != nil { return err } @@ -494,15 +481,15 @@ func networkPrefixAdd(driver *metalgo.Driver, args []string) error { Networkid: networkID, Prefix: viper.GetString("prefix"), } - resp, err := driver.NetworkAddPrefix(nur) + resp, err := c.driver.NetworkAddPrefix(nur) if err != nil { return err } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func networkPrefixRemove(driver *metalgo.Driver, args []string) error { - networkID, err := getNetworkID(args) +func (c *config) networkPrefixRemove(args []string) error { + networkID, err := c.getNetworkID(args) if err != nil { return err } @@ -511,14 +498,14 @@ func networkPrefixRemove(driver *metalgo.Driver, args []string) error { Networkid: networkID, Prefix: viper.GetString("prefix"), } - resp, err := driver.NetworkRemovePrefix(nur) + resp, err := c.driver.NetworkRemovePrefix(nur) if err != nil { return err } - return detailer.Detail(resp.Network) + return output.NewDetailer().Detail(resp.Network) } -func ipList(driver *metalgo.Driver) error { +func (c *config) ipList() error { var resp *metalgo.IPListResponse var err error if atLeastOneViperStringFlagGiven("ipaddress", "project", "prefix", "machineid", "network", "type", "tags", "name") { @@ -532,17 +519,17 @@ func ipList(driver *metalgo.Driver) error { Tags: viperStringSlice("tags"), Name: viperString("name"), } - resp, err = driver.IPFind(ifr) + resp, err = c.driver.IPFind(ifr) } else { - resp, err = driver.IPList() + resp, err = c.driver.IPList() } if err != nil { return fmt.Errorf("IP list error:%w", err) } - return printer.Print(resp.IPs) + return output.New().Print(resp.IPs) } -func ipApply(driver *metalgo.Driver) error { +func (c *config) ipApply() error { var iars []metalgo.IPAllocateRequest var iar metalgo.IPAllocateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -561,14 +548,14 @@ func ipApply(driver *metalgo.Driver) error { iar := iar if iar.IPAddress == "" { // acquire - resp, err := driver.IPAllocate(&iar) + resp, err := c.driver.IPAllocate(&iar) if err != nil { return err } response = append(response, resp.IP) continue } - i, err := driver.IPGet(iar.IPAddress) + i, err := c.driver.IPGet(iar.IPAddress) if err != nil { var r *ipmodel.FindIPDefault if !errors.As(err, &r) { @@ -580,7 +567,7 @@ func ipApply(driver *metalgo.Driver) error { } if i == nil { - resp, err := driver.IPAllocate(&iar) + resp, err := c.driver.IPAllocate(&iar) if err != nil { return err } @@ -595,24 +582,24 @@ func ipApply(driver *metalgo.Driver) error { Type: iar.Type, Tags: iar.Tags, } - resp, err := driver.IPUpdate(&iur) + resp, err := c.driver.IPUpdate(&iur) if err != nil { return err } response = append(response, resp.IP) } - return detailer.Detail(response) + return output.NewDetailer().Detail(response) } -func ipEdit(driver *metalgo.Driver, args []string) error { +func (c *config) ipEdit(args []string) error { if len(args) < 1 { return fmt.Errorf("no IP given") } ip := args[0] getFunc := func(ip string) ([]byte, error) { - resp, err := driver.IPGet(ip) + resp, err := c.driver.IPGet(ip) if err != nil { return nil, err } @@ -630,11 +617,11 @@ func ipEdit(driver *metalgo.Driver, args []string) error { if len(iurs) != 1 { return fmt.Errorf("ip update error more or less than one ip given:%d", len(iurs)) } - uresp, err := driver.IPUpdate(&iurs[0]) + uresp, err := c.driver.IPUpdate(&iurs[0]) if err != nil { return err } - return detailer.Detail(uresp.IP) + return output.NewDetailer().Detail(uresp.IP) } return edit(ip, getFunc, updateFunc) @@ -656,7 +643,7 @@ func readIPUpdateRequests(filename string) ([]metalgo.IPUpdateRequest, error) { return iurs, nil } -func ipAllocate(driver *metalgo.Driver, args []string) error { +func (c *config) ipAllocate(args []string) error { specificIP := "" if len(args) > 0 { specificIP = args[0] @@ -670,40 +657,40 @@ func ipAllocate(driver *metalgo.Driver, args []string) error { Type: viper.GetString("type"), Tags: viper.GetStringSlice("tags"), } - resp, err := driver.IPAllocate(iar) + resp, err := c.driver.IPAllocate(iar) if err != nil { return err } - return detailer.Detail(resp.IP) + return output.NewDetailer().Detail(resp.IP) } -func ipFree(driver *metalgo.Driver, args []string) error { +func (c *config) ipFree(args []string) error { if len(args) < 1 { return fmt.Errorf("no IP given") } ip := args[0] - resp, err := driver.IPFree(ip) + resp, err := c.driver.IPFree(ip) if err != nil { return err } - return detailer.Detail(resp.IP) + return output.NewDetailer().Detail(resp.IP) } -func getNetworkID(args []string) (string, error) { +func (c *config) getNetworkID(args []string) (string, error) { if len(args) < 1 { return "", fmt.Errorf("no network ID given") } networkID := args[0] - _, err := driver.NetworkGet(networkID) + _, err := c.driver.NetworkGet(networkID) if err != nil { return "", err } return networkID, nil } -func ipIssues(driver *metalgo.Driver) error { - ml, err := driver.MachineList() +func (c *config) ipIssues() error { + ml, err := c.driver.MachineList() if err != nil { return fmt.Errorf("machine list error:%w", err) } @@ -714,7 +701,7 @@ func ipIssues(driver *metalgo.Driver) error { var resp []*models.V1IPResponse - iplist, err := driver.IPList() + iplist, err := c.driver.IPList() if err != nil { return err } @@ -745,5 +732,5 @@ func ipIssues(driver *metalgo.Driver) error { } } } - return printer.Print(resp) + return output.New().Print(resp) } diff --git a/cmd/detailer.go b/cmd/output/detailer.go similarity index 95% rename from cmd/detailer.go rename to cmd/output/detailer.go index d29cc4d5..4ebfb90e 100644 --- a/cmd/detailer.go +++ b/cmd/output/detailer.go @@ -1,12 +1,13 @@ -package cmd +package output import ( - "fmt" + "log" "os" "sort" "github.com/metal-stack/metal-go/api/models" "github.com/olekukonko/tablewriter" + "github.com/spf13/viper" ) type ( @@ -29,7 +30,8 @@ type ( ) // NewDetailer create a new Detailer which will print more details about metal objects. -func NewDetailer(format string) (Detailer, error) { +func NewDetailer() Detailer { + format := viper.GetString("output-format") var detailer Detailer switch format { case "yaml": @@ -39,9 +41,9 @@ func NewDetailer(format string) (Detailer, error) { case "table", "wide", "markdown", "template": detailer = newTableDetailer("custom") default: - return nil, fmt.Errorf("unknown format:%s", format) + log.Fatalf("unknown format:%s", format) } - return detailer, nil + return detailer } func newTableDetailer(format string) TableDetailer { diff --git a/cmd/printer.go b/cmd/output/printer.go similarity index 91% rename from cmd/printer.go rename to cmd/output/printer.go index c6b8bda4..1e86b423 100644 --- a/cmd/printer.go +++ b/cmd/output/printer.go @@ -1,8 +1,11 @@ -package cmd +package output import ( "encoding/json" "fmt" + "log" + "math" + "net" "os" "path/filepath" "sort" @@ -18,7 +21,9 @@ import ( "github.com/fatih/color" "github.com/metal-stack/metal-go/api/models" "github.com/metal-stack/metal-lib/pkg/tag" + "github.com/metal-stack/metalctl/pkg/api" "github.com/olekukonko/tablewriter" + "github.com/spf13/viper" "gopkg.in/yaml.v3" ) @@ -130,6 +135,29 @@ type ( } ) +// New returns a suitable stdout printer for the given format +func New() Printer { + printer, err := newPrinter( + viper.GetString("output-format"), + viper.GetString("order"), + viper.GetString("template"), + viper.GetBool("no-headers"), + ) + if err != nil { + log.Fatalf("unable to initialize printer:%v", err) + } + + if viper.IsSet("force-color") { + enabled := viper.GetBool("force-color") + if enabled { + color.NoColor = false + } else { + color.NoColor = true + } + } + return printer +} + // render the table shortHeader and shortData are always expected. func (t *TablePrinter) render() { if t.template != nil { @@ -190,7 +218,7 @@ func (t *TablePrinter) rowOrTemplate(row []string, data interface{}) []string { } // NewPrinter returns a suitable stdout printer for the given format -func NewPrinter(format, order, tpl string, noHeaders bool) (Printer, error) { +func newPrinter(format, order, tpl string, noHeaders bool) (Printer, error) { var printer Printer switch format { case "yaml": @@ -263,7 +291,7 @@ func (y YAMLPrinter) Print(data interface{}) error { } // Print a model in yaml format -func (m ContextPrinter) Print(data *Contexts) error { +func (m ContextPrinter) Print(data *api.Contexts) error { for name, c := range data.Contexts { if name == data.CurrentContext { name = name + " [*]" @@ -283,7 +311,7 @@ func (t TablePrinter) Print(data interface{}) error { MetalMachineTablePrinter{t}.Print(d) case *models.V1MachineResponse: MetalMachineTablePrinter{t}.Print([]*models.V1MachineResponse{d}) - case MachineIssues: + case api.MachineIssues: MetalMachineIssuesTablePrinter{t}.Print(d) case []*models.V1FirewallResponse: MetalFirewallTablePrinter{t}.Print(d) @@ -321,7 +349,7 @@ func (t TablePrinter) Print(data interface{}) error { FilesystemLayoutPrinter{t}.Print([]*models.V1FilesystemLayoutResponse{d}) case []*models.V1FilesystemLayoutResponse: FilesystemLayoutPrinter{t}.Print(d) - case *Contexts: + case *api.Contexts: return ContextPrinter{t}.Print(d) default: return fmt.Errorf("unknown table printer for type: %T", d) @@ -699,7 +727,7 @@ func (m MetalMachineTablePrinter) Print(data []*models.V1MachineResponse) { } // Print a MetalSize in a table -func (m MetalMachineIssuesTablePrinter) Print(data MachineIssues) { +func (m MetalMachineIssuesTablePrinter) Print(data api.MachineIssues) { m.shortHeader = []string{"ID", "Power", "Lock", "Lock Reason", "Status", "Last Event", "When", "Issues"} m.wideHeader = []string{"ID", "Name", "Partition", "Project", "Power", "Status", "State", "Lock Reason", "Last Event", "When", "Issues"} @@ -1351,3 +1379,117 @@ func depth(path string) uint { } return count } + +// genericObject transforms the input to a struct which has fields with the same name as in the json struct. +// this is handy for template rendering as the output of -o json|yaml can be used as the input for the template +func genericObject(input interface{}) map[string]interface{} { + b, err := json.Marshal(input) + if err != nil { + fmt.Printf("unable to marshall input:%v", err) + os.Exit(1) + } + var result interface{} + err = json.Unmarshal(b, &result) + if err != nil { + fmt.Printf("unable to unmarshal input:%v", err) + os.Exit(1) + } + return result.(map[string]interface{}) +} + +// strValue returns the value of a string pointer of not nil, otherwise empty string +func strValue(strPtr *string) string { + if strPtr != nil { + return *strPtr + } + return "" +} + +//nolint:unparam +func truncate(input string, maxlength int) string { + elipsis := "..." + il := len(input) + el := len(elipsis) + if il <= maxlength { + return input + } + if maxlength <= el { + return input[:maxlength] + } + startlength := ((maxlength - el) / 2) - el/2 + + output := input[:startlength] + elipsis + missing := maxlength - len(output) + output = output + input[il-missing:] + return output +} + +func truncateEnd(input string, maxlength int) string { + elipsis := "..." + length := len(input) + len(elipsis) + if length <= maxlength { + return input + } + return input[:maxlength] + elipsis +} + +func humanizeDuration(duration time.Duration) string { + days := int64(duration.Hours() / 24) + hours := int64(math.Mod(duration.Hours(), 24)) + minutes := int64(math.Mod(duration.Minutes(), 60)) + seconds := int64(math.Mod(duration.Seconds(), 60)) + + chunks := []struct { + singularName string + amount int64 + }{ + {"d", days}, + {"h", hours}, + {"m", minutes}, + {"s", seconds}, + } + + parts := []string{} + + for _, chunk := range chunks { + switch chunk.amount { + case 0: + continue + default: + parts = append(parts, fmt.Sprintf("%d%s", chunk.amount, chunk.singularName)) + } + } + + if len(parts) == 0 { + return "0s" + } + if len(parts) > 2 { + parts = parts[:2] + } + return strings.Join(parts, " ") +} +func sortIPs(v1ips []*models.V1IPResponse) []*models.V1IPResponse { + + v1ipmap := make(map[string]*models.V1IPResponse) + var ips []string + for _, v1ip := range v1ips { + v1ipmap[*v1ip.Ipaddress] = v1ip + ips = append(ips, *v1ip.Ipaddress) + } + + realIPs := make([]net.IP, 0, len(ips)) + + for _, ip := range ips { + realIPs = append(realIPs, net.ParseIP(ip)) + } + + sort.Slice(realIPs, func(i, j int) bool { + return bytes.Compare(realIPs[i], realIPs[j]) < 0 + }) + + var result []*models.V1IPResponse + for _, ip := range realIPs { + result = append(result, v1ipmap[ip.String()]) + } + return result +} diff --git a/cmd/printer_test.go b/cmd/output/printer_test.go similarity index 99% rename from cmd/printer_test.go rename to cmd/output/printer_test.go index 860d1a7e..3f8fe7f1 100644 --- a/cmd/printer_test.go +++ b/cmd/output/printer_test.go @@ -1,4 +1,4 @@ -package cmd +package output import ( "testing" diff --git a/cmd/partition.go b/cmd/partition.go index 59640f05..8773c165 100644 --- a/cmd/partition.go +++ b/cmd/partition.go @@ -3,94 +3,92 @@ package cmd import ( "errors" "fmt" - "log" "net/http" metalgo "github.com/metal-stack/metal-go" partitionmodel "github.com/metal-stack/metal-go/api/client/partition" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - partitionCmd = &cobra.Command{ +func newPartitionCmd(c *config) *cobra.Command { + partitionCmd := &cobra.Command{ Use: "partition", Short: "manage partitions", Long: "a partition is a group of machines and network which is logically separated from other partitions. Machines have no direct network connections between partitions.", } - partitionListCmd = &cobra.Command{ + partitionListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all partitions", RunE: func(cmd *cobra.Command, args []string) error { - return partitionList(driver) + return c.partitionList() }, PreRun: bindPFlags, } - partitionCapacityCmd = &cobra.Command{ + partitionCapacityCmd := &cobra.Command{ Use: "capacity", Short: "show partition capacity", RunE: func(cmd *cobra.Command, args []string) error { - return partitionCapacity(driver) + return c.partitionCapacity() }, PreRun: bindPFlags, } - partitionDescribeCmd = &cobra.Command{ + partitionDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionDescribe(driver, args) + return c.partitionDescribe(args) }, - ValidArgsFunction: partitionListCompletionFunc, + ValidArgsFunction: c.comp.PartitionListCompletion, } - partitionCreateCmd = &cobra.Command{ + partitionCreateCmd := &cobra.Command{ Use: "create", Short: "create a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionCreate(driver) + return c.partitionCreate() }, PreRun: bindPFlags, } - partitionUpdateCmd = &cobra.Command{ + partitionUpdateCmd := &cobra.Command{ Use: "update", Short: "update a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionUpdate(driver) + return c.partitionUpdate() }, PreRun: bindPFlags, } - partitionApplyCmd = &cobra.Command{ + partitionApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionApply(driver) + return c.partitionApply() }, PreRun: bindPFlags, } - partitionDeleteCmd = &cobra.Command{ + partitionDeleteCmd := &cobra.Command{ Use: "delete ", Short: "delete a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionDelete(driver, args) + return c.partitionDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: partitionListCompletionFunc, + ValidArgsFunction: c.comp.PartitionListCompletion, } - partitionEditCmd = &cobra.Command{ + partitionEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a partition", RunE: func(cmd *cobra.Command, args []string) error { - return partitionEdit(driver, args) + return c.partitionEdit(args) }, PreRun: bindPFlags, - ValidArgsFunction: partitionListCompletionFunc, + ValidArgsFunction: c.comp.PartitionListCompletion, } -) -func init() { partitionCreateCmd.Flags().StringP("id", "", "", "ID of the partition. [required]") partitionCreateCmd.Flags().StringP("name", "n", "", "Name of the partition. [optional]") partitionCreateCmd.Flags().StringP("description", "d", "", "Description of the partition. [required]") @@ -108,10 +106,7 @@ Example: # cat a.yaml | metalctl partition apply -f - ## or via file # metalctl partition apply -f a.yaml`) - err := partitionApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(partitionApplyCmd.MarkFlagRequired("file")) partitionUpdateCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin. Example: @@ -122,27 +117,12 @@ Example: # cat a.yaml | metalctl partition update -f - ## or via file # metalctl partition update -f a.yaml`) - err = partitionUpdateCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(partitionUpdateCmd.MarkFlagRequired("file")) partitionCapacityCmd.Flags().StringP("id", "", "", "filter on partition id. [optional]") partitionCapacityCmd.Flags().StringP("size", "", "", "filter on size id. [optional]") - err = partitionCapacityCmd.RegisterFlagCompletionFunc("id", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return partitionListCompletion(driver) - }) - if err != nil { - log.Fatal(err.Error()) - } - err = partitionCapacityCmd.RegisterFlagCompletionFunc("size", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - sizes, comp := sizeListCompletion(driver) - sizes = append(sizes, "unknown") - return sizes, comp - }) - if err != nil { - log.Fatal(err.Error()) - } + must(partitionCapacityCmd.RegisterFlagCompletionFunc("id", c.comp.PartitionListCompletion)) + must(partitionCapacityCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) partitionCmd.AddCommand(partitionListCmd) partitionCmd.AddCommand(partitionCapacityCmd) @@ -152,29 +132,31 @@ Example: partitionCmd.AddCommand(partitionApplyCmd) partitionCmd.AddCommand(partitionDeleteCmd) partitionCmd.AddCommand(partitionEditCmd) + + return partitionCmd } -func partitionList(driver *metalgo.Driver) error { - resp, err := driver.PartitionList() +func (c *config) partitionList() error { + resp, err := c.driver.PartitionList() if err != nil { return err } - return printer.Print(resp.Partition) + return output.New().Print(resp.Partition) } -func partitionDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) partitionDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no partition ID given") } partitionID := args[0] - resp, err := driver.PartitionGet(partitionID) + resp, err := c.driver.PartitionGet(partitionID) if err != nil { return err } - return detailer.Detail(resp.Partition) + return output.NewDetailer().Detail(resp.Partition) } -func partitionCapacity(driver *metalgo.Driver) error { +func (c *config) partitionCapacity() error { var ( pcr = metalgo.PartitionCapacityRequest{} id = viper.GetString("id") @@ -188,14 +170,14 @@ func partitionCapacity(driver *metalgo.Driver) error { pcr.Size = &size } - resp, err := driver.PartitionCapacity(pcr) + resp, err := c.driver.PartitionCapacity(pcr) if err != nil { return err } - return printer.Print(resp.Capacity) + return output.New().Print(resp.Capacity) } -func partitionCreate(driver *metalgo.Driver) error { +func (c *config) partitionCreate() error { var icrs []metalgo.PartitionCreateRequest var icr metalgo.PartitionCreateRequest if viper.GetString("file") != "" { @@ -224,14 +206,14 @@ func partitionCreate(driver *metalgo.Driver) error { } } - resp, err := driver.PartitionCreate(icr) + resp, err := c.driver.PartitionCreate(icr) if err != nil { return err } - return detailer.Detail(resp.Partition) + return output.NewDetailer().Detail(resp.Partition) } -func partitionUpdate(driver *metalgo.Driver) error { +func (c *config) partitionUpdate() error { icrs, err := readPartitionCreateRequests(viper.GetString("file")) if err != nil { return err @@ -239,11 +221,11 @@ func partitionUpdate(driver *metalgo.Driver) error { if len(icrs) != 1 { return fmt.Errorf("partition update error more or less than one partition given:%d", len(icrs)) } - resp, err := driver.PartitionUpdate(icrs[0]) + resp, err := c.driver.PartitionUpdate(icrs[0]) if err != nil { return err } - return detailer.Detail(resp.Partition) + return output.NewDetailer().Detail(resp.Partition) } func readPartitionCreateRequests(filename string) ([]metalgo.PartitionCreateRequest, error) { @@ -263,7 +245,7 @@ func readPartitionCreateRequests(filename string) ([]metalgo.PartitionCreateRequ } // TODO: General apply method would be useful as these are quite a lot of lines and it's getting erroneous -func partitionApply(driver *metalgo.Driver) error { +func (c *config) partitionApply() error { var iars []metalgo.PartitionCreateRequest var iar metalgo.PartitionCreateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -278,7 +260,7 @@ func partitionApply(driver *metalgo.Driver) error { } var response []*models.V1PartitionResponse for _, iar := range iars { - resp, err := driver.PartitionGet(iar.ID) + resp, err := c.driver.PartitionGet(iar.ID) if err != nil { var r *partitionmodel.FindPartitionDefault if !errors.As(err, &r) { @@ -289,7 +271,7 @@ func partitionApply(driver *metalgo.Driver) error { } } if resp.Partition == nil { - resp, err := driver.PartitionCreate(iar) + resp, err := c.driver.PartitionCreate(iar) if err != nil { return err } @@ -297,35 +279,35 @@ func partitionApply(driver *metalgo.Driver) error { continue } - updateResponse, err := driver.PartitionUpdate(iar) + updateResponse, err := c.driver.PartitionUpdate(iar) if err != nil { return err } response = append(response, updateResponse.Partition) } - return detailer.Detail(response) + return output.NewDetailer().Detail(response) } -func partitionDelete(driver *metalgo.Driver, args []string) error { +func (c *config) partitionDelete(args []string) error { if len(args) < 1 { return fmt.Errorf("no partition ID given") } partitionID := args[0] - resp, err := driver.PartitionDelete(partitionID) + resp, err := c.driver.PartitionDelete(partitionID) if err != nil { return err } - return detailer.Detail(resp.Partition) + return output.NewDetailer().Detail(resp.Partition) } -func partitionEdit(driver *metalgo.Driver, args []string) error { +func (c *config) partitionEdit(args []string) error { if len(args) < 1 { return fmt.Errorf("no partition ID given") } partitionID := args[0] getFunc := func(id string) ([]byte, error) { - resp, err := driver.PartitionGet(partitionID) + resp, err := c.driver.PartitionGet(partitionID) if err != nil { return nil, err } @@ -343,11 +325,11 @@ func partitionEdit(driver *metalgo.Driver, args []string) error { if len(iars) != 1 { return fmt.Errorf("partition update error more or less than one partition given:%d", len(iars)) } - uresp, err := driver.PartitionUpdate(iars[0]) + uresp, err := c.driver.PartitionUpdate(iars[0]) if err != nil { return err } - return detailer.Detail(uresp.Partition) + return output.NewDetailer().Detail(uresp.Partition) } return edit(partitionID, getFunc, updateFunc) diff --git a/cmd/project.go b/cmd/project.go index 9d609806..b752e342 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -3,80 +3,77 @@ package cmd import ( "errors" "fmt" - "log" "net/http" v1 "github.com/metal-stack/masterdata-api/api/rest/v1" - metalgo "github.com/metal-stack/metal-go" projectmodel "github.com/metal-stack/metal-go/api/client/project" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - projectCmd = &cobra.Command{ +func newProjectCmd(c *config) *cobra.Command { + projectCmd := &cobra.Command{ Use: "project", Short: "manage projects", Long: "a project groups multiple networks for a tenant", } - projectListCmd = &cobra.Command{ + projectListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all projects", RunE: func(cmd *cobra.Command, args []string) error { - return projectList(driver) + return c.projectList() }, } - projectDescribeCmd = &cobra.Command{ + projectDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a project", RunE: func(cmd *cobra.Command, args []string) error { - return projectDescribe(driver, args) + return c.projectDescribe(args) }, - ValidArgsFunction: projectListCompletionFunc, + ValidArgsFunction: c.comp.ProjectListCompletion, } - projectCreateCmd = &cobra.Command{ + projectCreateCmd := &cobra.Command{ Use: "create", Short: "create a project", RunE: func(cmd *cobra.Command, args []string) error { - return projectCreate() + return c.projectCreate() }, PreRun: bindPFlags, } - projectDeleteCmd = &cobra.Command{ + projectDeleteCmd := &cobra.Command{ Use: "remove ", Aliases: []string{"rm", "delete"}, Short: "delete a project", RunE: func(cmd *cobra.Command, args []string) error { - return projectDelete(args) + return c.projectDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: projectListCompletionFunc, + ValidArgsFunction: c.comp.ProjectListCompletion, } - projectApplyCmd = &cobra.Command{ + projectApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a project", RunE: func(cmd *cobra.Command, args []string) error { - return projectApply() + return c.projectApply() }, PreRun: bindPFlags, } - projectEditCmd = &cobra.Command{ + projectEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a project", RunE: func(cmd *cobra.Command, args []string) error { - return projectEdit(args) + return c.projectEdit(args) }, PreRun: bindPFlags, - ValidArgsFunction: projectListCompletionFunc, + ValidArgsFunction: c.comp.ProjectListCompletion, } -) -func init() { projectCreateCmd.Flags().String("name", "", "name of the project, max 10 characters. [required]") projectCreateCmd.Flags().String("description", "", "description of the project. [required]") projectCreateCmd.Flags().String("tenant", "", "create project for given tenant") @@ -85,10 +82,7 @@ func init() { projectCreateCmd.Flags().Int32("cluster-quota", 0, "cluster quota") projectCreateCmd.Flags().Int32("machine-quota", 0, "machine quota") projectCreateCmd.Flags().Int32("ip-quota", 0, "ip quota") - err := projectCreateCmd.MarkFlagRequired("name") - if err != nil { - log.Fatal(err.Error()) - } + must(projectCreateCmd.MarkFlagRequired("name")) projectApplyCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin. Example project update: @@ -100,10 +94,7 @@ Example project update: ## or via file # cloudctl project apply -f project1.yaml `) - err = projectApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(projectApplyCmd.MarkFlagRequired("file")) projectListCmd.Flags().StringP("name", "", "", "Name of the project.") projectListCmd.Flags().StringP("id", "", "", "ID of the project.") @@ -116,13 +107,12 @@ Example project update: projectCmd.AddCommand(projectApplyCmd) projectCmd.AddCommand(projectEditCmd) - err = viper.BindPFlags(projectListCmd.Flags()) - if err != nil { - log.Fatal(err.Error()) - } + must(viper.BindPFlags(projectListCmd.Flags())) + + return projectCmd } -func projectList(driver *metalgo.Driver) error { +func (c *config) projectList() error { if atLeastOneViperStringFlagGiven("id", "name", "tenant") { pfr := v1.ProjectFindRequest{} id := viper.GetString("id") @@ -138,32 +128,32 @@ func projectList(driver *metalgo.Driver) error { if tenantID != "" { pfr.TenantId = &tenantID } - resp, err := driver.ProjectFind(pfr) + resp, err := c.driver.ProjectFind(pfr) if err != nil { return err } - return printer.Print(resp.Project) + return output.New().Print(resp.Project) } - resp, err := driver.ProjectList() + resp, err := c.driver.ProjectList() if err != nil { return err } - return printer.Print(resp.Project) + return output.New().Print(resp.Project) } -func projectDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) projectDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no project ID given") } projectID := args[0] - resp, err := driver.ProjectGet(projectID) + resp, err := c.driver.ProjectGet(projectID) if err != nil { return err } - return detailer.Detail(resp.Project) + return output.NewDetailer().Detail(resp.Project) } -func projectCreate() error { +func (c *config) projectCreate() error { tenant := viper.GetString("tenant") name := viper.GetString("name") desc := viper.GetString("description") @@ -210,15 +200,15 @@ func projectCreate() error { Project: p, } - response, err := driver.ProjectCreate(pcr) + response, err := c.driver.ProjectCreate(pcr) if err != nil { return err } - return printer.Print(response.Project) + return output.New().Print(response.Project) } -func projectApply() error { +func (c *config) projectApply() error { var pars []v1.Project var par v1.Project err := readFrom(viper.GetString("file"), &par, func(data interface{}) { @@ -234,7 +224,7 @@ func projectApply() error { var response []*models.V1ProjectResponse for _, par := range pars { if par.Meta.Id == "" { - resp, err := driver.ProjectCreate(v1.ProjectCreateRequest{Project: par}) + resp, err := c.driver.ProjectCreate(v1.ProjectCreateRequest{Project: par}) if err != nil { return err } @@ -242,7 +232,7 @@ func projectApply() error { continue } - resp, err := driver.ProjectGet(par.Meta.Id) + resp, err := c.driver.ProjectGet(par.Meta.Id) if err != nil { var r *projectmodel.FindProjectDefault if !errors.As(err, &r) { @@ -253,7 +243,7 @@ func projectApply() error { } } if resp.Project == nil { - resp, err := driver.ProjectCreate(v1.ProjectCreateRequest{Project: par}) + resp, err := c.driver.ProjectCreate(v1.ProjectCreateRequest{Project: par}) if err != nil { return err } @@ -261,23 +251,23 @@ func projectApply() error { continue } - resp, err = driver.ProjectUpdate(v1.ProjectUpdateRequest{Project: par}) + resp, err = c.driver.ProjectUpdate(v1.ProjectUpdateRequest{Project: par}) if err != nil { return err } response = append(response, resp.Project) } - return printer.Print(response) + return output.New().Print(response) } -func projectEdit(args []string) error { +func (c *config) projectEdit(args []string) error { id, err := projectID("edit", args) if err != nil { return err } getFunc := func(id string) ([]byte, error) { - resp, err := driver.ProjectGet(id) + resp, err := c.driver.ProjectGet(id) if err != nil { return nil, err } @@ -295,28 +285,28 @@ func projectEdit(args []string) error { if len(purs) != 1 { return fmt.Errorf("project update error more or less than one project given:%d", len(purs)) } - uresp, err := driver.ProjectUpdate(v1.ProjectUpdateRequest{Project: purs[0]}) + uresp, err := c.driver.ProjectUpdate(v1.ProjectUpdateRequest{Project: purs[0]}) if err != nil { return err } - return printer.Print(uresp.Project) + return output.New().Print(uresp.Project) } return edit(id, getFunc, updateFunc) } -func projectDelete(args []string) error { +func (c *config) projectDelete(args []string) error { id, err := projectID("delete", args) if err != nil { return err } - response, err := driver.ProjectDelete(id) + response, err := c.driver.ProjectDelete(id) if err != nil { return err } - return printer.Print(response.Project) + return output.New().Print(response.Project) } func projectID(verb string, args []string) (string, error) { diff --git a/cmd/root.go b/cmd/root.go index cb685f90..5ad8d8cd 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -4,30 +4,18 @@ import ( "fmt" "log" "os" - "os/user" - "path/filepath" "strings" - "github.com/fatih/color" metalgo "github.com/metal-stack/metal-go" + "github.com/metal-stack/metalctl/cmd/completion" + "github.com/metal-stack/metalctl/pkg/api" "github.com/spf13/cobra" "github.com/spf13/cobra/doc" "github.com/spf13/viper" ) -const ( - cfgFileType = "yaml" - // name of the application, used for help, config location and env config variable names. - programName = "metalctl" -) - var ( - ctx Context - printer Printer - detailer Detailer - driverURL string - driver *metalgo.Driver defaultSSHKeys = [...]string{"id_ed25519", "id_rsa", "id_dsa"} // will bind all viper flags to subcommands and @@ -39,34 +27,12 @@ var ( log.Fatal(err.Error()) } } - - rootCmd = &cobra.Command{ - Use: programName, - Aliases: []string{"m"}, - Short: "a cli to manage metal devices.", - Long: "", - PersistentPreRun: func(cmd *cobra.Command, args []string) { - initPrinter() - }, - SilenceUsage: true, - } - - markdownCmd = &cobra.Command{ - Use: "markdown", - Short: "create markdown documentation", - RunE: func(cmd *cobra.Command, args []string) error { - err := doc.GenMarkdownTree(rootCmd, "./docs") - if err != nil { - return err - } - return nil - }, - } ) // Execute is the entrypoint of the metal-go application func Execute() { - err := rootCmd.Execute() + cmd := newRootCmd() + err := cmd.Execute() if err != nil { if viper.GetBool("debug") { panic(err) @@ -75,9 +41,23 @@ func Execute() { os.Exit(1) } } +func newRootCmd() *cobra.Command { + name := "metalctl" + rootCmd := &cobra.Command{ + Use: name, + Aliases: []string{"m"}, + Short: "a cli to manage metal devices.", + Long: "", + SilenceUsage: true, + } -func init() { - cobra.OnInitialize(initConfig) + markdownCmd := &cobra.Command{ + Use: "markdown", + Short: "create markdown documentation", + RunE: func(cmd *cobra.Command, args []string) error { + return doc.GenMarkdownTree(rootCmd, "./docs") + }, + } rootCmd.PersistentFlags().StringP("config", "c", "", `alternative config file path, (default is ~/.metalctl/config.yaml). Example config.yaml: @@ -104,51 +84,48 @@ metalctl machine list -o template --template "{{ .id }}:{{ .size.id }}" rootCmd.PersistentFlags().Bool("debug", false, "debug output") rootCmd.PersistentFlags().Bool("force-color", false, "force colored output even without tty") - err := rootCmd.RegisterFlagCompletionFunc("output-format", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return outputFormatListCompletion() - }) - if err != nil { - log.Fatal(err.Error()) - } - err = rootCmd.RegisterFlagCompletionFunc("order", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return outputOrderListCompletion() - }) - if err != nil { - log.Fatal(err.Error()) - } - - rootCmd.AddCommand(firmwareCmd) - rootCmd.AddCommand(machineCmd) - rootCmd.AddCommand(firewallCmd) - rootCmd.AddCommand(projectCmd) - rootCmd.AddCommand(sizeCmd) - rootCmd.AddCommand(filesystemLayoutCmd) - rootCmd.AddCommand(imageCmd) - rootCmd.AddCommand(partitionCmd) - rootCmd.AddCommand(switchCmd) - rootCmd.AddCommand(networkCmd) + must(rootCmd.RegisterFlagCompletionFunc("output-format", completion.OutputFormatListCompletion)) + must(rootCmd.RegisterFlagCompletionFunc("order", completion.OutputOrderListCompletion)) + + c := getConfig(name) + + rootCmd.AddCommand(newFirmwareCmd(c)) + rootCmd.AddCommand(newMachineCmd(c)) + rootCmd.AddCommand(newFirewallCmd(c)) + rootCmd.AddCommand(newProjectCmd(c)) + rootCmd.AddCommand(newSizeCmd(c)) + rootCmd.AddCommand(newFilesystemLayoutCmd(c)) + rootCmd.AddCommand(newImageCmd(c)) + rootCmd.AddCommand(newPartitionCmd(c)) + rootCmd.AddCommand(newSwitchCmd(c)) + rootCmd.AddCommand(newNetworkCmd(c)) rootCmd.AddCommand(markdownCmd) - rootCmd.AddCommand(healthCmd) - rootCmd.AddCommand(versionCmd) + rootCmd.AddCommand(newHealthCmd(c)) + rootCmd.AddCommand(newVersionCmd(c)) + rootCmd.AddCommand(newLoginCmd()) + rootCmd.AddCommand(newWhoamiCmd()) + rootCmd.AddCommand(newContextCmd(c)) - rootCmd.AddCommand(loginCmd) - rootCmd.AddCommand(whoamiCmd) - rootCmd.AddCommand(contextCmd) + rootCmd.AddCommand(newUpdateCmd(c.name)) - rootCmd.AddCommand(updateCmd) + must(viper.BindPFlags(rootCmd.PersistentFlags())) - err = viper.BindPFlags(rootCmd.PersistentFlags()) - if err != nil { - log.Fatalf("error setup root cmd:%v", err) - } + return rootCmd +} + +type config struct { + name string + driverURL string + comp *completion.Completion + driver *metalgo.Driver } -func initConfig() { - viper.SetEnvPrefix(strings.ToUpper(programName)) +func getConfig(name string) *config { + viper.SetEnvPrefix(strings.ToUpper(name)) viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) viper.AutomaticEnv() - viper.SetConfigType(cfgFileType) + viper.SetConfigType("yaml") cfgFile := viper.GetString("config") if cfgFile != "" { @@ -158,12 +135,12 @@ func initConfig() { } } else { viper.SetConfigName("config") - viper.AddConfigPath(fmt.Sprintf("/etc/%s", programName)) + viper.AddConfigPath(fmt.Sprintf("/etc/%s", name)) h, err := os.UserHomeDir() if err != nil { log.Printf("unable to figure out user home directory, skipping config lookup path: %v", err) } else { - viper.AddConfigPath(fmt.Sprintf(h+"/.%s", programName)) + viper.AddConfigPath(fmt.Sprintf(h+"/.%s", name)) } viper.AddConfigPath(".") if err := viper.ReadInConfig(); err != nil { @@ -174,8 +151,8 @@ func initConfig() { } } - ctx = mustDefaultContext() - driverURL = viper.GetString("url") + ctx := api.MustDefaultContext() + driverURL := viper.GetString("url") if driverURL == "" && ctx.ApiURL != "" { driverURL = ctx.ApiURL } @@ -196,80 +173,15 @@ func initConfig() { } var err error - driver, err = metalgo.NewDriver(driverURL, apiToken, hmacKey) + driver, err := metalgo.NewDriver(driverURL, apiToken, hmacKey) if err != nil { log.Fatal(err) } -} - -func initPrinter() { - if viper.IsSet("force-color") { - enabled := viper.GetBool("force-color") - if enabled { - color.NoColor = false - } else { - color.NoColor = true - } - } - - var err error - printer, err = NewPrinter( - viper.GetString("output-format"), - viper.GetString("order"), - viper.GetString("template"), - viper.GetBool("no-headers"), - ) - if err != nil { - log.Fatalf("unable to initialize printer:%v", err) - } - detailer, err = NewDetailer(viper.GetString("output-format")) - if err != nil { - log.Fatalf("unable to initialize detailer:%v", err) - } -} - -func searchSSHKey() (string, error) { - currentUser, err := user.Current() - if err != nil { - return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) - } - homeDir := currentUser.HomeDir - defaultDir := filepath.Join(homeDir, "/.ssh/") - var key string - for _, k := range defaultSSHKeys { - possibleKey := filepath.Join(defaultDir, k) - _, err := os.ReadFile(possibleKey) - if err == nil { - fmt.Printf("using SSH identity: %s. Another identity can be specified with --sshidentity/-p\n", - possibleKey) - key = possibleKey - break - } - } - - if key == "" { - return "", fmt.Errorf("failure to locate a SSH identity in default location (%s). "+ - "Another identity can be specified with --sshidentity/-p\n", defaultDir) - } - return key, nil -} - -func readFromFile(filePath string) (string, error) { - currentUser, err := user.Current() - if err != nil { - return "", fmt.Errorf("unable to determine current user for expanding userdata path:%w", err) - } - homeDir := currentUser.HomeDir - if filePath == "~" { - filePath = homeDir - } else if strings.HasPrefix(filePath, "~/") { - filePath = filepath.Join(homeDir, filePath[2:]) - } - - content, err := os.ReadFile(filePath) - if err != nil { - return "", fmt.Errorf("unable to read from given file %s error:%w", filePath, err) + return &config{ + name: name, + comp: completion.NewCompletion(driver), + driver: driver, + driverURL: driverURL, } - return strings.TrimSpace(string(content)), nil } diff --git a/cmd/size.go b/cmd/size.go index c42ef724..d2d53bcb 100644 --- a/cmd/size.go +++ b/cmd/size.go @@ -3,95 +3,92 @@ package cmd import ( "errors" "fmt" - "log" "net/http" "github.com/dustin/go-humanize" metalgo "github.com/metal-stack/metal-go" sizemodel "github.com/metal-stack/metal-go/api/client/size" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - sizeCmd = &cobra.Command{ +func newSizeCmd(c *config) *cobra.Command { + sizeCmd := &cobra.Command{ Use: "size", Short: "manage sizes", Long: "a size is a distinct hardware equipment in terms of cpu cores, ram and storage of a machine.", } - sizeListCmd = &cobra.Command{ + sizeListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all sizes", RunE: func(cmd *cobra.Command, args []string) error { - return sizeList(driver) + return c.sizeList() }, PreRun: bindPFlags, } - sizeDescribeCmd = &cobra.Command{ + sizeDescribeCmd := &cobra.Command{ Use: "describe ", Short: "describe a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeDescribe(driver, args) + return c.sizeDescribe(args) }, - ValidArgsFunction: sizeListCompletionFunc, + ValidArgsFunction: c.comp.SizeListCompletion, } - sizeTryCmd = &cobra.Command{ + sizeTryCmd := &cobra.Command{ Use: "try", Short: "try a specific hardware spec and give the chosen size back", RunE: func(cmd *cobra.Command, args []string) error { - return sizeTry(driver) + return c.sizeTry() }, PreRun: bindPFlags, } - sizeCreateCmd = &cobra.Command{ + sizeCreateCmd := &cobra.Command{ Use: "create", Short: "create a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeCreate(driver) + return c.sizeCreate() }, PreRun: bindPFlags, } - sizeUpdateCmd = &cobra.Command{ + sizeUpdateCmd := &cobra.Command{ Use: "update", Short: "update a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeUpdate(driver) + return c.sizeUpdate() }, PreRun: bindPFlags, } - sizeApplyCmd = &cobra.Command{ + sizeApplyCmd := &cobra.Command{ Use: "apply", Short: "create/update a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeApply(driver) + return c.sizeApply() }, PreRun: bindPFlags, } - sizeDeleteCmd = &cobra.Command{ + sizeDeleteCmd := &cobra.Command{ Use: "delete ", Short: "delete a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeDelete(driver, args) + return c.sizeDelete(args) }, PreRun: bindPFlags, - ValidArgsFunction: sizeListCompletionFunc, + ValidArgsFunction: c.comp.SizeListCompletion, } - sizeEditCmd = &cobra.Command{ + sizeEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a size", RunE: func(cmd *cobra.Command, args []string) error { - return sizeEdit(driver, args) + return c.sizeEdit(args) }, PreRun: bindPFlags, - ValidArgsFunction: sizeListCompletionFunc, + ValidArgsFunction: c.comp.SizeListCompletion, } -) - -func init() { sizeCreateCmd.Flags().StringP("id", "", "", "ID of the size. [required]") sizeCreateCmd.Flags().StringP("name", "n", "", "Name of the size. [optional]") @@ -110,10 +107,7 @@ Example: # cat c1-xlarge-x86.yaml | metalctl size apply -f - ## or via file # metalctl size apply -f c1-xlarge-x86.yaml`) - err := sizeApplyCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(sizeApplyCmd.MarkFlagRequired("file")) sizeUpdateCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin. Example: @@ -124,10 +118,7 @@ Example: # cat c1-xlarge-x86.yaml | metalctl size update -f - ## or via file # metalctl size update -f c1-xlarge-x86.yaml`) - err = sizeUpdateCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(sizeUpdateCmd.MarkFlagRequired("file")) sizeTryCmd.Flags().Int32P("cores", "C", 1, "Cores of the hardware to try") sizeTryCmd.Flags().StringP("memory", "M", "", "Memory of the hardware to try, can be given in bytes or any human readable size spec") @@ -141,29 +132,31 @@ Example: sizeCmd.AddCommand(sizeDeleteCmd) sizeCmd.AddCommand(sizeApplyCmd) sizeCmd.AddCommand(sizeEditCmd) + + return sizeCmd } -func sizeList(driver *metalgo.Driver) error { - resp, err := driver.SizeList() +func (c *config) sizeList() error { + resp, err := c.driver.SizeList() if err != nil { return err } - return printer.Print(resp.Size) + return output.New().Print(resp.Size) } -func sizeDescribe(driver *metalgo.Driver, args []string) error { +func (c *config) sizeDescribe(args []string) error { if len(args) < 1 { return fmt.Errorf("no size ID given") } sizeID := args[0] - resp, err := driver.SizeGet(sizeID) + resp, err := c.driver.SizeGet(sizeID) if err != nil { return err } - return detailer.Detail(resp.Size) + return output.NewDetailer().Detail(resp.Size) } -func sizeTry(driver *metalgo.Driver) error { +func (c *config) sizeTry() error { cores := viper.GetInt32("cores") memory, err := humanize.ParseBytes(viper.GetString("memory")) @@ -175,11 +168,11 @@ func sizeTry(driver *metalgo.Driver) error { return err } - resp, _ := driver.SizeTry(cores, memory, storagesize) - return printer.Print(resp.Logs) + resp, _ := c.driver.SizeTry(cores, memory, storagesize) + return output.New().Print(resp.Logs) } -func sizeCreate(driver *metalgo.Driver) error { +func (c *config) sizeCreate() error { var icrs []metalgo.SizeCreateRequest var icr metalgo.SizeCreateRequest if viper.GetString("file") != "" { @@ -212,14 +205,14 @@ func sizeCreate(driver *metalgo.Driver) error { } } - resp, err := driver.SizeCreate(icr) + resp, err := c.driver.SizeCreate(icr) if err != nil { return err } - return detailer.Detail(resp.Size) + return output.NewDetailer().Detail(resp.Size) } -func sizeUpdate(driver *metalgo.Driver) error { +func (c *config) sizeUpdate() error { icrs, err := readSizeCreateRequests(viper.GetString("file")) if err != nil { return err @@ -227,11 +220,11 @@ func sizeUpdate(driver *metalgo.Driver) error { if len(icrs) != 1 { return fmt.Errorf("size update error more or less than one size given:%d", len(icrs)) } - resp, err := driver.SizeUpdate(icrs[0]) + resp, err := c.driver.SizeUpdate(icrs[0]) if err != nil { return err } - return detailer.Detail(resp.Size) + return output.NewDetailer().Detail(resp.Size) } func readSizeCreateRequests(filename string) ([]metalgo.SizeCreateRequest, error) { @@ -251,7 +244,7 @@ func readSizeCreateRequests(filename string) ([]metalgo.SizeCreateRequest, error } // TODO: General apply method would be useful as these are quite a lot of lines and it's getting erroneous -func sizeApply(driver *metalgo.Driver) error { +func (c *config) sizeApply() error { var iars []metalgo.SizeCreateRequest var iar metalgo.SizeCreateRequest err := readFrom(viper.GetString("file"), &iar, func(data interface{}) { @@ -266,7 +259,7 @@ func sizeApply(driver *metalgo.Driver) error { } var response []*models.V1SizeResponse for _, iar := range iars { - p, err := driver.SizeGet(iar.ID) + p, err := c.driver.SizeGet(iar.ID) if err != nil { var r *sizemodel.FindSizeDefault if !errors.As(err, &r) { @@ -277,7 +270,7 @@ func sizeApply(driver *metalgo.Driver) error { } } if p.Size == nil { - resp, err := driver.SizeCreate(iar) + resp, err := c.driver.SizeCreate(iar) if err != nil { return err } @@ -285,35 +278,35 @@ func sizeApply(driver *metalgo.Driver) error { continue } - resp, err := driver.SizeUpdate(iar) + resp, err := c.driver.SizeUpdate(iar) if err != nil { return err } response = append(response, resp.Size) } - return detailer.Detail(response) + return output.NewDetailer().Detail(response) } -func sizeDelete(driver *metalgo.Driver, args []string) error { +func (c *config) sizeDelete(args []string) error { if len(args) < 1 { return fmt.Errorf("no size ID given") } sizeID := args[0] - resp, err := driver.SizeDelete(sizeID) + resp, err := c.driver.SizeDelete(sizeID) if err != nil { return err } - return detailer.Detail(resp.Size) + return output.NewDetailer().Detail(resp.Size) } -func sizeEdit(driver *metalgo.Driver, args []string) error { +func (c *config) sizeEdit(args []string) error { if len(args) < 1 { return fmt.Errorf("no size ID given") } sizeID := args[0] getFunc := func(id string) ([]byte, error) { - resp, err := driver.SizeGet(sizeID) + resp, err := c.driver.SizeGet(sizeID) if err != nil { return nil, err } @@ -331,11 +324,11 @@ func sizeEdit(driver *metalgo.Driver, args []string) error { if len(iars) != 1 { return fmt.Errorf("size update error more or less than one size given:%d", len(iars)) } - uresp, err := driver.SizeUpdate(iars[0]) + uresp, err := c.driver.SizeUpdate(iars[0]) if err != nil { return err } - return detailer.Detail(uresp.Size) + return output.NewDetailer().Detail(uresp.Size) } return edit(sizeID, getFunc, updateFunc) diff --git a/cmd/switch.go b/cmd/switch.go index 86b03f9f..8c32047b 100644 --- a/cmd/switch.go +++ b/cmd/switch.go @@ -7,63 +7,62 @@ import ( metalgo "github.com/metal-stack/metal-go" "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" "github.com/spf13/cobra" "github.com/spf13/viper" "gopkg.in/yaml.v3" ) -var ( - switchCmd = &cobra.Command{ +func newSwitchCmd(c *config) *cobra.Command { + switchCmd := &cobra.Command{ Use: "switch", Short: "manage switches", } - switchListCmd = &cobra.Command{ + switchListCmd := &cobra.Command{ Use: "list", Aliases: []string{"ls"}, Short: "list all switches", RunE: func(cmd *cobra.Command, args []string) error { - return switchList(driver) + return c.switchList() }, } - switchDetailCmd = &cobra.Command{ + switchDetailCmd := &cobra.Command{ Use: "detail", Short: "switch details", RunE: func(cmd *cobra.Command, args []string) error { - return switchDetail(driver) + return c.switchDetail() }, } - switchUpdateCmd = &cobra.Command{ + switchUpdateCmd := &cobra.Command{ Use: "update", Short: "update a switch", RunE: func(cmd *cobra.Command, args []string) error { - return switchUpdate(driver) + return c.switchUpdate() }, PreRun: bindPFlags, } - switchEditCmd = &cobra.Command{ + switchEditCmd := &cobra.Command{ Use: "edit ", Short: "edit a switch", RunE: func(cmd *cobra.Command, args []string) error { - return switchEdit(driver, args) + return c.switchEdit(args) }, PreRun: bindPFlags, } - switchReplaceCmd = &cobra.Command{ + switchReplaceCmd := &cobra.Command{ Use: "replace ", Short: "puts a switch in replace mode in preparation for physical replacement", RunE: func(cmd *cobra.Command, args []string) error { - return switchReplace(driver, args) + return c.switchReplace(args) }, PreRun: bindPFlags, } -) -func init() { switchCmd.AddCommand(switchListCmd) switchCmd.AddCommand(switchUpdateCmd) switchCmd.AddCommand(switchEditCmd) @@ -71,28 +70,24 @@ func init() { switchCmd.AddCommand(switchReplaceCmd) switchUpdateCmd.Flags().StringP("file", "f", "", `filename of the create or update request in yaml format, or - for stdin.`) - err := switchUpdateCmd.MarkFlagRequired("file") - if err != nil { - log.Fatal(err.Error()) - } + must(switchUpdateCmd.MarkFlagRequired("file")) switchDetailCmd.Flags().StringP("filter", "F", "", "filter for site, rack, ID") - err = viper.BindPFlags(switchDetailCmd.Flags()) - if err != nil { - log.Fatal(err.Error()) - } + must(viper.BindPFlags(switchDetailCmd.Flags())) + + return switchCmd } -func switchList(driver *metalgo.Driver) error { - resp, err := driver.SwitchList() +func (c *config) switchList() error { + resp, err := c.driver.SwitchList() if err != nil { return err } - return printer.Print(resp.Switch) + return output.New().Print(resp.Switch) } -func switchDetail(driver *metalgo.Driver) error { - resp, err := driver.SwitchList() +func (c *config) switchDetail() error { + resp, err := c.driver.SwitchList() if err != nil { return err } @@ -114,29 +109,29 @@ func switchDetail(driver *metalgo.Driver) error { log.Printf("no switch detail for filter: %s", filter) return nil } - return detailer.Detail(result) + return output.NewDetailer().Detail(result) } -func switchUpdate(driver *metalgo.Driver) error { +func (c *config) switchUpdate() error { surs, err := readSwitchUpdateRequests(viper.GetString("file")) if err != nil { return err } - resp, err := driver.SwitchUpdate(surs[0]) + resp, err := c.driver.SwitchUpdate(surs[0]) if err != nil { return err } - return detailer.Detail(resp.Switch) + return output.NewDetailer().Detail(resp.Switch) } -func switchEdit(driver *metalgo.Driver, args []string) error { +func (c *config) switchEdit(args []string) error { if len(args) < 1 { return fmt.Errorf("no switch ID given") } switchID := args[0] getFunc := func(id string) ([]byte, error) { - resp, err := driver.SwitchGet(switchID) + resp, err := c.driver.SwitchGet(switchID) if err != nil { return nil, err } @@ -151,23 +146,23 @@ func switchEdit(driver *metalgo.Driver, args []string) error { if err != nil { return err } - uresp, err := driver.SwitchUpdate(items[0]) + uresp, err := c.driver.SwitchUpdate(items[0]) if err != nil { return err } - return detailer.Detail(uresp.Switch) + return output.NewDetailer().Detail(uresp.Switch) } return edit(switchID, getFunc, updateFunc) } -func switchReplace(driver *metalgo.Driver, args []string) error { +func (c *config) switchReplace(args []string) error { if len(args) < 1 { return fmt.Errorf("no switch ID given") } switchID := args[0] - resp, err := driver.SwitchGet(switchID) + resp, err := c.driver.SwitchGet(switchID) if err != nil { return err } @@ -179,11 +174,11 @@ func switchReplace(driver *metalgo.Driver, args []string) error { RackID: *s.RackID, Mode: "replace", } - uresp, err := driver.SwitchUpdate(sur) + uresp, err := c.driver.SwitchUpdate(sur) if err != nil { return err } - return detailer.Detail(uresp.Switch) + return output.NewDetailer().Detail(uresp.Switch) } func readSwitchUpdateRequests(filename string) ([]metalgo.SwitchUpdateRequest, error) { diff --git a/cmd/update.go b/cmd/update.go index a2b730ef..f1239240 100644 --- a/cmd/update.go +++ b/cmd/update.go @@ -6,36 +6,34 @@ import ( "github.com/metal-stack/updater" ) -var ( - updateCmd = &cobra.Command{ +func newUpdateCmd(name string) *cobra.Command { + updateCmd := &cobra.Command{ Use: "update", Short: "update the program", } - updateCheckCmd = &cobra.Command{ + updateCheckCmd := &cobra.Command{ Use: "check", Short: "check for update of the program", RunE: func(cmd *cobra.Command, args []string) error { - u, err := updater.New("metal-stack", programName, programName) + u, err := updater.New("metal-stack", name, name) if err != nil { return err } return u.Check() }, } - updateDoCmd = &cobra.Command{ + updateDoCmd := &cobra.Command{ Use: "do", Short: "do the update of the program", RunE: func(cmd *cobra.Command, args []string) error { - u, err := updater.New("metal-stack", programName, programName) + u, err := updater.New("metal-stack", name, name) if err != nil { return err } return u.Do() }, } -) - -func init() { updateCmd.AddCommand(updateCheckCmd) updateCmd.AddCommand(updateDoCmd) + return updateCmd } diff --git a/cmd/version.go b/cmd/version.go index 00b5d69f..ceeee359 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,37 +3,35 @@ package cmd import ( "fmt" - metalmodels "github.com/metal-stack/metal-go/api/models" + "github.com/metal-stack/metalctl/cmd/output" + "github.com/metal-stack/metalctl/pkg/api" "github.com/metal-stack/v" "github.com/spf13/cobra" ) -type Version struct { - Client string `yaml:"client"` - Server *metalmodels.RestVersion `yaml:"server,omitempty"` -} - -var versionCmd = &cobra.Command{ - Use: "version", - Short: "print the client and server version information", - Long: "print the client and server version information", - RunE: func(cmd *cobra.Command, args []string) error { - v := Version{ - Client: v.V.String(), - } - - resp, err := driver.VersionGet() - if err == nil { - v.Server = resp.Version - } +func newVersionCmd(c *config) *cobra.Command { + versionCmd := &cobra.Command{ + Use: "version", + Short: "print the client and server version information", + Long: "print the client and server version information", + RunE: func(cmd *cobra.Command, args []string) error { + v := api.Version{ + Client: v.V.String(), + } - if err2 := detailer.Detail(v); err2 != nil { - return err2 - } - if err != nil { - return fmt.Errorf("failed to get server info: %w", err) - } - return nil - }, - PreRun: bindPFlags, + resp, err := c.driver.VersionGet() + if err == nil { + v.Server = resp.Version + } + if err2 := output.NewDetailer().Detail(v); err2 != nil { + return err2 + } + if err != nil { + return fmt.Errorf("failed to get server info: %w", err) + } + return nil + }, + PreRun: bindPFlags, + } + return versionCmd } diff --git a/cmd/whoami.go b/cmd/whoami.go index 6957e814..0ac659f4 100644 --- a/cmd/whoami.go +++ b/cmd/whoami.go @@ -9,40 +9,43 @@ import ( "github.com/spf13/viper" ) -var whoamiCmd = &cobra.Command{ - Use: "whoami", - Short: "shows current user", - Long: "shows the current user, that will be used to authenticate commands.", - RunE: func(cmd *cobra.Command, args []string) error { - - authContext, err := getAuthContext(viper.GetString("kubeconfig")) - if err != nil { - return err - } - - if !authContext.AuthProviderOidc { - return fmt.Errorf("active user %s has no oidc authProvider, check config", authContext.User) - } - - user, parsedClaims, err := sec.ParseTokenUnvalidatedUnfiltered(authContext.IDToken) - if err != nil { - return err - } - - fmt.Printf("UserId: %s\n", user.Name) - if user.Tenant != "" { - fmt.Printf("Tenant: %s\n", user.Tenant) - } - if user.Issuer != "" { - fmt.Printf("Issuer: %s\n", user.Issuer) - } - fmt.Printf("Groups:\n") - for _, g := range user.Groups { - fmt.Printf(" %s\n", g) - } - fmt.Printf("Expires at %s\n", time.Unix(parsedClaims.ExpiresAt, 0).Format("Mon Jan 2 15:04:05 MST 2006")) - - return nil - }, - PreRun: bindPFlags, +func newWhoamiCmd() *cobra.Command { + whoamiCmd := &cobra.Command{ + Use: "whoami", + Short: "shows current user", + Long: "shows the current user, that will be used to authenticate commands.", + RunE: func(cmd *cobra.Command, args []string) error { + + authContext, err := getAuthContext(viper.GetString("kubeconfig")) + if err != nil { + return err + } + + if !authContext.AuthProviderOidc { + return fmt.Errorf("active user %s has no oidc authProvider, check config", authContext.User) + } + + user, parsedClaims, err := sec.ParseTokenUnvalidatedUnfiltered(authContext.IDToken) + if err != nil { + return err + } + + fmt.Printf("UserId: %s\n", user.Name) + if user.Tenant != "" { + fmt.Printf("Tenant: %s\n", user.Tenant) + } + if user.Issuer != "" { + fmt.Printf("Issuer: %s\n", user.Issuer) + } + fmt.Printf("Groups:\n") + for _, g := range user.Groups { + fmt.Printf(" %s\n", g) + } + fmt.Printf("Expires at %s\n", time.Unix(parsedClaims.ExpiresAt, 0).Format("Mon Jan 2 15:04:05 MST 2006")) + + return nil + }, + PreRun: bindPFlags, + } + return whoamiCmd } diff --git a/docker-make.yml b/docker-make.yml index b170450d..1b469646 100644 --- a/docker-make.yml +++ b/docker-make.yml @@ -11,9 +11,11 @@ after: - mv tmp/bin/metalctl-linux-amd64 result - mv tmp/bin/metalctl-windows-amd64 result - mv tmp/bin/metalctl-darwin-amd64 result + - mv tmp/bin/metalctl-darwin-arm64 result - md5sum result/metalctl-linux-amd64 > result/metalctl-linux-amd64.md5 - md5sum result/metalctl-windows-amd64 > result/metalctl-windows-amd64.md5 - md5sum result/metalctl-darwin-amd64 > result/metalctl-darwin-amd64.md5 + - md5sum result/metalctl-darwin-arm64 > result/metalctl-darwin-arm64.md5 builds: - name: metalctl-slug tags: diff --git a/pkg/api/auth.go b/pkg/api/auth.go new file mode 100644 index 00000000..e8fdcb2f --- /dev/null +++ b/pkg/api/auth.go @@ -0,0 +1,36 @@ +package api + +import ( + "fmt" + + "github.com/metal-stack/metal-lib/auth" +) + +const CloudContext = "metalctl" + +// getAuthContext reads AuthContext from given kubeconfig +func GetAuthContext(kubeconfig string) (*auth.AuthContext, error) { + cs, err := GetContexts() + if err != nil { + return nil, err + } + authContext, err := auth.GetAuthContext(kubeconfig, FormatContextName(CloudContext, cs.CurrentContext)) + if err != nil { + return nil, err + } + + if !authContext.AuthProviderOidc { + return nil, fmt.Errorf("active user %s has no oidc authProvider, check config", authContext.User) + } + + return &authContext, nil +} + +// formatContextName returns the contextName for the given suffix. suffix can be empty. +func FormatContextName(prefix string, suffix string) string { + contextName := prefix + if suffix != "" { + contextName = fmt.Sprintf("%s-%s", CloudContext, suffix) + } + return contextName +} diff --git a/pkg/api/context.go b/pkg/api/context.go new file mode 100644 index 00000000..722087b9 --- /dev/null +++ b/pkg/api/context.go @@ -0,0 +1,70 @@ +package api + +import ( + "fmt" + "os" + + "github.com/fatih/color" + "github.com/spf13/viper" + "gopkg.in/yaml.v3" +) + +// Contexts contains all configuration contexts of metalctl +type Contexts struct { + CurrentContext string `yaml:"current"` + PreviousContext string `yaml:"previous"` + Contexts map[string]Context +} + +// Context configure metalctl behaviour +type Context struct { + ApiURL string `yaml:"url"` + IssuerURL string `yaml:"issuer_url"` + IssuerType string `yaml:"issuer_type"` + CustomScopes string `yaml:"custom_scopes"` + ClientID string `yaml:"client_id"` + ClientSecret string `yaml:"client_secret"` + HMAC *string `yaml:"hmac"` +} + +var defaultCtx = Context{ + ApiURL: "http://localhost:8080/cloud", + IssuerURL: "http://localhost:8080/", +} + +func GetContexts() (*Contexts, error) { + var ctxs Contexts + cfgFile := viper.GetViper().ConfigFileUsed() + c, err := os.ReadFile(cfgFile) + if err != nil { + return nil, fmt.Errorf("unable to read config, please create a config.yaml in either: /etc/metalctl/, $HOME/.metalctl/ or in the current directory, see metalctl ctx -h for examples") + } + err = yaml.Unmarshal(c, &ctxs) + return &ctxs, err +} + +func WriteContexts(ctxs *Contexts) error { + c, err := yaml.Marshal(ctxs) + if err != nil { + return err + } + cfgFile := viper.GetViper().ConfigFileUsed() + err = os.WriteFile(cfgFile, c, 0600) + if err != nil { + return err + } + fmt.Printf("%s switched context to \"%s\"\n", color.GreenString("✔"), color.GreenString(ctxs.CurrentContext)) + return nil +} + +func MustDefaultContext() Context { + ctxs, err := GetContexts() + if err != nil { + return defaultCtx + } + ctx, ok := ctxs.Contexts[ctxs.CurrentContext] + if !ok { + return defaultCtx + } + return ctx +} diff --git a/cmd/issue.go b/pkg/api/issue.go similarity index 99% rename from cmd/issue.go rename to pkg/api/issue.go index c2d4540b..8cfe2a19 100644 --- a/cmd/issue.go +++ b/pkg/api/issue.go @@ -1,4 +1,4 @@ -package cmd +package api import ( "fmt" @@ -26,6 +26,8 @@ type ( ) var ( + circle = "↻" + IssueNoPartition = Issue{ ShortName: "no-partition", Description: "machine with no partition", diff --git a/pkg/api/version.go b/pkg/api/version.go new file mode 100644 index 00000000..94bc0e6c --- /dev/null +++ b/pkg/api/version.go @@ -0,0 +1,10 @@ +package api + +import ( + metalmodels "github.com/metal-stack/metal-go/api/models" +) + +type Version struct { + Client string `yaml:"client"` + Server *metalmodels.RestVersion `yaml:"server,omitempty"` +}