diff --git a/Makefile b/Makefile index ca83fa4..0aaa425 100644 --- a/Makefile +++ b/Makefile @@ -37,4 +37,4 @@ testenv: .PHONY: mocks mocks: - docker run --user $$(id -u):$$(id -g) --rm -w /work -v ${PWD}:/work vektra/mockery:v2.14.0 --name testClient --dir /work/pkg/genericcli --output /work/pkg/genericcli --filename generic_mock_test.go --testonly --inpackage + docker run --user $$(id -u):$$(id -g) --rm -w /work -v ${PWD}:/work vektra/mockery:v2.45.1 --name testClient --dir /work/pkg/genericcli --output /work/pkg/genericcli --filename generic_mock_test.go --testonly --inpackage diff --git a/pkg/genericcli/cmds.go b/pkg/genericcli/cmds.go index 7774201..2a53697 100644 --- a/pkg/genericcli/cmds.go +++ b/pkg/genericcli/cmds.go @@ -1,6 +1,7 @@ package genericcli import ( + "errors" "fmt" "io" "strings" @@ -47,7 +48,10 @@ func OnlyCmds(cmds ...DefaultCmd) map[DefaultCmd]bool { // CmdsConfig provides the configuration for the default commands. type CmdsConfig[C any, U any, R any] struct { + // GenericCLI is the generic CLI used by the cobra commands. this uses only single positional arguments. if you have multiple, use multi arg generic cli. GenericCLI *GenericCLI[C, U, R] + // MultiArgGenericCLI is the generic CLI used by the cobra commands. this can use n positional arguments. + MultiArgGenericCLI *MultiArgGenericCLI[C, U, R] // OnlyCmds defines which default commands to include from the generic cli. if empty, all default commands will be added. OnlyCmds map[DefaultCmd]bool @@ -61,6 +65,9 @@ type CmdsConfig[C any, U any, R any] struct { // Aliases provides additional aliases for the root cmd. Aliases []string + // Args defines how many arguments are being used for the entity's id and how they are named, this defaults to ["id"] + Args []string + // DescribePrinter is the printer that is used for describing the entity. It's a function because printers potentially get initialized later in the game. DescribePrinter func() printers.Printer // ListPrinter is the printer that is used for listing multiple entities. It's a function because printers potentially get initialized later in the game. @@ -98,8 +105,14 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob if len(c.OnlyCmds) == 0 { c.OnlyCmds = allCmds() } + if len(c.Args) == 0 { + c.Args = []string{"id"} + } + if c.GenericCLI != nil { + c.MultiArgGenericCLI = c.GenericCLI.multiCLI + } if c.Sorter != nil { - c.GenericCLI = c.GenericCLI.WithSorter(c.Sorter) + c.MultiArgGenericCLI = c.MultiArgGenericCLI.WithSorter(c.Sorter) } Must(c.validate()) @@ -124,7 +137,7 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob return err } - return c.GenericCLI.ListAndPrint(c.ListPrinter(), sortKeys...) + return c.MultiArgGenericCLI.ListAndPrint(c.ListPrinter(), sortKeys...) }, } @@ -140,17 +153,22 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob } if _, ok := c.OnlyCmds[DescribeCmd]; ok { + use := "describe" + for _, arg := range c.Args { + use += fmt.Sprintf(" <%s>", arg) + } + cmd := &cobra.Command{ - Use: "describe ", + Use: use, Aliases: []string{"get"}, Short: fmt.Sprintf("describes the %s", c.Singular), RunE: func(cmd *cobra.Command, args []string) error { - id, err := GetExactlyOneArg(args) + id, err := GetExactlyNArgs(len(c.Args), args) if err != nil { return err } - return c.GenericCLI.DescribeAndPrint(id, c.DescribePrinter()) + return c.MultiArgGenericCLI.DescribeAndPrint(c.DescribePrinter(), id...) }, ValidArgsFunction: c.ValidArgsFn, } @@ -173,12 +191,12 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob return err } - return c.GenericCLI.CreateAndPrint(rq, c.DescribePrinter()) + return c.MultiArgGenericCLI.CreateAndPrint(rq, c.DescribePrinter()) } p := c.evalBulkFlags() - return c.GenericCLI.CreateFromFileAndPrint(viper.GetString("file"), p()) + return c.MultiArgGenericCLI.CreateFromFileAndPrint(viper.GetString("file"), p()) }, } @@ -192,8 +210,15 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob } if _, ok := c.OnlyCmds[UpdateCmd]; ok { + use := "update" + if c.UpdateRequestFromCLI != nil { + for _, arg := range c.Args { + use += fmt.Sprintf(" <%s>", arg) + } + } + cmd := &cobra.Command{ - Use: "update", + Use: use, Short: fmt.Sprintf("updates the %s", c.Singular), RunE: func(cmd *cobra.Command, args []string) error { if c.UpdateRequestFromCLI != nil && !viper.IsSet("file") { @@ -202,12 +227,12 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob return err } - return c.GenericCLI.UpdateAndPrint(rq, c.DescribePrinter()) + return c.MultiArgGenericCLI.UpdateAndPrint(rq, c.DescribePrinter()) } p := c.evalBulkFlags() - return c.GenericCLI.UpdateFromFileAndPrint(viper.GetString("file"), p()) + return c.MultiArgGenericCLI.UpdateFromFileAndPrint(viper.GetString("file"), p()) }, ValidArgsFunction: c.ValidArgsFn, } @@ -222,23 +247,28 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob } if _, ok := c.OnlyCmds[DeleteCmd]; ok { + use := "delete" + for _, arg := range c.Args { + use += fmt.Sprintf(" <%s>", arg) + } + cmd := &cobra.Command{ - Use: "delete ", + Use: use, Short: fmt.Sprintf("deletes the %s", c.Singular), Aliases: []string{"destroy", "rm", "remove"}, RunE: func(cmd *cobra.Command, args []string) error { if !viper.IsSet("file") { - id, err := GetExactlyOneArg(args) + id, err := GetExactlyNArgs(len(c.Args), args) if err != nil { return err } - return c.GenericCLI.DeleteAndPrint(id, c.DescribePrinter()) + return c.MultiArgGenericCLI.DeleteAndPrint(c.DescribePrinter(), id...) } p := c.evalBulkFlags() - return c.GenericCLI.DeleteFromFileAndPrint(viper.GetString("file"), p()) + return c.MultiArgGenericCLI.DeleteFromFileAndPrint(viper.GetString("file"), p()) }, ValidArgsFunction: c.ValidArgsFn, } @@ -258,12 +288,12 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob Short: fmt.Sprintf("applies one or more %s from a given file", c.Plural), RunE: func(cmd *cobra.Command, args []string) error { if !viper.GetBool("skip-security-prompts") { - c.GenericCLI = c.GenericCLI.WithBulkSecurityPrompt(c.In, c.Out) + c.MultiArgGenericCLI = c.MultiArgGenericCLI.WithBulkSecurityPrompt(c.In, c.Out) } p := c.evalBulkFlags() - return c.GenericCLI.ApplyFromFileAndPrint(viper.GetString("file"), p()) + return c.MultiArgGenericCLI.ApplyFromFileAndPrint(viper.GetString("file"), p()) }, } @@ -278,11 +308,16 @@ func NewCmds[C any, U any, R any](c *CmdsConfig[C, U, R], additionalCmds ...*cob } if _, ok := c.OnlyCmds[EditCmd]; ok { + use := "edit" + for _, arg := range c.Args { + use += fmt.Sprintf(" <%s>", arg) + } + cmd := &cobra.Command{ - Use: "edit ", + Use: use, Short: fmt.Sprintf("edit the %s through an editor and update", c.Singular), RunE: func(cmd *cobra.Command, args []string) error { - return c.GenericCLI.EditAndPrint(args, c.DescribePrinter()) + return c.MultiArgGenericCLI.EditAndPrint(len(c.Args), args, c.DescribePrinter()) }, ValidArgsFunction: c.ValidArgsFn, } @@ -345,7 +380,7 @@ func (c *CmdsConfig[C, U, R]) addFileFlags(cmd *cobra.Command) { } func (c *CmdsConfig[C, U, R]) validate() error { - if c.GenericCLI == nil { + if c.MultiArgGenericCLI == nil { return fmt.Errorf("generic cli must not be nil, command: %s", c.Singular) } if c.DescribePrinter == nil { @@ -369,23 +404,26 @@ func (c *CmdsConfig[C, U, R]) validate() error { if c.Description == "" { return fmt.Errorf("description must not be empty, command: %s", c.Singular) } + if len(c.Args) < 1 { + return errors.New("at least one arg for id is required") + } return nil } func (c *CmdsConfig[C, U, R]) evalBulkFlags() func() printers.Printer { if !viper.GetBool("skip-security-prompts") { - c.GenericCLI = c.GenericCLI.WithBulkSecurityPrompt(c.In, c.Out) + c.MultiArgGenericCLI = c.MultiArgGenericCLI.WithBulkSecurityPrompt(c.In, c.Out) } if viper.GetBool("timestamps") { - c.GenericCLI = c.GenericCLI.WithTimestamps() + c.MultiArgGenericCLI = c.MultiArgGenericCLI.WithTimestamps() } p := c.DescribePrinter if viper.GetBool("bulk-output") { p = c.ListPrinter - c.GenericCLI = c.GenericCLI.WithBulkPrint() + c.MultiArgGenericCLI = c.MultiArgGenericCLI.WithBulkPrint() } return p diff --git a/pkg/genericcli/crud.go b/pkg/genericcli/crud.go index d71e993..a179e4b 100644 --- a/pkg/genericcli/crud.go +++ b/pkg/genericcli/crud.go @@ -5,6 +5,7 @@ import ( "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/multisort" + "github.com/metal-stack/metal-lib/pkg/pointer" ) func GetExactlyOneArg(args []string) (string, error) { @@ -18,7 +19,22 @@ func GetExactlyOneArg(args []string) (string, error) { } } -func (a *GenericCLI[C, U, R]) List(sortKeys ...multisort.Key) ([]R, error) { +func GetExactlyNArgs(n int, args []string) ([]string, error) { + switch { + case n == 1: + arg, err := GetExactlyOneArg(args) + if err != nil { + return nil, err + } + return pointer.WrapInSlice(arg), nil + case len(args) == n: + return args, nil + default: + return nil, fmt.Errorf("%d positional args are required, %d were provided", n, len(args)) + } +} + +func (a *MultiArgGenericCLI[C, U, R]) List(sortKeys ...multisort.Key) ([]R, error) { resp, err := a.crud.List() if err != nil { return nil, err @@ -33,7 +49,7 @@ func (a *GenericCLI[C, U, R]) List(sortKeys ...multisort.Key) ([]R, error) { return resp, nil } -func (a *GenericCLI[C, U, R]) ListAndPrint(p printers.Printer, sortKeys ...multisort.Key) error { +func (a *MultiArgGenericCLI[C, U, R]) ListAndPrint(p printers.Printer, sortKeys ...multisort.Key) error { resp, err := a.List(sortKeys...) if err != nil { return err @@ -42,10 +58,10 @@ func (a *GenericCLI[C, U, R]) ListAndPrint(p printers.Printer, sortKeys ...multi return p.Print(resp) } -func (a *GenericCLI[C, U, R]) Describe(id string) (R, error) { +func (a *MultiArgGenericCLI[C, U, R]) Describe(id ...string) (R, error) { var zero R - resp, err := a.crud.Get(id) + resp, err := a.crud.Get(id...) if err != nil { return zero, err } @@ -53,8 +69,8 @@ func (a *GenericCLI[C, U, R]) Describe(id string) (R, error) { return resp, nil } -func (a *GenericCLI[C, U, R]) DescribeAndPrint(id string, p printers.Printer) error { - resp, err := a.Describe(id) +func (a *MultiArgGenericCLI[C, U, R]) DescribeAndPrint(p printers.Printer, id ...string) error { + resp, err := a.Describe(id...) if err != nil { return err } @@ -62,10 +78,10 @@ func (a *GenericCLI[C, U, R]) DescribeAndPrint(id string, p printers.Printer) er return p.Print(resp) } -func (a *GenericCLI[C, U, R]) Delete(id string) (R, error) { +func (a *MultiArgGenericCLI[C, U, R]) Delete(id ...string) (R, error) { var zero R - resp, err := a.crud.Delete(id) + resp, err := a.crud.Delete(id...) if err != nil { return zero, err } @@ -73,8 +89,8 @@ func (a *GenericCLI[C, U, R]) Delete(id string) (R, error) { return resp, nil } -func (a *GenericCLI[C, U, R]) DeleteAndPrint(id string, p printers.Printer) error { - resp, err := a.Delete(id) +func (a *MultiArgGenericCLI[C, U, R]) DeleteAndPrint(p printers.Printer, id ...string) error { + resp, err := a.Delete(id...) if err != nil { return err } @@ -82,7 +98,7 @@ func (a *GenericCLI[C, U, R]) DeleteAndPrint(id string, p printers.Printer) erro return p.Print(resp) } -func (a *GenericCLI[C, U, R]) Create(rq C) (R, error) { +func (a *MultiArgGenericCLI[C, U, R]) Create(rq C) (R, error) { var zero R resp, err := a.crud.Create(rq) @@ -93,7 +109,7 @@ func (a *GenericCLI[C, U, R]) Create(rq C) (R, error) { return resp, nil } -func (a *GenericCLI[C, U, R]) CreateAndPrint(rq C, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) CreateAndPrint(rq C, p printers.Printer) error { resp, err := a.Create(rq) if err != nil { return err @@ -102,7 +118,7 @@ func (a *GenericCLI[C, U, R]) CreateAndPrint(rq C, p printers.Printer) error { return p.Print(resp) } -func (a *GenericCLI[C, U, R]) Update(rq U) (R, error) { +func (a *MultiArgGenericCLI[C, U, R]) Update(rq U) (R, error) { var zero R resp, err := a.crud.Update(rq) @@ -113,7 +129,7 @@ func (a *GenericCLI[C, U, R]) Update(rq U) (R, error) { return resp, nil } -func (a *GenericCLI[C, U, R]) UpdateAndPrint(rq U, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) UpdateAndPrint(rq U, p printers.Printer) error { resp, err := a.Update(rq) if err != nil { return err diff --git a/pkg/genericcli/edit.go b/pkg/genericcli/edit.go index ce52d44..16d0078 100644 --- a/pkg/genericcli/edit.go +++ b/pkg/genericcli/edit.go @@ -10,10 +10,10 @@ import ( "sigs.k8s.io/yaml" ) -func (a *GenericCLI[C, U, R]) Edit(args []string) (R, error) { +func (a *MultiArgGenericCLI[C, U, R]) Edit(n int, args []string) (R, error) { var zero R - id, err := GetExactlyOneArg(args) + id, err := GetExactlyNArgs(n, args) if err != nil { return zero, err } @@ -31,7 +31,7 @@ func (a *GenericCLI[C, U, R]) Edit(args []string) (R, error) { _ = a.fs.Remove(tmpfile.Name()) }() - doc, err := a.crud.Get(id) + doc, err := a.crud.Get(id...) if err != nil { return zero, err } @@ -88,8 +88,8 @@ func (a *GenericCLI[C, U, R]) Edit(args []string) (R, error) { return result, nil } -func (a *GenericCLI[C, U, R]) EditAndPrint(args []string, p printers.Printer) error { - result, err := a.Edit(args) +func (a *MultiArgGenericCLI[C, U, R]) EditAndPrint(n int, args []string, p printers.Printer) error { + result, err := a.Edit(n, args) if err != nil { return err } diff --git a/pkg/genericcli/fromfile.go b/pkg/genericcli/fromfile.go index a1716a7..197723c 100644 --- a/pkg/genericcli/fromfile.go +++ b/pkg/genericcli/fromfile.go @@ -96,7 +96,7 @@ func (ms BulkResults[R]) ToError(joinErrors bool) error { // // As this function uses response entities, it is possible that create and update entity representation // is inaccurate to a certain degree. -func (a *GenericCLI[C, U, R]) CreateFromFile(from string) (BulkResults[R], error) { +func (a *MultiArgGenericCLI[C, U, R]) CreateFromFile(from string) (BulkResults[R], error) { return a.multiOperation(&multiOperationArgs[C, U, R]{ from: from, op: multiOperationCreate[C, U, R]{}, @@ -104,7 +104,7 @@ func (a *GenericCLI[C, U, R]) CreateFromFile(from string) (BulkResults[R], error }) } -func (a *GenericCLI[C, U, R]) CreateFromFileAndPrint(from string, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) CreateFromFileAndPrint(from string, p printers.Printer) error { return a.multiOperationPrint(from, p, multiOperationCreate[C, U, R]{}) } @@ -112,7 +112,7 @@ func (a *GenericCLI[C, U, R]) CreateFromFileAndPrint(from string, p printers.Pri // // As this function uses response entities, it is possible that create and update entity representation // is inaccurate to a certain degree. -func (a *GenericCLI[C, U, R]) UpdateFromFile(from string) (BulkResults[R], error) { +func (a *MultiArgGenericCLI[C, U, R]) UpdateFromFile(from string) (BulkResults[R], error) { return a.multiOperation(&multiOperationArgs[C, U, R]{ from: from, op: multiOperationUpdate[C, U, R]{}, @@ -120,7 +120,7 @@ func (a *GenericCLI[C, U, R]) UpdateFromFile(from string) (BulkResults[R], error }) } -func (a *GenericCLI[C, U, R]) UpdateFromFileAndPrint(from string, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) UpdateFromFileAndPrint(from string, p printers.Printer) error { return a.multiOperationPrint(from, p, multiOperationUpdate[C, U, R]{}) } @@ -129,7 +129,7 @@ func (a *GenericCLI[C, U, R]) UpdateFromFileAndPrint(from string, p printers.Pri // // As this function uses response entities, it is possible that create and update entity representation // is inaccurate to a certain degree. -func (a *GenericCLI[C, U, R]) ApplyFromFile(from string) (BulkResults[R], error) { +func (a *MultiArgGenericCLI[C, U, R]) ApplyFromFile(from string) (BulkResults[R], error) { return a.multiOperation(&multiOperationArgs[C, U, R]{ from: from, op: multiOperationApply[C, U, R]{}, @@ -137,7 +137,7 @@ func (a *GenericCLI[C, U, R]) ApplyFromFile(from string) (BulkResults[R], error) }) } -func (a *GenericCLI[C, U, R]) ApplyFromFileAndPrint(from string, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) ApplyFromFileAndPrint(from string, p printers.Printer) error { return a.multiOperationPrint(from, p, multiOperationApply[C, U, R]{}) } @@ -145,7 +145,7 @@ func (a *GenericCLI[C, U, R]) ApplyFromFileAndPrint(from string, p printers.Prin // // As this function uses response entities, it is possible that create and update entity representation // is inaccurate to a certain degree. -func (a *GenericCLI[C, U, R]) DeleteFromFile(from string) (BulkResults[R], error) { +func (a *MultiArgGenericCLI[C, U, R]) DeleteFromFile(from string) (BulkResults[R], error) { return a.multiOperation(&multiOperationArgs[C, U, R]{ from: from, op: multiOperationDelete[C, U, R]{}, @@ -153,13 +153,13 @@ func (a *GenericCLI[C, U, R]) DeleteFromFile(from string) (BulkResults[R], error }) } -func (a *GenericCLI[C, U, R]) DeleteFromFileAndPrint(from string, p printers.Printer) error { +func (a *MultiArgGenericCLI[C, U, R]) DeleteFromFileAndPrint(from string, p printers.Printer) error { return a.multiOperationPrint(from, p, multiOperationDelete[C, U, R]{}) } type ( multiOperation[C any, U any, R any] interface { - do(crud CRUD[C, U, R], doc R) BulkResult[R] + do(crud MultiArgCRUD[C, U, R], doc R) BulkResult[R] verb() string } multiOperationCreate[C any, U any, R any] struct{} @@ -200,7 +200,7 @@ func bulkPrintCallback[R any](p printers.Printer) func(BulkResults[R]) error { } } -func (a *GenericCLI[C, U, R]) securityPromptCallback(c *PromptConfig, op multiOperation[C, U, R]) func(R) error { +func (a *MultiArgGenericCLI[C, U, R]) securityPromptCallback(c *PromptConfig, op multiOperation[C, U, R]) func(R) error { return func(r R) error { id, _, _, err := a.Interface().Convert(r) if err != nil { @@ -213,9 +213,6 @@ func (a *GenericCLI[C, U, R]) securityPromptCallback(c *PromptConfig, op multiOp } colored := PrintColoredYAML(raw) - if err != nil { - return err - } c.Message = fmt.Sprintf("%s %q, continue?\n\n%s\n\n", op.verb(), id, colored) @@ -223,7 +220,7 @@ func (a *GenericCLI[C, U, R]) securityPromptCallback(c *PromptConfig, op multiOp } } -func (a *GenericCLI[C, U, R]) multiOperationPrint(from string, p printers.Printer, op multiOperation[C, U, R]) error { +func (a *MultiArgGenericCLI[C, U, R]) multiOperationPrint(from string, p printers.Printer, op multiOperation[C, U, R]) error { var ( beforeCallbacks []func(R) error afterCallbacks []func(BulkResult[R]) error @@ -275,7 +272,7 @@ func (a *GenericCLI[C, U, R]) multiOperationPrint(from string, p printers.Printe return err } -func (a *GenericCLI[C, U, R]) multiOperation(args *multiOperationArgs[C, U, R]) (results BulkResults[R], err error) { +func (a *MultiArgGenericCLI[C, U, R]) multiOperation(args *multiOperationArgs[C, U, R]) (results BulkResults[R], err error) { var ( callbackErr = func(err error) (BulkResults[R], error) { bulkErr := results.ToError(args.joinErrors) @@ -339,7 +336,7 @@ func (m multiOperationCreate[C, U, R]) verb() string { //nolint:unused return "creating" } -func (m multiOperationCreate[C, U, R]) do(crud CRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused +func (m multiOperationCreate[C, U, R]) do(crud MultiArgCRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused _, createDoc, _, err := crud.Convert(doc) if err != nil { return BulkResult[R]{Action: BulkErrorOnCreate, Error: fmt.Errorf("error converting to create entity: %w", err)} @@ -357,7 +354,7 @@ func (m multiOperationUpdate[C, U, R]) verb() string { //nolint:unused return "updating" } -func (m multiOperationUpdate[C, U, R]) do(crud CRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused +func (m multiOperationUpdate[C, U, R]) do(crud MultiArgCRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused _, _, updateDoc, err := crud.Convert(doc) if err != nil { return BulkResult[R]{Action: BulkErrorOnUpdate, Error: fmt.Errorf("error converting to update entity: %w", err)} @@ -375,7 +372,7 @@ func (m multiOperationApply[C, U, R]) verb() string { //nolint:unused return "applying" } -func (m multiOperationApply[C, U, R]) do(crud CRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused +func (m multiOperationApply[C, U, R]) do(crud MultiArgCRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused _, createDoc, _, err := crud.Convert(doc) if err != nil { return BulkResult[R]{Action: BulkErrorOnCreate, Error: fmt.Errorf("error converting to create entity: %w", err)} @@ -398,13 +395,13 @@ func (m multiOperationDelete[C, U, R]) verb() string { //nolint:unused return "deleting" } -func (m multiOperationDelete[C, U, R]) do(crud CRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused +func (m multiOperationDelete[C, U, R]) do(crud MultiArgCRUD[C, U, R], doc R) BulkResult[R] { //nolint:unused id, _, _, err := crud.Convert(doc) if err != nil { return BulkResult[R]{Action: BulkErrorOnDelete, Error: fmt.Errorf("error retrieving id from response entity: %w", err)} } - result, err := crud.Delete(id) + result, err := crud.Delete(id...) if err != nil { return BulkResult[R]{Action: BulkErrorOnDelete, Error: fmt.Errorf("error deleting entity: %w", err)} } diff --git a/pkg/genericcli/fromfile_test.go b/pkg/genericcli/fromfile_test.go index 6b4564e..93f03c7 100644 --- a/pkg/genericcli/fromfile_test.go +++ b/pkg/genericcli/fromfile_test.go @@ -269,11 +269,11 @@ error creating entity: creation error for id 1 } } -func newMockCLI(t *testing.T, mockFn func(mock *mockTestClient), fileMockFn func(fs afero.Fs)) *GenericCLI[*testCreate, *testUpdate, *testResponse] { +func newMockCLI(t *testing.T, mockFn func(mock *mockTestClient), fileMockFn func(fs afero.Fs)) *MultiArgGenericCLI[*testCreate, *testUpdate, *testResponse] { client := newMockTestClient(t) fs := afero.NewMemMapFs() - cli := GenericCLI[*testCreate, *testUpdate, *testResponse]{ + cli := MultiArgGenericCLI[*testCreate, *testUpdate, *testResponse]{ crud: testCRUD{client: client}, fs: fs, parser: MultiDocumentYAML[*testResponse]{fs: fs}, diff --git a/pkg/genericcli/generic-multi-arg.go b/pkg/genericcli/generic-multi-arg.go new file mode 100644 index 0000000..f06c2c5 --- /dev/null +++ b/pkg/genericcli/generic-multi-arg.go @@ -0,0 +1,103 @@ +package genericcli + +import ( + "io" + + "github.com/metal-stack/metal-lib/pkg/multisort" + "github.com/spf13/afero" +) + +// MultiArgGenericCLI can be used to gain generic CLI functionality. +// +// C is the create request for an entity. +// U is the update request for an entity. +// R is the response object of an entity. +type MultiArgGenericCLI[C any, U any, R any] struct { + fs afero.Fs + crud MultiArgCRUD[C, U, R] + parser MultiDocumentYAML[R] + sorter *multisort.Sorter[R] + + bulkPrint bool + bulkSecurityPrompt *PromptConfig + timestamps bool +} + +// MultiArgCRUD must be implemented in order to get generic CLI functionality. +// +// C is the create request for an entity. +// U is the update request for an entity. +// R is the response object of an entity. +type MultiArgCRUD[C any, U any, R any] interface { + // Get returns the entity with the given id. It can be that multiple ids are passed in case the id is a compound key. + Get(id ...string) (R, error) + // List returns a slice of entities. + List() ([]R, error) + // Create tries to create the entity with the given request and returns the created entity. + Create(rq C) (R, error) + // Update tries to update the entity with the given request and returns the updated entity. + Update(rq U) (R, error) + // Delete tries to delete the entity with the given id and returns the deleted entity. It can be that multiple ids are passed in case the id is a compound key. + Delete(id ...string) (R, error) + // Convert converts an entity's response object to best possible create and update requests and additionally returns the entities ID. + // This is required for capabilities like creation/update/deletion from a file of response objects. + Convert(r R) ([]string, C, U, error) +} + +// NewGenericMultiArgCLI returns a new generic cli. +// +// C is the create request for an entity. +// U is the update request for an entity. +// R is the response object of an entity. +func NewGenericMultiArgCLI[C any, U any, R any](crud MultiArgCRUD[C, U, R]) *MultiArgGenericCLI[C, U, R] { + fs := afero.NewOsFs() + return &MultiArgGenericCLI[C, U, R]{ + crud: crud, + fs: fs, + parser: MultiDocumentYAML[R]{fs: fs}, + bulkPrint: false, + } +} + +func (a *MultiArgGenericCLI[C, U, R]) WithFS(fs afero.Fs) *MultiArgGenericCLI[C, U, R] { + a.fs = fs + a.parser = MultiDocumentYAML[R]{fs: fs} + return a +} + +func (a *MultiArgGenericCLI[C, U, R]) WithSorter(sorter *multisort.Sorter[R]) *MultiArgGenericCLI[C, U, R] { + a.sorter = sorter + return a +} + +// WithBulkPrint prints results in a bulk at the end on multi-entity operations, the results are a list. +// default is printing results intermediately during the bulk operation, which causes single entities to be printed in sequence. +func (a *MultiArgGenericCLI[C, U, R]) WithBulkPrint() *MultiArgGenericCLI[C, U, R] { + a.bulkPrint = true + return a +} + +// WithBulkSecurityPrompt prints interactive prompts before a multi-entity operation if there is a tty. +func (a *MultiArgGenericCLI[C, U, R]) WithBulkSecurityPrompt(in io.Reader, out io.Writer) *MultiArgGenericCLI[C, U, R] { + a.bulkSecurityPrompt = &PromptConfig{ + In: in, + Out: out, + } + return a +} + +// WithBulkTimestamps prints out the duration of an operation to stdout during a bulk operation. +func (a *MultiArgGenericCLI[C, U, R]) WithTimestamps() *MultiArgGenericCLI[C, U, R] { + a.timestamps = true + return a +} + +// Interface returns the interface that was used to create this generic cli. +func (a *MultiArgGenericCLI[C, U, R]) Interface() MultiArgCRUD[C, U, R] { + return a.crud +} + +// Sorter returns the sorter of this generic cli. +func (a *MultiArgGenericCLI[C, U, R]) Sorter() *multisort.Sorter[R] { + return a.sorter +} diff --git a/pkg/genericcli/generic.go b/pkg/genericcli/generic.go index a8d4077..8682fdc 100644 --- a/pkg/genericcli/generic.go +++ b/pkg/genericcli/generic.go @@ -3,6 +3,7 @@ package genericcli import ( "io" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/multisort" "github.com/spf13/afero" ) @@ -13,14 +14,8 @@ import ( // U is the update request for an entity. // R is the response object of an entity. type GenericCLI[C any, U any, R any] struct { - fs afero.Fs - crud CRUD[C, U, R] - parser MultiDocumentYAML[R] - sorter *multisort.Sorter[R] - - bulkPrint bool - bulkSecurityPrompt *PromptConfig - timestamps bool + // internally we map everyhing to multi arg cli to reduce code redundance + multiCLI *MultiArgGenericCLI[C, U, R] } // CRUD must be implemented in order to get generic CLI functionality. @@ -50,56 +45,150 @@ type CRUD[C any, U any, R any] interface { // U is the update request for an entity. // R is the response object of an entity. func NewGenericCLI[C any, U any, R any](crud CRUD[C, U, R]) *GenericCLI[C, U, R] { - fs := afero.NewOsFs() return &GenericCLI[C, U, R]{ - crud: crud, - fs: fs, - parser: MultiDocumentYAML[R]{fs: fs}, - bulkPrint: false, + multiCLI: NewGenericMultiArgCLI(multiArgMapper[C, U, R]{singleArg: crud}), } } func (a *GenericCLI[C, U, R]) WithFS(fs afero.Fs) *GenericCLI[C, U, R] { - a.fs = fs - a.parser = MultiDocumentYAML[R]{fs: fs} + a.multiCLI.WithFS(fs) return a } func (a *GenericCLI[C, U, R]) WithSorter(sorter *multisort.Sorter[R]) *GenericCLI[C, U, R] { - a.sorter = sorter + a.multiCLI.WithSorter(sorter) return a } // WithBulkPrint prints results in a bulk at the end on multi-entity operations, the results are a list. // default is printing results intermediately during the bulk operation, which causes single entities to be printed in sequence. func (a *GenericCLI[C, U, R]) WithBulkPrint() *GenericCLI[C, U, R] { - a.bulkPrint = true + a.multiCLI.WithBulkPrint() return a } // WithBulkSecurityPrompt prints interactive prompts before a multi-entity operation if there is a tty. func (a *GenericCLI[C, U, R]) WithBulkSecurityPrompt(in io.Reader, out io.Writer) *GenericCLI[C, U, R] { - a.bulkSecurityPrompt = &PromptConfig{ - In: in, - Out: out, - } + a.multiCLI.WithBulkSecurityPrompt(in, out) return a } // WithBulkTimestamps prints out the duration of an operation to stdout during a bulk operation. func (a *GenericCLI[C, U, R]) WithTimestamps() *GenericCLI[C, U, R] { - a.timestamps = true + a.multiCLI.WithTimestamps() return a } // Interface returns the interface that was used to create this generic cli. -func (a *GenericCLI[C, U, R]) Interface() CRUD[C, U, R] { - return a.crud +func (a *GenericCLI[C, U, R]) Interface() MultiArgCRUD[C, U, R] { + return a.multiCLI.Interface() } // Sorter returns the sorter of this generic cli. func (a *GenericCLI[C, U, R]) Sorter() *multisort.Sorter[R] { - return a.sorter + return a.multiCLI.Sorter() +} + +func (a *GenericCLI[C, U, R]) List(sortKeys ...multisort.Key) ([]R, error) { + return a.multiCLI.List(sortKeys...) +} +func (a *GenericCLI[C, U, R]) ListAndPrint(p printers.Printer, sortKeys ...multisort.Key) error { + return a.multiCLI.ListAndPrint(p, sortKeys...) +} +func (a *GenericCLI[C, U, R]) Describe(id string) (R, error) { + return a.multiCLI.Describe(id) +} +func (a *GenericCLI[C, U, R]) DescribeAndPrint(id string, p printers.Printer) error { + return a.multiCLI.DescribeAndPrint(p, id) +} +func (a *GenericCLI[C, U, R]) Delete(id string) (R, error) { + return a.multiCLI.Delete(id) +} +func (a *GenericCLI[C, U, R]) DeleteAndPrint(id string, p printers.Printer) error { + return a.multiCLI.DeleteAndPrint(p, id) +} +func (a *GenericCLI[C, U, R]) Create(rq C) (R, error) { + return a.multiCLI.Create(rq) +} +func (a *GenericCLI[C, U, R]) CreateAndPrint(rq C, p printers.Printer) error { + return a.multiCLI.CreateAndPrint(rq, p) +} +func (a *GenericCLI[C, U, R]) Update(rq U) (R, error) { + return a.multiCLI.Update(rq) +} +func (a *GenericCLI[C, U, R]) UpdateAndPrint(rq U, p printers.Printer) error { + return a.multiCLI.UpdateAndPrint(rq, p) +} +func (a *GenericCLI[C, U, R]) Edit(args []string) (R, error) { + return a.multiCLI.Edit(1, args) +} +func (a *GenericCLI[C, U, R]) EditAndPrint(args []string, p printers.Printer) error { + return a.multiCLI.EditAndPrint(1, args, p) +} +func (a *GenericCLI[C, U, R]) CreateFromFile(from string) (BulkResults[R], error) { + return a.multiCLI.CreateFromFile(from) +} +func (a *GenericCLI[C, U, R]) CreateFromFileAndPrint(from string, p printers.Printer) error { + return a.multiCLI.CreateFromFileAndPrint(from, p) +} +func (a *GenericCLI[C, U, R]) UpdateFromFile(from string) (BulkResults[R], error) { + return a.multiCLI.UpdateFromFile(from) +} +func (a *GenericCLI[C, U, R]) UpdateFromFileAndPrint(from string, p printers.Printer) error { + return a.multiCLI.UpdateFromFileAndPrint(from, p) +} +func (a *GenericCLI[C, U, R]) ApplyFromFile(from string) (BulkResults[R], error) { + return a.multiCLI.ApplyFromFile(from) +} +func (a *GenericCLI[C, U, R]) ApplyFromFileAndPrint(from string, p printers.Printer) error { + return a.multiCLI.ApplyFromFileAndPrint(from, p) +} +func (a *GenericCLI[C, U, R]) DeleteFromFile(from string) (BulkResults[R], error) { + return a.multiCLI.DeleteFromFile(from) +} +func (a *GenericCLI[C, U, R]) DeleteFromFileAndPrint(from string, p printers.Printer) error { + return a.multiCLI.DeleteFromFileAndPrint(from, p) +} + +type multiArgMapper[C any, U any, R any] struct { + singleArg CRUD[C, U, R] +} + +func (v multiArgMapper[C, U, R]) Get(ids ...string) (R, error) { + id, err := GetExactlyOneArg(ids) + if err != nil { + var zero R + return zero, err + } + + return v.singleArg.Get(id) +} + +func (v multiArgMapper[C, U, R]) List() ([]R, error) { + return v.singleArg.List() +} + +func (v multiArgMapper[C, U, R]) Create(rq C) (R, error) { + return v.singleArg.Create(rq) +} + +func (v multiArgMapper[C, U, R]) Update(rq U) (R, error) { + return v.singleArg.Update(rq) +} + +func (v multiArgMapper[C, U, R]) Delete(ids ...string) (R, error) { + id, err := GetExactlyOneArg(ids) + if err != nil { + var zero R + return zero, err + } + + return v.singleArg.Delete(id) +} + +func (v multiArgMapper[C, U, R]) Convert(r R) ([]string, C, U, error) { + id, cr, ur, err := v.singleArg.Convert(r) + return []string{id}, cr, ur, err } // following only used for mock generation (has to be in non-test file), do not use: @@ -111,7 +200,7 @@ type ( Create(rq *testCreate) (*testResponse, error) Update(rq *testUpdate) (*testResponse, error) Delete(id string) (*testResponse, error) - Convert(r *testResponse) (string, *testCreate, *testUpdate, error) + Convert(r *testResponse) ([]string, *testCreate, *testUpdate, error) } testCRUD struct{ client testClient } testCreate struct { diff --git a/pkg/genericcli/generic_mock_test.go b/pkg/genericcli/generic_mock_test.go index 2af4253..8898fa5 100644 --- a/pkg/genericcli/generic_mock_test.go +++ b/pkg/genericcli/generic_mock_test.go @@ -1,4 +1,4 @@ -// Code generated by mockery v2.14.0. DO NOT EDIT. +// Code generated by mockery v2.45.1. DO NOT EDIT. package genericcli @@ -10,17 +10,28 @@ type mockTestClient struct { } // Convert provides a mock function with given fields: r -func (_m *mockTestClient) Convert(r *testResponse) (string, *testCreate, *testUpdate, error) { +func (_m *mockTestClient) Convert(r *testResponse) ([]string, *testCreate, *testUpdate, error) { ret := _m.Called(r) - var r0 string - if rf, ok := ret.Get(0).(func(*testResponse) string); ok { + if len(ret) == 0 { + panic("no return value specified for Convert") + } + + var r0 []string + var r1 *testCreate + var r2 *testUpdate + var r3 error + if rf, ok := ret.Get(0).(func(*testResponse) ([]string, *testCreate, *testUpdate, error)); ok { + return rf(r) + } + if rf, ok := ret.Get(0).(func(*testResponse) []string); ok { r0 = rf(r) } else { - r0 = ret.Get(0).(string) + if ret.Get(0) != nil { + r0 = ret.Get(0).([]string) + } } - var r1 *testCreate if rf, ok := ret.Get(1).(func(*testResponse) *testCreate); ok { r1 = rf(r) } else { @@ -29,7 +40,6 @@ func (_m *mockTestClient) Convert(r *testResponse) (string, *testCreate, *testUp } } - var r2 *testUpdate if rf, ok := ret.Get(2).(func(*testResponse) *testUpdate); ok { r2 = rf(r) } else { @@ -38,7 +48,6 @@ func (_m *mockTestClient) Convert(r *testResponse) (string, *testCreate, *testUp } } - var r3 error if rf, ok := ret.Get(3).(func(*testResponse) error); ok { r3 = rf(r) } else { @@ -52,7 +61,15 @@ func (_m *mockTestClient) Convert(r *testResponse) (string, *testCreate, *testUp func (_m *mockTestClient) Create(rq *testCreate) (*testResponse, error) { ret := _m.Called(rq) + if len(ret) == 0 { + panic("no return value specified for Create") + } + var r0 *testResponse + var r1 error + if rf, ok := ret.Get(0).(func(*testCreate) (*testResponse, error)); ok { + return rf(rq) + } if rf, ok := ret.Get(0).(func(*testCreate) *testResponse); ok { r0 = rf(rq) } else { @@ -61,7 +78,6 @@ func (_m *mockTestClient) Create(rq *testCreate) (*testResponse, error) { } } - var r1 error if rf, ok := ret.Get(1).(func(*testCreate) error); ok { r1 = rf(rq) } else { @@ -75,7 +91,15 @@ func (_m *mockTestClient) Create(rq *testCreate) (*testResponse, error) { func (_m *mockTestClient) Delete(id string) (*testResponse, error) { ret := _m.Called(id) + if len(ret) == 0 { + panic("no return value specified for Delete") + } + var r0 *testResponse + var r1 error + if rf, ok := ret.Get(0).(func(string) (*testResponse, error)); ok { + return rf(id) + } if rf, ok := ret.Get(0).(func(string) *testResponse); ok { r0 = rf(id) } else { @@ -84,7 +108,6 @@ func (_m *mockTestClient) Delete(id string) (*testResponse, error) { } } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { @@ -98,7 +121,15 @@ func (_m *mockTestClient) Delete(id string) (*testResponse, error) { func (_m *mockTestClient) Get(id string) (*testResponse, error) { ret := _m.Called(id) + if len(ret) == 0 { + panic("no return value specified for Get") + } + var r0 *testResponse + var r1 error + if rf, ok := ret.Get(0).(func(string) (*testResponse, error)); ok { + return rf(id) + } if rf, ok := ret.Get(0).(func(string) *testResponse); ok { r0 = rf(id) } else { @@ -107,7 +138,6 @@ func (_m *mockTestClient) Get(id string) (*testResponse, error) { } } - var r1 error if rf, ok := ret.Get(1).(func(string) error); ok { r1 = rf(id) } else { @@ -121,7 +151,15 @@ func (_m *mockTestClient) Get(id string) (*testResponse, error) { func (_m *mockTestClient) List() ([]*testResponse, error) { ret := _m.Called() + if len(ret) == 0 { + panic("no return value specified for List") + } + var r0 []*testResponse + var r1 error + if rf, ok := ret.Get(0).(func() ([]*testResponse, error)); ok { + return rf() + } if rf, ok := ret.Get(0).(func() []*testResponse); ok { r0 = rf() } else { @@ -130,7 +168,6 @@ func (_m *mockTestClient) List() ([]*testResponse, error) { } } - var r1 error if rf, ok := ret.Get(1).(func() error); ok { r1 = rf() } else { @@ -144,7 +181,15 @@ func (_m *mockTestClient) List() ([]*testResponse, error) { func (_m *mockTestClient) Update(rq *testUpdate) (*testResponse, error) { ret := _m.Called(rq) + if len(ret) == 0 { + panic("no return value specified for Update") + } + var r0 *testResponse + var r1 error + if rf, ok := ret.Get(0).(func(*testUpdate) (*testResponse, error)); ok { + return rf(rq) + } if rf, ok := ret.Get(0).(func(*testUpdate) *testResponse); ok { r0 = rf(rq) } else { @@ -153,7 +198,6 @@ func (_m *mockTestClient) Update(rq *testUpdate) (*testResponse, error) { } } - var r1 error if rf, ok := ret.Get(1).(func(*testUpdate) error); ok { r1 = rf(rq) } else { @@ -163,13 +207,12 @@ func (_m *mockTestClient) Update(rq *testUpdate) (*testResponse, error) { return r0, r1 } -type mockConstructorTestingTnewMockTestClient interface { +// newMockTestClient creates a new instance of mockTestClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func newMockTestClient(t interface { mock.TestingT Cleanup(func()) -} - -// newMockTestClient creates a new instance of mockTestClient. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. -func newMockTestClient(t mockConstructorTestingTnewMockTestClient) *mockTestClient { +}) *mockTestClient { mock := &mockTestClient{} mock.Mock.Test(t) diff --git a/pkg/genericcli/generic_test.go b/pkg/genericcli/generic_test.go index 4d25310..293527a 100644 --- a/pkg/genericcli/generic_test.go +++ b/pkg/genericcli/generic_test.go @@ -1,6 +1,11 @@ package genericcli -func (t testCRUD) Get(id string) (*testResponse, error) { +func (t testCRUD) Get(ids ...string) (*testResponse, error) { + id, err := GetExactlyOneArg(ids) + if err != nil { + return nil, err + } + return t.client.Get(id) } @@ -16,12 +21,17 @@ func (t testCRUD) Update(rq *testUpdate) (*testResponse, error) { return t.client.Update(rq) } -func (t testCRUD) Delete(id string) (*testResponse, error) { +func (t testCRUD) Delete(ids ...string) (*testResponse, error) { + id, err := GetExactlyOneArg(ids) + if err != nil { + return nil, err + } + return t.client.Delete(id) } -func (t testCRUD) Convert(r *testResponse) (string, *testCreate, *testUpdate, error) { - return r.ID, +func (t testCRUD) Convert(r *testResponse) ([]string, *testCreate, *testUpdate, error) { + return []string{r.ID}, &testCreate{ ID: r.ID, Name: r.Name,