From fd74ff50dbdb6c657951e1d73fe17240155b80c6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Tue, 17 Sep 2024 10:17:23 +0200 Subject: [PATCH 01/19] Add `project machine-reservations` command. --- cmd/printers.go | 2 +- cmd/project-reservations.go | 198 ++++++++++++++++++++++ cmd/project.go | 1 + cmd/tableprinters/printer.go | 26 ++- cmd/tableprinters/project-reservations.go | 26 +++ go.mod | 2 +- go.sum | 4 +- 7 files changed, 246 insertions(+), 13 deletions(-) create mode 100644 cmd/project-reservations.go create mode 100644 cmd/tableprinters/project-reservations.go diff --git a/cmd/printers.go b/cmd/printers.go index 3f5ec1b..15127cc 100644 --- a/cmd/printers.go +++ b/cmd/printers.go @@ -19,7 +19,7 @@ func newPrinterFromCLI(out io.Writer) printers.Printer { case "json": printer = printers.NewJSONPrinter().WithOut(out) case "table", "wide", "markdown": - tp := tableprinters.New() + tp := tableprinters.New(out) tablePrinter := printers.NewTablePrinter(&printers.TablePrinterConfig{ ToHeaderAndRows: tp.ToHeaderAndRows, diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go new file mode 100644 index 0000000..34a8aec --- /dev/null +++ b/cmd/project-reservations.go @@ -0,0 +1,198 @@ +package cmd + +import ( + "errors" + "fmt" + "strings" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type machineReservationsCmd struct { + *config +} + +func newMachineReservationsCmd(c *config) *cobra.Command { + w := machineReservationsCmd{ + config: c, + } + + cmdsConfig := &genericcli.CmdsConfig[*models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, *models.V1MachineReservationResponse]{ + BinaryName: binaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), + Singular: "machine-reservation", + Plural: "machine-reservations", + Description: "manage machine reservations, ids must be provided in the form @", + // Sorter: sorters.MachineReservationsSorter(), + // ValidArgsFn: c.comp.MachineReservationsListCompletion, + DescribePrinter: func() printers.Printer { return c.describePrinter }, + ListPrinter: func() printers.Printer { return c.listPrinter }, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("project", "", "show projects of given id") + cmd.Flags().String("size", "", "show projects of given name") + cmd.Flags().String("tenant", "", "show projects of given name") + genericcli.Must(cmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") + cmd.Flags().String("description", "", "the description of the reservation") + cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) + }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("project", "", "the project of the reservation") + cmd.Flags().String("size", "", "the size of the reservation") + cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") + cmd.Flags().String("description", "", "the description of the reservation") + cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) + }, + CreateRequestFromCLI: func() (*models.V1MachineReservationCreateRequest, error) { + return &models.V1MachineReservationCreateRequest{ + Amount: pointer.PointerOrNil(viper.GetInt32("amount")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partitionids: viper.GetStringSlice("partitions"), + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + }, nil + }, + UpdateRequestFromCLI: func(args []string) (*models.V1MachineReservationUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + p, size, err := w.fromCompoundID(id) + if err != nil { + return nil, err + } + + return &models.V1MachineReservationUpdateRequest{ + Amount: pointer.PointerOrNil(viper.GetInt32("amount")), + Description: pointer.PointerOrNil(viper.GetString("description")), + Partitionids: viper.GetStringSlice("partitions"), + Projectid: &p, + Sizeid: &size, + }, nil + }, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (m machineReservationsCmd) toCompoundID(project, size string) string { + return fmt.Sprintf("%s@%s", project, size) +} + +func (m machineReservationsCmd) fromCompoundID(id string) (project, size string, err error) { + project, size, ok := strings.Cut(id, "@") + if !ok { + return "", "", errors.New("reservation id must be in form @") + } + return project, size, nil +} + +func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) (string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { + id := m.toCompoundID(*r.Projectid, *r.Sizeid) + return id, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil +} + +func toMachineReservationCreateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationCreateRequest { + return &models.V1MachineReservationCreateRequest{ + Amount: r.Amount, + Description: &r.Description, + Partitionids: r.Partitionids, + Projectid: r.Projectid, + Sizeid: r.Sizeid, + } +} + +func toMachineReservationUpdateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationUpdateRequest { + return &models.V1MachineReservationUpdateRequest{ + Amount: r.Amount, + Description: &r.Description, + Partitionids: r.Partitionids, + Projectid: r.Projectid, + Sizeid: r.Sizeid, + } +} + +func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequest) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.CreateMachineReservation(project.NewCreateMachineReservationParams().WithBody(rq), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Delete(id string) (*models.V1MachineReservationResponse, error) { + p, size, err := m.fromCompoundID(id) + if err != nil { + return nil, err + } + + resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams().WithProject(p).WithSize(size), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Get(id string) (*models.V1MachineReservationResponse, error) { + p, size, err := m.fromCompoundID(id) + if err != nil { + return nil, err + } + + all, err := m.List() + if err != nil { + return nil, fmt.Errorf("unable to fetch machine reservations: %w", err) + } + + for _, rv := range all { + if p != pointer.SafeDeref(rv.Projectid) { + continue + } + if size != pointer.SafeDeref(rv.Sizeid) { + continue + } + + return rv, nil + } + + return nil, fmt.Errorf("no reservation found with size %q for project %q", size, p) +} + +func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + }), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} + +func (m machineReservationsCmd) Update(rq *models.V1MachineReservationUpdateRequest) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.UpdateMachineReservation(project.NewUpdateMachineReservationParams().WithBody(rq), nil) + if err != nil { + return nil, err + } + + return resp.Payload, nil +} diff --git a/cmd/project.go b/cmd/project.go index a5153ac..0a6efac 100644 --- a/cmd/project.go +++ b/cmd/project.go @@ -104,6 +104,7 @@ func newProjectCmd(c *config) *cobra.Command { projectCmd.AddCommand(projectListCmd) projectCmd.AddCommand(projectApplyCmd) projectCmd.AddCommand(projectEditCmd) + projectCmd.AddCommand(newMachineReservationsCmd(c)) return projectCmd } diff --git a/cmd/tableprinters/printer.go b/cmd/tableprinters/printer.go index 89bac39..ee6f17c 100644 --- a/cmd/tableprinters/printer.go +++ b/cmd/tableprinters/printer.go @@ -3,8 +3,10 @@ package tableprinters import ( "io" + "github.com/fi-ts/cloud-go/api/models" "github.com/fi-ts/cloudctl/cmd/output" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" + "github.com/metal-stack/metal-lib/pkg/pointer" ) type TablePrinter struct { @@ -12,11 +14,13 @@ type TablePrinter struct { // TODO: we want to slowly migrate to the genericcli table printer // after everything was shifted to this package we can remove the "oldPrinter" oldPrinter printers.Printer + out io.Writer } -func New() *TablePrinter { +func New(out io.Writer) *TablePrinter { return &TablePrinter{ oldPrinter: output.New(), + out: out, } } @@ -25,13 +29,17 @@ func (t *TablePrinter) SetPrinter(printer *printers.TablePrinter) { } func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]string, error) { + t.t.WithOut(t.out) + // TODO: migrate old output package code to here - // switch d := data.(type) { - // default: - // return nil, nil, t.oldPrinter.Print(data) - // } - // - // fallback to old printer for as long as the migration takes: - t.t.WithOut(io.Discard) - return nil, nil, t.oldPrinter.Print(data) + switch d := data.(type) { + case *models.V1MachineReservationResponse: + return t.MachineReservationsTable(pointer.WrapInSlice(d), wide) + case []*models.V1MachineReservationResponse: + return t.MachineReservationsTable(d, wide) + default: + // fallback to old printer for as long as the migration takes: + t.t.WithOut(io.Discard) + return nil, nil, t.oldPrinter.Print(data) + } } diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go new file mode 100644 index 0000000..fd78316 --- /dev/null +++ b/cmd/tableprinters/project-reservations.go @@ -0,0 +1,26 @@ +package tableprinters + +import ( + "strconv" + "strings" + + "github.com/fi-ts/cloud-go/api/models" +) + +func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"Tenant", "Project", "Size", "Partitions", "Amount"} + rows [][]string + ) + + for _, p := range data { + + // labels, description + + row := []string{*p.Tenant, *p.Projectid, *p.Sizeid, strings.Join(p.Partitionids, ","), strconv.Itoa(int(*p.Amount))} + + rows = append(rows, row) + } + + return header, rows, nil +} diff --git a/go.mod b/go.mod index 9aafd3a..1e30ce5 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.0 + github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326 github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 diff --git a/go.sum b/go.sum index 1aaa184..1b8b9e1 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.0 h1:MAg5Vsac9XYxCCL73USnceLtry1pvJovcNjwvqrv0lU= -github.com/fi-ts/cloud-go v0.28.0/go.mod h1:R7JMkC92eGvxkkMO1oP6lEevBH86DFiO9H9mo7YD5Sw= +github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326 h1:eujakn8xwpEM8pONokO/foDXmS2Bno2AQyxlSh+PgIg= +github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326/go.mod h1:R7JMkC92eGvxkkMO1oP6lEevBH86DFiO9H9mo7YD5Sw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= From 326d37e1ae064e1fe8e5fb74cf4c6d40807448c5 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Sep 2024 15:13:36 +0200 Subject: [PATCH 02/19] Some progress. --- cmd/completion/project-reservations.go | 59 ++++++++++ cmd/project-reservations.go | 131 ++++++++++++---------- cmd/sorters/project-reservations.go | 47 ++++++++ cmd/tableprinters/printer.go | 7 ++ cmd/tableprinters/project-reservations.go | 76 ++++++++++++- go.mod | 4 +- go.sum | 8 +- 7 files changed, 266 insertions(+), 66 deletions(-) create mode 100644 cmd/completion/project-reservations.go create mode 100644 cmd/sorters/project-reservations.go diff --git a/cmd/completion/project-reservations.go b/cmd/completion/project-reservations.go new file mode 100644 index 0000000..04d5930 --- /dev/null +++ b/cmd/completion/project-reservations.go @@ -0,0 +1,59 @@ +package completion + +import ( + "sort" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + "github.com/spf13/cobra" +) + +func (c *Completion) MachineReservationListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + if len(args) == 0 { + resp, err := c.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{}), nil) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var projects []string + + for _, rv := range resp.Payload { + if rv.Projectid == nil { + continue + } + + projects = append(projects, *rv.Projectid) + } + + sort.Strings(projects) + + return projects, cobra.ShellCompDirectiveNoFileComp + } + + p := args[0] + + resp, err := c.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ + Projectid: &p, + }), nil) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + + var sizes []string + + for _, rv := range resp.Payload { + if rv.Projectid == nil || rv.Sizeid == nil { + continue + } + + if *rv.Projectid != p { + continue + } + + sizes = append(sizes, *rv.Sizeid) + } + + sort.Strings(sizes) + + return sizes, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 34a8aec..7ced97f 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -1,12 +1,11 @@ package cmd import ( - "errors" "fmt" - "strings" "github.com/fi-ts/cloud-go/api/client/project" "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/sorters" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" "github.com/metal-stack/metal-lib/pkg/pointer" @@ -24,19 +23,20 @@ func newMachineReservationsCmd(c *config) *cobra.Command { } cmdsConfig := &genericcli.CmdsConfig[*models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, *models.V1MachineReservationResponse]{ - BinaryName: binaryName, - GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), - Singular: "machine-reservation", - Plural: "machine-reservations", - Description: "manage machine reservations, ids must be provided in the form @", - // Sorter: sorters.MachineReservationsSorter(), - // ValidArgsFn: c.comp.MachineReservationsListCompletion, + BinaryName: binaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), + Args: []string{"project", "size"}, + Singular: "machine-reservation", + Plural: "machine-reservations", + Description: "manage machine reservations, ids must be provided in the form @", + Sorter: sorters.MachineReservationsSorter(), + ValidArgsFn: c.comp.MachineReservationListCompletion, DescribePrinter: func() printers.Printer { return c.describePrinter }, ListPrinter: func() printers.Printer { return c.listPrinter }, ListCmdMutateFn: func(cmd *cobra.Command) { - cmd.Flags().String("project", "", "show projects of given id") - cmd.Flags().String("size", "", "show projects of given name") - cmd.Flags().String("tenant", "", "show projects of given name") + cmd.Flags().String("project", "", "show reservations of given project") + cmd.Flags().String("size", "", "show reservations of given size") + cmd.Flags().String("tenant", "", "show reservations of given tenant") genericcli.Must(cmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) @@ -45,6 +45,7 @@ func newMachineReservationsCmd(c *config) *cobra.Command { cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") cmd.Flags().String("description", "", "the description of the reservation") cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + cmd.Flags().Bool("force", false, "allows overbooking of a partition") genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) }, CreateCmdMutateFn: func(cmd *cobra.Command) { @@ -53,6 +54,7 @@ func newMachineReservationsCmd(c *config) *cobra.Command { cmd.Flags().Int32("amount", 0, "the amount of machines to reserve") cmd.Flags().String("description", "", "the description of the reservation") cmd.Flags().StringSlice("partitions", nil, "the partitions in which this reservation is being made") + cmd.Flags().Bool("force", false, "allows overbooking of a partition") genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) @@ -67,12 +69,7 @@ func newMachineReservationsCmd(c *config) *cobra.Command { }, nil }, UpdateRequestFromCLI: func(args []string) (*models.V1MachineReservationUpdateRequest, error) { - id, err := genericcli.GetExactlyOneArg(args) - if err != nil { - return nil, err - } - - p, size, err := w.fromCompoundID(id) + ids, err := genericcli.GetExactlyNArgs(2, args) if err != nil { return nil, err } @@ -81,30 +78,33 @@ func newMachineReservationsCmd(c *config) *cobra.Command { Amount: pointer.PointerOrNil(viper.GetInt32("amount")), Description: pointer.PointerOrNil(viper.GetString("description")), Partitionids: viper.GetStringSlice("partitions"), - Projectid: &p, - Sizeid: &size, + Projectid: &ids[w.projectIndex()], + Sizeid: &ids[w.sizeIndex()], }, nil }, } - return genericcli.NewCmds(cmdsConfig) -} + usageCmd := &cobra.Command{ + Use: "usage", + Short: "shows the current usage of machine reservations", + ValidArgsFunction: c.comp.MachineReservationListCompletion, + RunE: func(cmd *cobra.Command, _ []string) error { + return w.machineReservationsUsage() + }, + } -func (m machineReservationsCmd) toCompoundID(project, size string) string { - return fmt.Sprintf("%s@%s", project, size) -} + usageCmd.Flags().String("project", "", "show reservations of given project") + usageCmd.Flags().String("size", "", "show reservations of given size") + usageCmd.Flags().String("tenant", "", "show reservations of given tenant") + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) + genericcli.Must(usageCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) -func (m machineReservationsCmd) fromCompoundID(id string) (project, size string, err error) { - project, size, ok := strings.Cut(id, "@") - if !ok { - return "", "", errors.New("reservation id must be in form @") - } - return project, size, nil + return genericcli.NewCmds(cmdsConfig, usageCmd) } -func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) (string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { - id := m.toCompoundID(*r.Projectid, *r.Sizeid) - return id, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil +func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) ([]string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { + return []string{*r.Projectid, *r.Sizeid}, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil } func toMachineReservationCreateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationCreateRequest { @@ -128,7 +128,9 @@ func toMachineReservationUpdateRequest(r *models.V1MachineReservationResponse) * } func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequest) (*models.V1MachineReservationResponse, error) { - resp, err := m.cloud.Project.CreateMachineReservation(project.NewCreateMachineReservationParams().WithBody(rq), nil) + resp, err := m.cloud.Project.CreateMachineReservation(project.NewCreateMachineReservationParams(). + WithBody(rq). + WithForce(pointer.Pointer(viper.GetBool("force"))), nil) if err != nil { return nil, err } @@ -136,13 +138,10 @@ func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequ return resp.Payload, nil } -func (m machineReservationsCmd) Delete(id string) (*models.V1MachineReservationResponse, error) { - p, size, err := m.fromCompoundID(id) - if err != nil { - return nil, err - } - - resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams().WithProject(p).WithSize(size), nil) +func (m machineReservationsCmd) Delete(id ...string) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams(). + WithProject(pointer.Pointer(id[m.projectIndex()])). + WithSize(pointer.Pointer(id[m.sizeIndex()])), nil) if err != nil { return nil, err } @@ -150,37 +149,33 @@ func (m machineReservationsCmd) Delete(id string) (*models.V1MachineReservationR return resp.Payload, nil } -func (m machineReservationsCmd) Get(id string) (*models.V1MachineReservationResponse, error) { - p, size, err := m.fromCompoundID(id) - if err != nil { - return nil, err - } - +func (m machineReservationsCmd) Get(id ...string) (*models.V1MachineReservationResponse, error) { all, err := m.List() if err != nil { return nil, fmt.Errorf("unable to fetch machine reservations: %w", err) } for _, rv := range all { - if p != pointer.SafeDeref(rv.Projectid) { + if id[m.projectIndex()] != pointer.SafeDeref(rv.Projectid) { continue } - if size != pointer.SafeDeref(rv.Sizeid) { + if id[m.sizeIndex()] != pointer.SafeDeref(rv.Sizeid) { continue } return rv, nil } - return nil, fmt.Errorf("no reservation found with size %q for project %q", size, p) + return nil, fmt.Errorf("no reservation found with size %q for project %q", id[1], id[0]) } func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, error) { - resp, err := m.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ - Projectid: pointer.PointerOrNil(viper.GetString("project")), - Sizeid: pointer.PointerOrNil(viper.GetString("size")), - Tenant: pointer.PointerOrNil(viper.GetString("tenant")), - }), nil) + resp, err := m.cloud.Project.ListMachineReservations(project.NewListMachineReservationsParams(). + WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + }), nil) if err != nil { return nil, err } @@ -189,10 +184,32 @@ func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, } func (m machineReservationsCmd) Update(rq *models.V1MachineReservationUpdateRequest) (*models.V1MachineReservationResponse, error) { - resp, err := m.cloud.Project.UpdateMachineReservation(project.NewUpdateMachineReservationParams().WithBody(rq), nil) + resp, err := m.cloud.Project.UpdateMachineReservation(project.NewUpdateMachineReservationParams().WithBody(rq). + WithForce(pointer.Pointer(viper.GetBool("force"))), nil) if err != nil { return nil, err } return resp.Payload, nil } + +func (m machineReservationsCmd) machineReservationsUsage() error { + resp, err := m.cloud.Project.MachineReservationsUsage(project.NewMachineReservationsUsageParams(). + WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.PointerOrNil(viper.GetString("project")), + Sizeid: pointer.PointerOrNil(viper.GetString("size")), + Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + }), nil) + if err != nil { + return err + } + + return m.listPrinter.Print(resp.Payload) +} + +func (m machineReservationsCmd) projectIndex() int { + return 0 +} +func (m machineReservationsCmd) sizeIndex() int { + return 1 +} diff --git a/cmd/sorters/project-reservations.go b/cmd/sorters/project-reservations.go new file mode 100644 index 0000000..7e9598a --- /dev/null +++ b/cmd/sorters/project-reservations.go @@ -0,0 +1,47 @@ +package sorters + +import ( + "github.com/fi-ts/cloud-go/api/models" + "github.com/metal-stack/metal-lib/pkg/multisort" + p "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func MachineReservationsSorter() *multisort.Sorter[*models.V1MachineReservationResponse] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationResponse]{ + "tenant": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "amount": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Amount), p.SafeDeref(b.Amount), descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "size"}, {ID: "amount"}}) +} + +func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReservationUsageResponse] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationUsageResponse]{ + "tenant": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "partition": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Partitionid), p.SafeDeref(b.Partitionid), descending) + }, + "reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Reservations), p.SafeDeref(b.Reservations), descending) + }, + "unused-reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Usedreservations), p.SafeDeref(b.Usedreservations), descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "reservations"}}) +} diff --git a/cmd/tableprinters/printer.go b/cmd/tableprinters/printer.go index ee6f17c..edaa41e 100644 --- a/cmd/tableprinters/printer.go +++ b/cmd/tableprinters/printer.go @@ -33,10 +33,17 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin // TODO: migrate old output package code to here switch d := data.(type) { + + // project machine reservations case *models.V1MachineReservationResponse: return t.MachineReservationsTable(pointer.WrapInSlice(d), wide) case []*models.V1MachineReservationResponse: return t.MachineReservationsTable(d, wide) + case *models.V1MachineReservationUsageResponse: + return t.MachineReservationsUsageTable(pointer.WrapInSlice(d), wide) + case []*models.V1MachineReservationUsageResponse: + return t.MachineReservationsUsageTable(d, wide) + default: // fallback to old printer for as long as the migration takes: t.t.WithOut(io.Discard) diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index fd78316..8803a60 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -1,26 +1,96 @@ package tableprinters import ( + "fmt" "strconv" "strings" "github.com/fi-ts/cloud-go/api/models" + "github.com/metal-stack/metal-lib/pkg/genericcli" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/olekukonko/tablewriter" ) func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { var ( - header = []string{"Tenant", "Project", "Size", "Partitions", "Amount"} + header = []string{"Tenant", "Project", "Size", "Amount", "Partitions"} rows [][]string ) + if wide { + header = append(header, "Description", "Labels") + } + for _, p := range data { + row := []string{ + pointer.SafeDeref(p.Tenant), + pointer.SafeDeref(p.Projectid), + pointer.SafeDeref(p.Sizeid), + strconv.Itoa(int(pointer.SafeDeref(p.Amount))), + strings.Join(p.Partitionids, ","), + } - // labels, description + if wide { + labels := []string{} + for k, v := range p.Labels { + labels = append(labels, k+"="+v) + } + lbls := strings.Join(labels, "\n") - row := []string{*p.Tenant, *p.Projectid, *p.Sizeid, strings.Join(p.Partitionids, ","), strconv.Itoa(int(*p.Amount))} + row = append(row, genericcli.TruncateEnd(p.Description, 50), lbls) + } rows = append(rows, row) } + t.t.MutateTable(func(table *tablewriter.Table) { + table.SetAutoWrapText(false) + }) + + return header, rows, nil +} + +func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineReservationUsageResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"Tenant", "Project", "Partition", "Size", "Reservations"} + rows [][]string + ) + + if wide { + header = append(header, "Labels") + } + + for _, p := range data { + reservations := "0" + if pointer.SafeDeref(p.Reservations) > 0 { + unused := pointer.SafeDeref(p.Reservations) - pointer.SafeDeref(p.Usedreservations) + reservations = fmt.Sprintf("%d (%d/%d used)", unused, pointer.SafeDeref(p.Usedreservations), pointer.SafeDeref(p.Reservations)) + } + + row := []string{ + pointer.SafeDeref(p.Tenant), + pointer.SafeDeref(p.Projectid), + pointer.SafeDeref(p.Partitionid), + pointer.SafeDeref(p.Sizeid), + reservations, + } + + if wide { + labels := []string{} + for k, v := range p.Labels { + labels = append(labels, k+"="+v) + } + lbls := strings.Join(labels, "\n") + + row = append(row, lbls) + } + + rows = append(rows, row) + } + + t.t.MutateTable(func(table *tablewriter.Table) { + table.SetAutoWrapText(false) + }) + return header, rows, nil } diff --git a/go.mod b/go.mod index 1e30ce5..146e299 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326 + github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 @@ -20,7 +20,7 @@ require ( github.com/jinzhu/now v1.1.5 github.com/metal-stack/duros-go v0.5.1 github.com/metal-stack/metal-go v0.34.0 - github.com/metal-stack/metal-lib v0.18.2 + github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26 github.com/metal-stack/updater v1.2.2 github.com/metal-stack/v v1.0.3 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 1b8b9e1..d839faf 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326 h1:eujakn8xwpEM8pONokO/foDXmS2Bno2AQyxlSh+PgIg= -github.com/fi-ts/cloud-go v0.28.1-0.20240917130058-a9bec42d1326/go.mod h1:R7JMkC92eGvxkkMO1oP6lEevBH86DFiO9H9mo7YD5Sw= +github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a h1:VZNmN6mlN7fyG6lHEllA0cdEJRjqiLZX6CnoeOkkvNI= +github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a/go.mod h1:R7JMkC92eGvxkkMO1oP6lEevBH86DFiO9H9mo7YD5Sw= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -279,8 +279,8 @@ github.com/metal-stack/duros-go v0.5.1 h1:baE/c0AKy9sTOztPhILJLaoMmT17Ajsb+xRMfH github.com/metal-stack/duros-go v0.5.1/go.mod h1:Z9mzI9ds2gI8zHC03PUCQvmlWa7WAPukDCUhowtVeOk= github.com/metal-stack/metal-go v0.34.0 h1:X4Wlt2OAhsu3Lq+rHSWnWeASmX6CYvOxnL6DxmjnzbU= github.com/metal-stack/metal-go v0.34.0/go.mod h1:3MJTYCS4YJz8D8oteTKhjpaAKNMMjMKYDrIy9awHGtQ= -github.com/metal-stack/metal-lib v0.18.2 h1:EAmZkZeKpenAvxZRSKsA6gj9Jd8XLR6Z0/QhABFCCDE= -github.com/metal-stack/metal-lib v0.18.2/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= +github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26 h1:SILDyd9Tm40iiXfVAx+CbnbEWukFB3UbQlMAFezDOdw= +github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= github.com/metal-stack/security v0.8.1 h1:4zmVUxZvDWShVvVIxM3XhIv7pTmPe9DvACRIHW6YTsk= github.com/metal-stack/security v0.8.1/go.mod h1:OO8ZilZO6fUV5QEmwc7HP/RAjqYrGQxXoYIddJ9TvqE= github.com/metal-stack/updater v1.2.2 h1:gnUrnQgfT20QFMDtFBY89opKoBAkdeI/8T2iwMHNdxs= From 168dc3bb6730b2cfacc7d1b7a4955a1a2ae3570d Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 18 Sep 2024 15:38:17 +0200 Subject: [PATCH 03/19] Progress. --- cmd/project-reservations.go | 3 +++ cmd/tableprinters/project-reservations.go | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 7ced97f..6dd2e94 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -59,6 +59,9 @@ func newMachineReservationsCmd(c *config) *cobra.Command { genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("partitions", c.comp.PartitionListCompletion)) }, + EditCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().Bool("force", false, "allows overbooking of a partition") + }, CreateRequestFromCLI: func() (*models.V1MachineReservationCreateRequest, error) { return &models.V1MachineReservationCreateRequest{ Amount: pointer.PointerOrNil(viper.GetInt32("amount")), diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index 8803a60..28e97c7 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -13,12 +13,12 @@ import ( func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { var ( - header = []string{"Tenant", "Project", "Size", "Amount", "Partitions"} + header = []string{"Tenant", "Project", "Size", "Amount", "Partitions", "Description"} rows [][]string ) if wide { - header = append(header, "Description", "Labels") + header = append(header, "Labels") } for _, p := range data { @@ -28,6 +28,7 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat pointer.SafeDeref(p.Sizeid), strconv.Itoa(int(pointer.SafeDeref(p.Amount))), strings.Join(p.Partitionids, ","), + genericcli.TruncateEnd(p.Description, 50), } if wide { @@ -37,7 +38,7 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat } lbls := strings.Join(labels, "\n") - row = append(row, genericcli.TruncateEnd(p.Description, 50), lbls) + row = append(row, p.Description, lbls) } rows = append(rows, row) From 7b421e15b4ffdee2eec775a6f94a1e602e31fac0 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 09:27:15 +0200 Subject: [PATCH 04/19] Update. --- cmd/project-reservations.go | 20 ++++++++++---------- go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 6dd2e94..3cd7925 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -23,16 +23,16 @@ func newMachineReservationsCmd(c *config) *cobra.Command { } cmdsConfig := &genericcli.CmdsConfig[*models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, *models.V1MachineReservationResponse]{ - BinaryName: binaryName, - GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), - Args: []string{"project", "size"}, - Singular: "machine-reservation", - Plural: "machine-reservations", - Description: "manage machine reservations, ids must be provided in the form @", - Sorter: sorters.MachineReservationsSorter(), - ValidArgsFn: c.comp.MachineReservationListCompletion, - DescribePrinter: func() printers.Printer { return c.describePrinter }, - ListPrinter: func() printers.Printer { return c.listPrinter }, + BinaryName: binaryName, + MultiArgGenericCLI: genericcli.NewGenericMultiArgCLI(w).WithFS(c.fs), + Args: []string{"project", "size"}, + Singular: "machine-reservation", + Plural: "machine-reservations", + Description: "manage machine reservations, ids must be provided in the form @", + Sorter: sorters.MachineReservationsSorter(), + ValidArgsFn: c.comp.MachineReservationListCompletion, + DescribePrinter: func() printers.Printer { return c.describePrinter }, + ListPrinter: func() printers.Printer { return c.listPrinter }, ListCmdMutateFn: func(cmd *cobra.Command) { cmd.Flags().String("project", "", "show reservations of given project") cmd.Flags().String("size", "", "show reservations of given size") diff --git a/go.mod b/go.mod index 146e299..1e31e22 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jinzhu/now v1.1.5 github.com/metal-stack/duros-go v0.5.1 github.com/metal-stack/metal-go v0.34.0 - github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26 + github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4 github.com/metal-stack/updater v1.2.2 github.com/metal-stack/v v1.0.3 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index d839faf..6acdddb 100644 --- a/go.sum +++ b/go.sum @@ -279,8 +279,8 @@ github.com/metal-stack/duros-go v0.5.1 h1:baE/c0AKy9sTOztPhILJLaoMmT17Ajsb+xRMfH github.com/metal-stack/duros-go v0.5.1/go.mod h1:Z9mzI9ds2gI8zHC03PUCQvmlWa7WAPukDCUhowtVeOk= github.com/metal-stack/metal-go v0.34.0 h1:X4Wlt2OAhsu3Lq+rHSWnWeASmX6CYvOxnL6DxmjnzbU= github.com/metal-stack/metal-go v0.34.0/go.mod h1:3MJTYCS4YJz8D8oteTKhjpaAKNMMjMKYDrIy9awHGtQ= -github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26 h1:SILDyd9Tm40iiXfVAx+CbnbEWukFB3UbQlMAFezDOdw= -github.com/metal-stack/metal-lib v0.18.3-0.20240918090307-0996d850cd26/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= +github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4 h1:yRaTmSEn+aRLl1g/9uXmVLJe0UnwZOmKVYJYfSQnEkI= +github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= github.com/metal-stack/security v0.8.1 h1:4zmVUxZvDWShVvVIxM3XhIv7pTmPe9DvACRIHW6YTsk= github.com/metal-stack/security v0.8.1/go.mod h1:OO8ZilZO6fUV5QEmwc7HP/RAjqYrGQxXoYIddJ9TvqE= github.com/metal-stack/updater v1.2.2 h1:gnUrnQgfT20QFMDtFBY89opKoBAkdeI/8T2iwMHNdxs= From f984b37df79fa804288e11015719ca84b8993984 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 11:00:58 +0200 Subject: [PATCH 05/19] First tests. --- cmd/common_test.go | 337 +++++++++++++++++++++++++++++++ cmd/output/printer.go | 2 +- cmd/project-reservations_test.go | 300 +++++++++++++++++++++++++++ cmd/root.go | 44 ++-- go.mod | 5 +- go.sum | 8 +- 6 files changed, 671 insertions(+), 25 deletions(-) create mode 100644 cmd/common_test.go create mode 100644 cmd/project-reservations_test.go diff --git a/cmd/common_test.go b/cmd/common_test.go new file mode 100644 index 0000000..aa6c9cc --- /dev/null +++ b/cmd/common_test.go @@ -0,0 +1,337 @@ +package cmd + +import ( + "bytes" + "encoding/json" + "fmt" + "log/slog" + "os" + "strconv" + "strings" + "testing" + "time" + + "slices" + + "github.com/fi-ts/cloud-go/api/client" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/fi-ts/cloudctl/cmd/completion" + "github.com/google/go-cmp/cmp" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/spf13/afero" + "github.com/spf13/pflag" + "github.com/spf13/viper" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/undefinedlabs/go-mpatch" + "gopkg.in/yaml.v3" +) + +var testTime = time.Date(2022, time.May, 19, 1, 2, 3, 4, time.UTC) + +func init() { + _, err := mpatch.PatchMethod(time.Now, func() time.Time { return testTime }) + if err != nil { + panic(err) + } +} + +type test[R any] struct { + name string + + mocks *testclient.CloudMockFns + fsMocks func(fs afero.Fs, want R) + cmd func(want R) []string + + // disableMockClient can switch off mock client creation + // + // BE CAREFUL WITH THIS FLAG! + // the tests will then run with an HTTP client that really communicates with an endpoint. + // + // only use this flag for testing code parts for client creation! + // + // point to a test http server and make sure that environment variables + // that can potentially override values for client creation are cleaned up before running the test + disableMockClient bool + + wantErr error + want R // for json and yaml + wantTable *string // for table printer + wantWideTable *string // for wide table printer + template *string // for template printer + wantTemplate *string // for template printer + wantMarkdown *string // for markdown printer +} + +func (c *test[R]) testCmd(t *testing.T) { + require.NotEmpty(t, c.name, "test name must not be empty") + require.NotEmpty(t, c.cmd, "cmd must not be empty") + + t.Setenv(strings.ToUpper(binaryName)+"_FORCE_COLOR", strconv.FormatBool(false)) + + if c.wantErr != nil { + _, _, config := c.newMockConfig(t) + + cmd := newRootCmd(config) + os.Args = append([]string{binaryName}, c.cmd(c.want)...) + + err := cmd.Execute() + if diff := cmp.Diff(c.wantErr, err, testcommon.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + + } + + for _, format := range outputFormats(c) { + format := format + t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { + _, out, config := c.newMockConfig(t) + + viper.Reset() + + cmd := newRootCmd(config) + os.Args = append([]string{binaryName}, c.cmd(c.want)...) + os.Args = append(os.Args, format.Args()...) + + err := cmd.Execute() + require.NoError(t, err) + + format.Validate(t, out.Bytes()) + }) + } +} + +func (c *test[R]) newMockConfig(t *testing.T) (*client.CloudAPI, *bytes.Buffer, *config) { + mock := testclient.NewCloudMockClient(t, c.mocks) + + fs := afero.NewMemMapFs() + if c.fsMocks != nil { + c.fsMocks(fs, c.want) + } + + var ( + out bytes.Buffer + config = &config{ + fs: fs, + cloud: mock, + out: &out, + log: slog.New(slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{})), + comp: &completion.Completion{}, + } + ) + + if c.disableMockClient { + config.cloud = nil + } + + return mock, &out, config +} + +func assertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { + assertContainsPrefix := func(ss []string, prefix string) error { + for _, s := range ss { + if strings.HasPrefix(s, prefix) { + return nil + } + } + return fmt.Errorf("not exhaustive: does not contain %s", prefix) + } + + root := newRootCmd(&config{comp: &completion.Completion{}}) + cmd, args, err := root.Find(args) + require.NoError(t, err) + + cmd.LocalFlags().VisitAll(func(f *pflag.Flag) { + if slices.Contains(exclude, f.Name) { + return + } + require.NoError(t, assertContainsPrefix(args, "--"+f.Name), "please ensure you all available args are used in order to increase coverage or exclude them explicitly") + }) +} + +func mustMarshal(t *testing.T, d any) []byte { + b, err := json.MarshalIndent(d, "", " ") + require.NoError(t, err) + return b +} + +func mustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { + var parts []string + for _, elem := range data { + parts = append(parts, string(mustMarshal(t, elem))) + } + return []byte(strings.Join(parts, "\n---\n")) +} + +func mustJsonDeepCopy[O any](t *testing.T, object O) O { + raw, err := json.Marshal(&object) + require.NoError(t, err) + var copy O + err = json.Unmarshal(raw, ©) + require.NoError(t, err) + return copy +} + +func outputFormats[R any](c *test[R]) []outputFormat[R] { + var formats []outputFormat[R] + + if !pointer.IsZero(c.want) { + formats = append(formats, &jsonOutputFormat[R]{want: c.want}, &yamlOutputFormat[R]{want: c.want}) + } + + if c.wantTable != nil { + formats = append(formats, &tableOutputFormat[R]{table: *c.wantTable}) + } + + if c.wantWideTable != nil { + formats = append(formats, &wideTableOutputFormat[R]{table: *c.wantWideTable}) + } + + if c.template != nil && c.wantTemplate != nil { + formats = append(formats, &templateOutputFormat[R]{template: *c.template, templateOutput: *c.wantTemplate}) + } + + if c.wantMarkdown != nil { + formats = append(formats, &markdownOutputFormat[R]{table: *c.wantMarkdown}) + } + + return formats +} + +type outputFormat[R any] interface { + Args() []string + Validate(t *testing.T, output []byte) +} + +type jsonOutputFormat[R any] struct { + want R +} + +func (o *jsonOutputFormat[R]) Args() []string { + return []string{"-o", "json"} +} + +func (o *jsonOutputFormat[R]) Validate(t *testing.T, output []byte) { + var got R + err := json.Unmarshal(output, &got) + require.NoError(t, err, string(output)) + + if diff := cmp.Diff(o.want, got, testcommon.StrFmtDateComparer()); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type yamlOutputFormat[R any] struct { + want R +} + +func (o *yamlOutputFormat[R]) Args() []string { + return []string{"-o", "yaml"} +} + +func (o *yamlOutputFormat[R]) Validate(t *testing.T, output []byte) { + var got R + err := yaml.Unmarshal(output, &got) + require.NoError(t, err) + + if diff := cmp.Diff(o.want, got, testcommon.StrFmtDateComparer()); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type tableOutputFormat[R any] struct { + table string +} + +func (o *tableOutputFormat[R]) Args() []string { + return []string{"-o", "table"} +} + +func (o *tableOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +type wideTableOutputFormat[R any] struct { + table string +} + +func (o *wideTableOutputFormat[R]) Args() []string { + return []string{"-o", "wide"} +} + +func (o *wideTableOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +type templateOutputFormat[R any] struct { + template string + templateOutput string +} + +func (o *templateOutputFormat[R]) Args() []string { + return []string{"-o", "template", "--template", o.template} +} + +func (o *templateOutputFormat[R]) Validate(t *testing.T, output []byte) { + t.Logf("got following template output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", string(output)) + + if diff := cmp.Diff(strings.TrimSpace(o.templateOutput), strings.TrimSpace(string(output))); diff != "" { + t.Errorf("diff (+got -want):\n %s", diff) + } +} + +type markdownOutputFormat[R any] struct { + table string +} + +func (o *markdownOutputFormat[R]) Args() []string { + return []string{"-o", "markdown"} +} + +func (o *markdownOutputFormat[R]) Validate(t *testing.T, output []byte) { + validateTableRows(t, o.table, string(output)) +} + +func validateTableRows(t *testing.T, want, got string) { + trimAll := func(ss []string) []string { + var res []string + for _, s := range ss { + res = append(res, strings.TrimSpace(s)) + } + return res + } + + var ( + trimmedWant = strings.TrimSpace(want) + trimmedGot = strings.TrimSpace(string(got)) + + wantRows = trimAll(strings.Split(trimmedWant, "\n")) + gotRows = trimAll(strings.Split(trimmedGot, "\n")) + ) + + t.Logf("got following table output:\n\n%s\n\nconsider using this for test comparison if it looks correct.", trimmedGot) + + t.Log(cmp.Diff(trimmedWant, trimmedGot)) + + require.Equal(t, len(wantRows), len(gotRows), "tables have different lengths") + + for i := range wantRows { + wantFields := trimAll(strings.Split(wantRows[i], " ")) + gotFields := trimAll(strings.Split(gotRows[i], " ")) + + require.Equal(t, len(wantFields), len(gotFields), "table fields have different lengths") + + for i := range wantFields { + assert.Equal(t, wantFields[i], gotFields[i]) + } + } +} + +func appendFromFileCommonArgs(args ...string) []string { + return append(args, []string{"-f", "/file.yaml", "--skip-security-prompts", "--bulk-output"}...) +} + +func commonExcludedFileArgs() []string { + return []string{"file", "bulk-output", "skip-security-prompts", "timestamps"} +} diff --git a/cmd/output/printer.go b/cmd/output/printer.go index 6dbe418..f8d66ae 100644 --- a/cmd/output/printer.go +++ b/cmd/output/printer.go @@ -148,7 +148,7 @@ func newPrinter(format, order, tpl string, noHeaders bool, writer io.Writer) (Pr printer = printers.NewYAMLPrinter().WithOut(writer) case "json": printer = printers.NewJSONPrinter().WithOut(writer) - case "table", "wide": + case "table", "wide", "markdown": printer = newTablePrinter(format, order, noHeaders, nil, writer) case "template": tmpl, err := template.New("t").Funcs(sprig.TxtFuncMap()).Parse(tpl) diff --git a/cmd/project-reservations_test.go b/cmd/project-reservations_test.go new file mode 100644 index 0000000..39c89d2 --- /dev/null +++ b/cmd/project-reservations_test.go @@ -0,0 +1,300 @@ +package cmd + +import ( + "testing" + + "github.com/fi-ts/cloud-go/api/client/project" + "github.com/fi-ts/cloud-go/api/models" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/stretchr/testify/mock" +) + +var ( + machineReservation1 = &models.V1MachineReservationResponse{ + Amount: pointer.Pointer(int32(3)), + Description: "for firewalls", + Labels: map[string]string{ + "size.metal-stack.io/reserved-at": "2024-09-19T08:57:40Z", + "size.metal-stack.io/reserved-by": "fits", + }, + Partitionids: []string{"partition-a"}, + Projectid: pointer.Pointer("project-a"), + Sizeid: pointer.Pointer("size-a"), + Tenant: pointer.Pointer("fits"), + } + machineReservation2 = &models.V1MachineReservationResponse{ + Amount: pointer.Pointer(int32(3)), + Description: "for machines", + Labels: map[string]string{ + "size.metal-stack.io/reserved-by": "fits", + }, + Partitionids: []string{"partition-a", "partition-b"}, + Projectid: pointer.Pointer("project-b"), + Sizeid: pointer.Pointer("size-b"), + Tenant: pointer.Pointer("fits"), + } +) + +func Test_ProjectMachineReservationsCmd_MultiResult(t *testing.T) { + tests := []*test[[]*models.V1MachineReservationResponse]{ + { + name: "list", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "list"} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{})), nil).Return(&project.ListMachineReservationsOK{ + Payload: []*models.V1MachineReservationResponse{ + machineReservation2, + machineReservation1, + }, + }, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + machineReservation2, + }, + wantTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +fits project-a size-a 3 partition-a for firewalls +fits project-b size-b 3 partition-a,partition-b for machines +`), + wantWideTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits +fits project-b size-b 3 partition-a,partition-b for machines for machines size.metal-stack.io/reserved-by=fits +`), + template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), + wantTemplate: pointer.Pointer(` +size-a project-a +size-b project-b +`), + wantMarkdown: pointer.Pointer(` +| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|--------|-----------|--------|--------|-------------------------|---------------| +| fits | project-a | size-a | 3 | partition-a | for firewalls | +| fits | project-b | size-b | 3 | partition-a,partition-b | for machines | +`), + }, + // { + // name: "list with filters", + // cmd: func(want []*models.V1MachineReservationResponse) []string { + // args := []string{"project", "list", "--name", "project-1", "--tenant", "metal-stack", "--id", want[0].Meta.ID} + // assertExhaustiveArgs(t, args, "sort-by") + // return args + // }, + // mocks: &client.MetalMockFns{ + // Project: func(mock *mock.Mock) { + // mock.On("FindProjects", testcommon.MatchIgnoreContext(t, project.NewFindProjectsParams().WithBody(&models.V1ProjectFindRequest{ + // Name: "project-1", + // TenantID: "metal-stack", + // ID: "1", + // })), nil).Return(&project.FindProjectsOK{ + // Payload: []*models.V1ProjectResponse{ + // project1, + // }, + // }, nil) + // }, + // }, + // want: []*models.V1ProjectResponse{ + // project1, + // }, + // wantTable: pointer.Pointer(` + // UID TENANT NAME DESCRIPTION LABELS ANNOTATIONS + // 1 metal-stack project-1 project 1 c a=b + // `), + // wantWideTable: pointer.Pointer(` + // UID TENANT NAME DESCRIPTION QUOTAS CLUSTERS/MACHINES/IPS LABELS ANNOTATIONS + // 1 metal-stack project-1 project 1 1/3/2 c a=b + // `), + // template: pointer.Pointer("{{ .meta.id }} {{ .name }}"), + // wantTemplate: pointer.Pointer(` + // 1 project-1 + // `), + // wantMarkdown: pointer.Pointer(` + // | UID | TENANT | NAME | DESCRIPTION | LABELS | ANNOTATIONS | + // |-----|-------------|-----------|-------------|--------|-------------| + // | 1 | metal-stack | project-1 | project 1 | c | a=b | + // `), + // }, + // { + // name: "apply", + // cmd: func(want []*models.V1ProjectResponse) []string { + // return appendFromFileCommonArgs("project", "apply") + // }, + // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { + // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + // }, + // mocks: &client.MetalMockFns{ + // Project: func(mock *mock.Mock) { + // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project1))), nil).Return(nil, &project.CreateProjectConflict{}).Once() + // mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ + // Payload: project1, + // }, nil) + // mock.On("UpdateProject", testcommon.MatchIgnoreContext(t, project.NewUpdateProjectParams().WithBody(projectResponseToUpdate(project1))), nil).Return(&project.UpdateProjectOK{ + // Payload: project1, + // }, nil) + // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project2))), nil).Return(&project.CreateProjectCreated{ + // Payload: project2, + // }, nil) + // }, + // }, + // want: []*models.V1ProjectResponse{ + // project1, + // project2, + // }, + // }, + // { + // name: "create from file", + // cmd: func(want []*models.V1ProjectResponse) []string { + // return appendFromFileCommonArgs("project", "create") + // }, + // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { + // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + // }, + // mocks: &client.MetalMockFns{ + // Project: func(mock *mock.Mock) { + // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project1))), nil).Return(&project.CreateProjectCreated{ + // Payload: project1, + // }, nil) + // }, + // }, + // want: []*models.V1ProjectResponse{ + // project1, + // }, + // }, + // { + // name: "update from file", + // cmd: func(want []*models.V1ProjectResponse) []string { + // return appendFromFileCommonArgs("project", "update") + // }, + // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { + // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + // }, + // mocks: &client.MetalMockFns{ + // Project: func(mock *mock.Mock) { + // mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ + // Payload: project1, + // }, nil) + // mock.On("UpdateProject", testcommon.MatchIgnoreContext(t, project.NewUpdateProjectParams().WithBody(projectResponseToUpdate(project1))), nil).Return(&project.UpdateProjectOK{ + // Payload: project1, + // }, nil) + // }, + // }, + // want: []*models.V1ProjectResponse{ + // project1, + // }, + // }, + // { + // name: "delete from file", + // cmd: func(want []*models.V1ProjectResponse) []string { + // return appendFromFileCommonArgs("project", "delete") + // }, + // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { + // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + // }, + // mocks: &client.MetalMockFns{ + // Project: func(mock *mock.Mock) { + // mock.On("DeleteProject", testcommon.MatchIgnoreContext(t, project.NewDeleteProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.DeleteProjectOK{ + // Payload: project1, + // }, nil) + // }, + // }, + // want: []*models.V1ProjectResponse{ + // project1, + // }, + // }, + } + for _, tt := range tests { + tt.testCmd(t) + } +} + +// func Test_ProjectCmd_SingleResult(t *testing.T) { +// tests := []*test[*models.V1ProjectResponse]{ +// { +// name: "describe", +// cmd: func(want *models.V1ProjectResponse) []string { +// return []string{"project", "describe", want.Meta.ID} +// }, +// mocks: &client.MetalMockFns{ +// Project: func(mock *mock.Mock) { +// mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ +// Payload: project1, +// }, nil) +// }, +// }, +// want: project1, +// wantTable: pointer.Pointer(` +// UID TENANT NAME DESCRIPTION LABELS ANNOTATIONS +// 1 metal-stack project-1 project 1 c a=b +// `), +// wantWideTable: pointer.Pointer(` +// UID TENANT NAME DESCRIPTION QUOTAS CLUSTERS/MACHINES/IPS LABELS ANNOTATIONS +// 1 metal-stack project-1 project 1 1/3/2 c a=b +// `), +// template: pointer.Pointer("{{ .meta.id }} {{ .name }}"), +// wantTemplate: pointer.Pointer(` +// 1 project-1 +// `), +// wantMarkdown: pointer.Pointer(` +// | UID | TENANT | NAME | DESCRIPTION | LABELS | ANNOTATIONS | +// |-----|-------------|-----------|-------------|--------|-------------| +// | 1 | metal-stack | project-1 | project 1 | c | a=b | +// `), +// }, +// { +// name: "delete", +// cmd: func(want *models.V1ProjectResponse) []string { +// return []string{"project", "rm", want.Meta.ID} +// }, +// mocks: &client.MetalMockFns{ +// Project: func(mock *mock.Mock) { +// mock.On("DeleteProject", testcommon.MatchIgnoreContext(t, project.NewDeleteProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.DeleteProjectOK{ +// Payload: project1, +// }, nil) +// }, +// }, +// want: project1, +// }, +// { +// name: "create", +// cmd: func(want *models.V1ProjectResponse) []string { +// args := []string{"project", "create", +// "--name", want.Name, +// "--description", want.Description, +// "--tenant", want.TenantID, +// "--label", strings.Join(want.Meta.Labels, ","), +// "--annotation", strings.Join(genericcli.MapToLabels(want.Meta.Annotations), ","), +// "--cluster-quota", strconv.FormatInt(int64(want.Quotas.Cluster.Quota), 10), +// "--machine-quota", strconv.FormatInt(int64(want.Quotas.Machine.Quota), 10), +// "--ip-quota", strconv.FormatInt(int64(want.Quotas.IP.Quota), 10), +// } +// assertExhaustiveArgs(t, args, commonExcludedFileArgs()...) +// return args +// }, +// mocks: &client.MetalMockFns{ +// Project: func(mock *mock.Mock) { +// p := project1 +// p.Meta.ID = "" +// p.Meta.Version = 0 +// p.Quotas.Cluster.Used = 0 +// p.Quotas.IP.Used = 0 +// p.Quotas.Machine.Used = 0 +// mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(p))), nil).Return(&project.CreateProjectCreated{ +// Payload: project1, +// }, nil) +// }, +// }, +// want: project1, +// }, +// } +// for _, tt := range tests { +// tt.testCmd(t) +// } +// } diff --git a/cmd/root.go b/cmd/root.go index dbc0baf..519edb4 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -36,6 +36,26 @@ type config struct { listPrinter printers.Printer } +// Execute is the entrypoint of the cloudctl application +func Execute() { + // the config will be provided with more values on cobra init + // cobra flags do not work so early in the game + c := &config{ + fs: afero.NewOsFs(), + out: os.Stdout, + comp: &completion.Completion{}, + } + + cmd := newRootCmd(c) + err := cmd.Execute() + if err != nil { + if viper.GetBool("debug") { + panic(err) + } + os.Exit(1) + } +} + func newRootCmd(cfg *config) *cobra.Command { rootCmd := &cobra.Command{ Use: binaryName, @@ -92,26 +112,6 @@ func newRootCmd(cfg *config) *cobra.Command { return rootCmd } -// Execute is the entrypoint of the cloudctl application -func Execute() { - // the config will be provided with more values on cobra init - // cobra flags do not work so early in the game - c := &config{ - fs: afero.NewOsFs(), - out: os.Stdout, - comp: &completion.Completion{}, - } - - cmd := newRootCmd(c) - err := cmd.Execute() - if err != nil { - if viper.GetBool("debug") { - panic(err) - } - os.Exit(1) - } -} - func initConfigWithViperCtx(cfg *config) error { viper.SetEnvPrefix(strings.ToUpper(binaryName)) viper.SetEnvKeyReplacer(strings.NewReplacer("-", "_")) @@ -163,6 +163,10 @@ func initConfigWithViperCtx(cfg *config) error { } cfg.log = slog.New(slog.NewJSONHandler(os.Stdout, opts)) + if cfg.cloud != nil { + return nil + } + driverURL := viper.GetString("url") if driverURL == "" && ctx.ApiURL != "" { driverURL = ctx.ApiURL diff --git a/go.mod b/go.mod index 1e31e22..d10a1f9 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a + github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578 github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 @@ -26,8 +26,10 @@ require ( github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/afero v1.11.0 github.com/spf13/cobra v1.8.1 + github.com/spf13/pflag v1.0.5 github.com/spf13/viper v1.19.0 github.com/stretchr/testify v1.9.0 + github.com/undefinedlabs/go-mpatch v1.0.7 golang.org/x/sync v0.8.0 gopkg.in/yaml.v3 v3.0.1 k8s.io/api v0.29.8 @@ -149,7 +151,6 @@ require ( github.com/segmentio/asm v1.2.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/spf13/cast v1.7.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff // indirect diff --git a/go.sum b/go.sum index 6acdddb..2d2355a 100644 --- a/go.sum +++ b/go.sum @@ -82,6 +82,8 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= +github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -90,8 +92,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a h1:VZNmN6mlN7fyG6lHEllA0cdEJRjqiLZX6CnoeOkkvNI= -github.com/fi-ts/cloud-go v0.28.1-0.20240918115530-982c3cbb2d7a/go.mod h1:R7JMkC92eGvxkkMO1oP6lEevBH86DFiO9H9mo7YD5Sw= +github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578 h1:TS+trg3j7YOZJatONv97bg3uCKKu+FytwoWY1RLJB2s= +github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= @@ -391,6 +393,8 @@ github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/undefinedlabs/go-mpatch v1.0.7 h1:943FMskd9oqfbZV0qRVKOUsXQhTLXL0bQTVbQSpzmBs= +github.com/undefinedlabs/go-mpatch v1.0.7/go.mod h1:TyJZDQ/5AgyN7FSLiBJ8RO9u2c6wbtRvK827b6AVqY4= github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs= github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho= github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0= From d07e76acf3e5254db814b06e5d8327e97b32fa80 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 12:57:19 +0200 Subject: [PATCH 06/19] Complete tests. --- cmd/common_test.go | 17 +- cmd/project-reservations.go | 5 + cmd/project-reservations_test.go | 422 +++++++++++----------- cmd/tableprinters/project-reservations.go | 9 +- 4 files changed, 231 insertions(+), 222 deletions(-) diff --git a/cmd/common_test.go b/cmd/common_test.go index aa6c9cc..81826ad 100644 --- a/cmd/common_test.go +++ b/cmd/common_test.go @@ -164,14 +164,15 @@ func mustMarshalToMultiYAML[R any](t *testing.T, data []R) []byte { return []byte(strings.Join(parts, "\n---\n")) } -func mustJsonDeepCopy[O any](t *testing.T, object O) O { - raw, err := json.Marshal(&object) - require.NoError(t, err) - var copy O - err = json.Unmarshal(raw, ©) - require.NoError(t, err) - return copy -} +// might come in handy later: +// func mustJsonDeepCopy[O any](t *testing.T, object O) O { +// raw, err := json.Marshal(&object) +// require.NoError(t, err) +// var copy O +// err = json.Unmarshal(raw, ©) +// require.NoError(t, err) +// return copy +// } func outputFormats[R any](c *test[R]) []outputFormat[R] { var formats []outputFormat[R] diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 3cd7925..7e5aec5 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -1,6 +1,7 @@ package cmd import ( + "errors" "fmt" "github.com/fi-ts/cloud-go/api/client/project" @@ -135,6 +136,10 @@ func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequ WithBody(rq). WithForce(pointer.Pointer(viper.GetBool("force"))), nil) if err != nil { + var r *project.CreateMachineReservationConflict + if errors.As(err, &r) { + return nil, genericcli.AlreadyExistsError() + } return nil, err } diff --git a/cmd/project-reservations_test.go b/cmd/project-reservations_test.go index 39c89d2..585b9ec 100644 --- a/cmd/project-reservations_test.go +++ b/cmd/project-reservations_test.go @@ -1,6 +1,8 @@ package cmd import ( + "strconv" + "strings" "testing" "github.com/fi-ts/cloud-go/api/client/project" @@ -8,7 +10,9 @@ import ( testclient "github.com/fi-ts/cloud-go/test/client" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/spf13/afero" "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" ) var ( @@ -81,220 +85,216 @@ size-b project-b | fits | project-b | size-b | 3 | partition-a,partition-b | for machines | `), }, - // { - // name: "list with filters", - // cmd: func(want []*models.V1MachineReservationResponse) []string { - // args := []string{"project", "list", "--name", "project-1", "--tenant", "metal-stack", "--id", want[0].Meta.ID} - // assertExhaustiveArgs(t, args, "sort-by") - // return args - // }, - // mocks: &client.MetalMockFns{ - // Project: func(mock *mock.Mock) { - // mock.On("FindProjects", testcommon.MatchIgnoreContext(t, project.NewFindProjectsParams().WithBody(&models.V1ProjectFindRequest{ - // Name: "project-1", - // TenantID: "metal-stack", - // ID: "1", - // })), nil).Return(&project.FindProjectsOK{ - // Payload: []*models.V1ProjectResponse{ - // project1, - // }, - // }, nil) - // }, - // }, - // want: []*models.V1ProjectResponse{ - // project1, - // }, - // wantTable: pointer.Pointer(` - // UID TENANT NAME DESCRIPTION LABELS ANNOTATIONS - // 1 metal-stack project-1 project 1 c a=b - // `), - // wantWideTable: pointer.Pointer(` - // UID TENANT NAME DESCRIPTION QUOTAS CLUSTERS/MACHINES/IPS LABELS ANNOTATIONS - // 1 metal-stack project-1 project 1 1/3/2 c a=b - // `), - // template: pointer.Pointer("{{ .meta.id }} {{ .name }}"), - // wantTemplate: pointer.Pointer(` - // 1 project-1 - // `), - // wantMarkdown: pointer.Pointer(` - // | UID | TENANT | NAME | DESCRIPTION | LABELS | ANNOTATIONS | - // |-----|-------------|-----------|-------------|--------|-------------| - // | 1 | metal-stack | project-1 | project 1 | c | a=b | - // `), - // }, - // { - // name: "apply", - // cmd: func(want []*models.V1ProjectResponse) []string { - // return appendFromFileCommonArgs("project", "apply") - // }, - // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { - // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) - // }, - // mocks: &client.MetalMockFns{ - // Project: func(mock *mock.Mock) { - // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project1))), nil).Return(nil, &project.CreateProjectConflict{}).Once() - // mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ - // Payload: project1, - // }, nil) - // mock.On("UpdateProject", testcommon.MatchIgnoreContext(t, project.NewUpdateProjectParams().WithBody(projectResponseToUpdate(project1))), nil).Return(&project.UpdateProjectOK{ - // Payload: project1, - // }, nil) - // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project2))), nil).Return(&project.CreateProjectCreated{ - // Payload: project2, - // }, nil) - // }, - // }, - // want: []*models.V1ProjectResponse{ - // project1, - // project2, - // }, - // }, - // { - // name: "create from file", - // cmd: func(want []*models.V1ProjectResponse) []string { - // return appendFromFileCommonArgs("project", "create") - // }, - // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { - // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) - // }, - // mocks: &client.MetalMockFns{ - // Project: func(mock *mock.Mock) { - // mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(project1))), nil).Return(&project.CreateProjectCreated{ - // Payload: project1, - // }, nil) - // }, - // }, - // want: []*models.V1ProjectResponse{ - // project1, - // }, - // }, - // { - // name: "update from file", - // cmd: func(want []*models.V1ProjectResponse) []string { - // return appendFromFileCommonArgs("project", "update") - // }, - // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { - // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) - // }, - // mocks: &client.MetalMockFns{ - // Project: func(mock *mock.Mock) { - // mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ - // Payload: project1, - // }, nil) - // mock.On("UpdateProject", testcommon.MatchIgnoreContext(t, project.NewUpdateProjectParams().WithBody(projectResponseToUpdate(project1))), nil).Return(&project.UpdateProjectOK{ - // Payload: project1, - // }, nil) - // }, - // }, - // want: []*models.V1ProjectResponse{ - // project1, - // }, - // }, - // { - // name: "delete from file", - // cmd: func(want []*models.V1ProjectResponse) []string { - // return appendFromFileCommonArgs("project", "delete") - // }, - // fsMocks: func(fs afero.Fs, want []*models.V1ProjectResponse) { - // require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) - // }, - // mocks: &client.MetalMockFns{ - // Project: func(mock *mock.Mock) { - // mock.On("DeleteProject", testcommon.MatchIgnoreContext(t, project.NewDeleteProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.DeleteProjectOK{ - // Payload: project1, - // }, nil) - // }, - // }, - // want: []*models.V1ProjectResponse{ - // project1, - // }, - // }, + { + name: "list with filters", + cmd: func(want []*models.V1MachineReservationResponse) []string { + args := []string{"project", "machine-reservation", "list", "--tenant", *want[0].Tenant, "--project", *want[0].Projectid, "--size", *want[0].Sizeid} + assertExhaustiveArgs(t, args, "sort-by") + return args + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{ + Projectid: pointer.Pointer("project-a"), + Sizeid: pointer.Pointer("size-a"), + Tenant: pointer.Pointer("fits"), + })), nil).Return(&project.ListMachineReservationsOK{ + Payload: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + wantTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +fits project-a size-a 3 partition-a for firewalls + `), + wantWideTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits + + `), + template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), + wantTemplate: pointer.Pointer(` +size-a project-a + `), + wantMarkdown: pointer.Pointer(` +| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|--------|-----------|--------|--------|-------------|---------------| +| fits | project-a | size-a | 3 | partition-a | for firewalls | + `), + }, + { + name: "apply", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "apply") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(nil, &project.CreateMachineReservationConflict{}).Once() + mock.On("UpdateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewUpdateMachineReservationParams(). + WithBody(toMachineReservationUpdateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.UpdateMachineReservationOK{Payload: machineReservation1}, nil) + + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation2)).WithForce(pointer.Pointer(false))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation2}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + machineReservation2, + }, + }, + { + name: "create from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "create") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + { + name: "update from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "update") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("UpdateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewUpdateMachineReservationParams(). + WithBody(toMachineReservationUpdateRequest(machineReservation1)).WithForce(pointer.Pointer(false))), nil). + Return(&project.UpdateMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, + { + name: "delete from file", + cmd: func(want []*models.V1MachineReservationResponse) []string { + return appendFromFileCommonArgs("project", "machine-reservation", "delete") + }, + fsMocks: func(fs afero.Fs, want []*models.V1MachineReservationResponse) { + require.NoError(t, afero.WriteFile(fs, "/file.yaml", mustMarshalToMultiYAML(t, want), 0755)) + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams(). + WithProject(machineReservation1.Projectid).WithSize(machineReservation1.Sizeid)), nil). + Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: []*models.V1MachineReservationResponse{ + machineReservation1, + }, + }, } for _, tt := range tests { tt.testCmd(t) } } -// func Test_ProjectCmd_SingleResult(t *testing.T) { -// tests := []*test[*models.V1ProjectResponse]{ -// { -// name: "describe", -// cmd: func(want *models.V1ProjectResponse) []string { -// return []string{"project", "describe", want.Meta.ID} -// }, -// mocks: &client.MetalMockFns{ -// Project: func(mock *mock.Mock) { -// mock.On("FindProject", testcommon.MatchIgnoreContext(t, project.NewFindProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.FindProjectOK{ -// Payload: project1, -// }, nil) -// }, -// }, -// want: project1, -// wantTable: pointer.Pointer(` -// UID TENANT NAME DESCRIPTION LABELS ANNOTATIONS -// 1 metal-stack project-1 project 1 c a=b -// `), -// wantWideTable: pointer.Pointer(` -// UID TENANT NAME DESCRIPTION QUOTAS CLUSTERS/MACHINES/IPS LABELS ANNOTATIONS -// 1 metal-stack project-1 project 1 1/3/2 c a=b -// `), -// template: pointer.Pointer("{{ .meta.id }} {{ .name }}"), -// wantTemplate: pointer.Pointer(` -// 1 project-1 -// `), -// wantMarkdown: pointer.Pointer(` -// | UID | TENANT | NAME | DESCRIPTION | LABELS | ANNOTATIONS | -// |-----|-------------|-----------|-------------|--------|-------------| -// | 1 | metal-stack | project-1 | project 1 | c | a=b | -// `), -// }, -// { -// name: "delete", -// cmd: func(want *models.V1ProjectResponse) []string { -// return []string{"project", "rm", want.Meta.ID} -// }, -// mocks: &client.MetalMockFns{ -// Project: func(mock *mock.Mock) { -// mock.On("DeleteProject", testcommon.MatchIgnoreContext(t, project.NewDeleteProjectParams().WithID(project1.Meta.ID)), nil).Return(&project.DeleteProjectOK{ -// Payload: project1, -// }, nil) -// }, -// }, -// want: project1, -// }, -// { -// name: "create", -// cmd: func(want *models.V1ProjectResponse) []string { -// args := []string{"project", "create", -// "--name", want.Name, -// "--description", want.Description, -// "--tenant", want.TenantID, -// "--label", strings.Join(want.Meta.Labels, ","), -// "--annotation", strings.Join(genericcli.MapToLabels(want.Meta.Annotations), ","), -// "--cluster-quota", strconv.FormatInt(int64(want.Quotas.Cluster.Quota), 10), -// "--machine-quota", strconv.FormatInt(int64(want.Quotas.Machine.Quota), 10), -// "--ip-quota", strconv.FormatInt(int64(want.Quotas.IP.Quota), 10), -// } -// assertExhaustiveArgs(t, args, commonExcludedFileArgs()...) -// return args -// }, -// mocks: &client.MetalMockFns{ -// Project: func(mock *mock.Mock) { -// p := project1 -// p.Meta.ID = "" -// p.Meta.Version = 0 -// p.Quotas.Cluster.Used = 0 -// p.Quotas.IP.Used = 0 -// p.Quotas.Machine.Used = 0 -// mock.On("CreateProject", testcommon.MatchIgnoreContext(t, project.NewCreateProjectParams().WithBody(projectResponseToCreate(p))), nil).Return(&project.CreateProjectCreated{ -// Payload: project1, -// }, nil) -// }, -// }, -// want: project1, -// }, -// } -// for _, tt := range tests { -// tt.testCmd(t) -// } -// } +func Test_ProjectMachineReservationsCmd_SingleResult(t *testing.T) { + tests := []*test[*models.V1MachineReservationResponse]{ + { + name: "describe", + cmd: func(want *models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "describe", *want.Projectid, *want.Sizeid} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{})), nil).Return(&project.ListMachineReservationsOK{ + Payload: []*models.V1MachineReservationResponse{ + machineReservation2, + machineReservation1, + }, + }, nil) + }, + }, + want: machineReservation1, + wantTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +fits project-a size-a 3 partition-a for firewalls +`), + wantWideTable: pointer.Pointer(` +TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits +`), + template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), + wantTemplate: pointer.Pointer(` +size-a project-a +`), + wantMarkdown: pointer.Pointer(` +| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|--------|-----------|--------|--------|-------------|---------------| +| fits | project-a | size-a | 3 | partition-a | for firewalls | +`), + }, + { + name: "delete", + cmd: func(want *models.V1MachineReservationResponse) []string { + return []string{"project", "machine-reservation", "rm", *want.Projectid, *want.Sizeid} + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams(). + WithProject(machineReservation1.Projectid).WithSize(machineReservation1.Sizeid)), nil). + Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) + }, + }, + want: machineReservation1, + }, + { + name: "create", + cmd: func(want *models.V1MachineReservationResponse) []string { + args := []string{"project", "machine-reservation", "create", + "--amount", strconv.Itoa(int(*want.Amount)), //nolint:gosec + "--description", want.Description, + "--project", *want.Projectid, + "--force", + "--partitions", strings.Join(want.Partitionids, ","), + "--size", *want.Sizeid, + } + + assertExhaustiveArgs(t, args, commonExcludedFileArgs()...) + return args + }, + mocks: &testclient.CloudMockFns{ + Project: func(mock *mock.Mock) { + mock.On("CreateMachineReservation", testcommon.MatchIgnoreContext(t, project.NewCreateMachineReservationParams(). + WithBody(toMachineReservationCreateRequest(machineReservation1)).WithForce(pointer.Pointer(true))), nil). + Return(&project.CreateMachineReservationCreated{Payload: machineReservation1}, nil) + }, + }, + want: machineReservation1, + }, + } + for _, tt := range tests { + tt.testCmd(t) + } +} diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index 28e97c7..4a018a8 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -2,6 +2,7 @@ package tableprinters import ( "fmt" + "sort" "strconv" "strings" @@ -22,6 +23,8 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat } for _, p := range data { + sort.Strings(p.Partitionids) + row := []string{ pointer.SafeDeref(p.Tenant), pointer.SafeDeref(p.Projectid), @@ -32,13 +35,13 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat } if wide { - labels := []string{} + var labels []string for k, v := range p.Labels { labels = append(labels, k+"="+v) } - lbls := strings.Join(labels, "\n") + sort.Strings(labels) - row = append(row, p.Description, lbls) + row = append(row, p.Description, strings.Join(labels, "\n")) } rows = append(rows, row) From 33587d9ce57c4d83620eef83096e0d166beb8c23 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 13:01:07 +0200 Subject: [PATCH 07/19] Sort the usage. --- cmd/project-reservations.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 7e5aec5..16c7c42 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -103,6 +103,7 @@ func newMachineReservationsCmd(c *config) *cobra.Command { genericcli.Must(usageCmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) genericcli.Must(usageCmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) genericcli.Must(usageCmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) + genericcli.AddSortFlag(usageCmd, sorters.MachineReservationsUsageSorter()) return genericcli.NewCmds(cmdsConfig, usageCmd) } @@ -212,6 +213,11 @@ func (m machineReservationsCmd) machineReservationsUsage() error { return err } + err = sorters.MachineReservationsUsageSorter().SortBy(resp.Payload) + if err != nil { + return err + } + return m.listPrinter.Print(resp.Payload) } From 8f52e3bae75b3f6d9cdf3dd942d165c905bb7d32 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 13:04:27 +0200 Subject: [PATCH 08/19] Add allocations to wide. --- cmd/tableprinters/project-reservations.go | 42 ++++++++++++----------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index 4a018a8..e52ea8b 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -22,26 +22,26 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat header = append(header, "Labels") } - for _, p := range data { - sort.Strings(p.Partitionids) + for _, rv := range data { + sort.Strings(rv.Partitionids) row := []string{ - pointer.SafeDeref(p.Tenant), - pointer.SafeDeref(p.Projectid), - pointer.SafeDeref(p.Sizeid), - strconv.Itoa(int(pointer.SafeDeref(p.Amount))), - strings.Join(p.Partitionids, ","), - genericcli.TruncateEnd(p.Description, 50), + pointer.SafeDeref(rv.Tenant), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Sizeid), + strconv.Itoa(int(pointer.SafeDeref(rv.Amount))), + strings.Join(rv.Partitionids, ","), + genericcli.TruncateEnd(rv.Description, 50), } if wide { var labels []string - for k, v := range p.Labels { + for k, v := range rv.Labels { labels = append(labels, k+"="+v) } sort.Strings(labels) - row = append(row, p.Description, strings.Join(labels, "\n")) + row = append(row, rv.Description, strings.Join(labels, "\n")) } rows = append(rows, row) @@ -61,27 +61,29 @@ func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineRes ) if wide { - header = append(header, "Labels") + header = append(header, "Allocations", "Labels") } - for _, p := range data { + for _, rv := range data { reservations := "0" - if pointer.SafeDeref(p.Reservations) > 0 { - unused := pointer.SafeDeref(p.Reservations) - pointer.SafeDeref(p.Usedreservations) - reservations = fmt.Sprintf("%d (%d/%d used)", unused, pointer.SafeDeref(p.Usedreservations), pointer.SafeDeref(p.Reservations)) + if pointer.SafeDeref(rv.Reservations) > 0 { + unused := pointer.SafeDeref(rv.Reservations) - pointer.SafeDeref(rv.Usedreservations) + reservations = fmt.Sprintf("%d (%d/%d used)", unused, pointer.SafeDeref(rv.Usedreservations), pointer.SafeDeref(rv.Reservations)) } row := []string{ - pointer.SafeDeref(p.Tenant), - pointer.SafeDeref(p.Projectid), - pointer.SafeDeref(p.Partitionid), - pointer.SafeDeref(p.Sizeid), + pointer.SafeDeref(rv.Tenant), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Partitionid), + pointer.SafeDeref(rv.Sizeid), reservations, } if wide { + row = append(row, fmt.Sprintf("%d", pointer.SafeDeref(rv.Projectallocations))) + labels := []string{} - for k, v := range p.Labels { + for k, v := range rv.Labels { labels = append(labels, k+"="+v) } lbls := strings.Join(labels, "\n") From ef25ec9f00c49eeb32234ef3190e2df51a2edfcf Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 13:36:18 +0200 Subject: [PATCH 09/19] Fix version test. --- cmd/version.go | 3 +- cmd/version_test.go | 68 +++++++++++++++++++-------------------------- pkg/api/version.go | 4 +-- 3 files changed, 32 insertions(+), 43 deletions(-) diff --git a/cmd/version.go b/cmd/version.go index ff2e439..4bdb98a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -3,6 +3,7 @@ package cmd import ( "fmt" + "github.com/fi-ts/cloud-go/api/client/version" "github.com/fi-ts/cloudctl/pkg/api" "github.com/metal-stack/v" "github.com/spf13/cobra" @@ -18,7 +19,7 @@ func newVersionCmd(c *config) *cobra.Command { Client: v.V.String(), } - resp, err := c.cloud.Version.Info(nil, nil) + resp, err := c.cloud.Version.Info(version.NewInfoParams(), nil) if err == nil { v.Server = resp.Payload } diff --git a/cmd/version_test.go b/cmd/version_test.go index 67f3976..ff1520b 100644 --- a/cmd/version_test.go +++ b/cmd/version_test.go @@ -1,57 +1,45 @@ package cmd import ( - "bytes" - "fmt" "runtime" "testing" - "github.com/fi-ts/cloud-go/api/client" "github.com/fi-ts/cloud-go/api/client/version" "github.com/fi-ts/cloud-go/api/models" - mockversion "github.com/fi-ts/cloud-go/test/mocks/version" + testclient "github.com/fi-ts/cloud-go/test/client" + "github.com/fi-ts/cloudctl/pkg/api" "github.com/metal-stack/metal-lib/pkg/pointer" + "github.com/metal-stack/metal-lib/pkg/testcommon" "github.com/metal-stack/v" - "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" ) func Test_newVersionCmd(t *testing.T) { - v.BuildDate = "1.1.1970" - v.GitSHA1 = "abcdef" - v.Revision = "v0.0.0" - v.Version = "v0.0.0" - - mockVersionService := new(mockversion.ClientService) - cloud := client.CloudAPI{ - Version: mockVersionService, + tests := []*test[*api.Version]{ + { + name: "version", + cmd: func(want *api.Version) []string { + return []string{"version"} + }, + mocks: &testclient.CloudMockFns{ + Version: func(mock *mock.Mock) { + mock.On("Info", testcommon.MatchIgnoreContext(t, version.NewInfoParams()), nil).Return(&version.InfoOK{ + Payload: &models.RestVersion{ + Version: pointer.Pointer("server v1.0.0"), + }, + }, nil) + }, + }, + want: &api.Version{ + Client: "client v1.0.0, " + runtime.Version(), + Server: &models.RestVersion{ + Version: pointer.Pointer("server v1.0.0"), + }, + }, + }, } - - var out bytes.Buffer - - mockVersionService.On("Info", mock.Anything, mock.Anything).Return(&version.InfoOK{Payload: &models.RestVersion{Name: pointer.Pointer("cloudctl")}}, nil) - cfg := &config{ - cloud: &cloud, - describePrinter: defaultToYAMLPrinter(&out), + for _, tt := range tests { + v.Version = "client v1.0.0" + tt.testCmd(t) } - cmd := newVersionCmd(cfg) - - err := cmd.Execute() - if err != nil { - t.Fatal(err) - } - - mockVersionService.AssertExpectations(t) - - expected := fmt.Sprintf(`--- -Client: v0.0.0 (abcdef), v0.0.0, 1.1.1970, %s -Server: - builddate: null - gitsha1: null - min_client_version: null - name: cloudctl - revision: null - version: null -`, runtime.Version()) - assert.Equal(t, expected, string(out.String())) } diff --git a/pkg/api/version.go b/pkg/api/version.go index fd8131c..e7f9cdc 100644 --- a/pkg/api/version.go +++ b/pkg/api/version.go @@ -5,6 +5,6 @@ import ( ) type Version struct { - Client string `yaml:"client"` - Server *cloudmodels.RestVersion `yaml:"server,omitempty"` + Client string `json:"client" yaml:"client"` + Server *cloudmodels.RestVersion `json:"server,omitempty" yaml:"server,omitempty"` } From dbbb498035050d72e9e6f2a16bcb37f2efda9e0f Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 14:10:39 +0200 Subject: [PATCH 10/19] Fix. --- cmd/project-reservations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 16c7c42..299bfae 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -175,7 +175,7 @@ func (m machineReservationsCmd) Get(id ...string) (*models.V1MachineReservationR return rv, nil } - return nil, fmt.Errorf("no reservation found with size %q for project %q", id[1], id[0]) + return nil, fmt.Errorf("no reservation found with size %q for project %q", id[m.sizeIndex()], id[m.projectIndex()]) } func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, error) { From 1394e6a3e1bec29eb35d25fb6e766b2b8fab335e Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 19 Sep 2024 15:10:34 +0200 Subject: [PATCH 11/19] Update dep. --- go.mod | 2 +- go.sum | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/go.mod b/go.mod index d10a1f9..b5d630b 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jinzhu/now v1.1.5 github.com/metal-stack/duros-go v0.5.1 github.com/metal-stack/metal-go v0.34.0 - github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4 + github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1 github.com/metal-stack/updater v1.2.2 github.com/metal-stack/v v1.0.3 github.com/olekukonko/tablewriter v0.0.5 diff --git a/go.sum b/go.sum index 2d2355a..29d41cf 100644 --- a/go.sum +++ b/go.sum @@ -82,8 +82,6 @@ github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yez github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= -github.com/emicklei/go-restful/v3 v3.12.1 h1:PJMDIM/ak7btuL8Ex0iYET9hxM3CI2sjZtzpL63nKAU= -github.com/emicklei/go-restful/v3 v3.12.1/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -281,8 +279,8 @@ github.com/metal-stack/duros-go v0.5.1 h1:baE/c0AKy9sTOztPhILJLaoMmT17Ajsb+xRMfH github.com/metal-stack/duros-go v0.5.1/go.mod h1:Z9mzI9ds2gI8zHC03PUCQvmlWa7WAPukDCUhowtVeOk= github.com/metal-stack/metal-go v0.34.0 h1:X4Wlt2OAhsu3Lq+rHSWnWeASmX6CYvOxnL6DxmjnzbU= github.com/metal-stack/metal-go v0.34.0/go.mod h1:3MJTYCS4YJz8D8oteTKhjpaAKNMMjMKYDrIy9awHGtQ= -github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4 h1:yRaTmSEn+aRLl1g/9uXmVLJe0UnwZOmKVYJYfSQnEkI= -github.com/metal-stack/metal-lib v0.18.3-0.20240919071254-1f2cefefaca4/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= +github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1 h1:nzv+w8mFFnZO3KqEflMlAKlyJjIWYGIgMBq2R4ZCq00= +github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= github.com/metal-stack/security v0.8.1 h1:4zmVUxZvDWShVvVIxM3XhIv7pTmPe9DvACRIHW6YTsk= github.com/metal-stack/security v0.8.1/go.mod h1:OO8ZilZO6fUV5QEmwc7HP/RAjqYrGQxXoYIddJ9TvqE= github.com/metal-stack/updater v1.2.2 h1:gnUrnQgfT20QFMDtFBY89opKoBAkdeI/8T2iwMHNdxs= From 19f95ab496239bbbbb2a23103faa510b481f8de9 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 25 Sep 2024 10:39:25 +0200 Subject: [PATCH 12/19] Update. --- cmd/project-reservations.go | 64 +++++++------------ cmd/project-reservations_test.go | 76 ++++++++--------------- cmd/sorters/project-reservations.go | 10 ++- cmd/tableprinters/project-reservations.go | 3 +- go.mod | 2 +- go.sum | 4 +- 6 files changed, 62 insertions(+), 97 deletions(-) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 299bfae..0548933 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -2,7 +2,6 @@ package cmd import ( "errors" - "fmt" "github.com/fi-ts/cloud-go/api/client/project" "github.com/fi-ts/cloud-go/api/models" @@ -24,16 +23,15 @@ func newMachineReservationsCmd(c *config) *cobra.Command { } cmdsConfig := &genericcli.CmdsConfig[*models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, *models.V1MachineReservationResponse]{ - BinaryName: binaryName, - MultiArgGenericCLI: genericcli.NewGenericMultiArgCLI(w).WithFS(c.fs), - Args: []string{"project", "size"}, - Singular: "machine-reservation", - Plural: "machine-reservations", - Description: "manage machine reservations, ids must be provided in the form @", - Sorter: sorters.MachineReservationsSorter(), - ValidArgsFn: c.comp.MachineReservationListCompletion, - DescribePrinter: func() printers.Printer { return c.describePrinter }, - ListPrinter: func() printers.Printer { return c.listPrinter }, + BinaryName: binaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.fs), + Singular: "machine-reservation", + Plural: "machine-reservations", + Description: "manage machine reservations, ids must be provided in the form @", + Sorter: sorters.MachineReservationsSorter(), + ValidArgsFn: c.comp.MachineReservationListCompletion, + DescribePrinter: func() printers.Printer { return c.describePrinter }, + ListPrinter: func() printers.Printer { return c.listPrinter }, ListCmdMutateFn: func(cmd *cobra.Command) { cmd.Flags().String("project", "", "show reservations of given project") cmd.Flags().String("size", "", "show reservations of given size") @@ -73,17 +71,16 @@ func newMachineReservationsCmd(c *config) *cobra.Command { }, nil }, UpdateRequestFromCLI: func(args []string) (*models.V1MachineReservationUpdateRequest, error) { - ids, err := genericcli.GetExactlyNArgs(2, args) + id, err := genericcli.GetExactlyOneArg(args) if err != nil { return nil, err } return &models.V1MachineReservationUpdateRequest{ + ID: &id, Amount: pointer.PointerOrNil(viper.GetInt32("amount")), Description: pointer.PointerOrNil(viper.GetString("description")), Partitionids: viper.GetStringSlice("partitions"), - Projectid: &ids[w.projectIndex()], - Sizeid: &ids[w.sizeIndex()], }, nil }, } @@ -108,8 +105,11 @@ func newMachineReservationsCmd(c *config) *cobra.Command { return genericcli.NewCmds(cmdsConfig, usageCmd) } -func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) ([]string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { - return []string{*r.Projectid, *r.Sizeid}, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil +func (m machineReservationsCmd) Convert(r *models.V1MachineReservationResponse) (string, *models.V1MachineReservationCreateRequest, *models.V1MachineReservationUpdateRequest, error) { + if r.ID == nil { + return "", nil, nil, errors.New("id is not defined") + } + return *r.ID, toMachineReservationCreateRequest(r), toMachineReservationUpdateRequest(r), nil } func toMachineReservationCreateRequest(r *models.V1MachineReservationResponse) *models.V1MachineReservationCreateRequest { @@ -147,10 +147,8 @@ func (m machineReservationsCmd) Create(rq *models.V1MachineReservationCreateRequ return resp.Payload, nil } -func (m machineReservationsCmd) Delete(id ...string) (*models.V1MachineReservationResponse, error) { - resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams(). - WithProject(pointer.Pointer(id[m.projectIndex()])). - WithSize(pointer.Pointer(id[m.sizeIndex()])), nil) +func (m machineReservationsCmd) Delete(id string) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.DeleteMachineReservation(project.NewDeleteMachineReservationParams().WithID(id), nil) if err != nil { return nil, err } @@ -158,24 +156,13 @@ func (m machineReservationsCmd) Delete(id ...string) (*models.V1MachineReservati return resp.Payload, nil } -func (m machineReservationsCmd) Get(id ...string) (*models.V1MachineReservationResponse, error) { - all, err := m.List() +func (m machineReservationsCmd) Get(id string) (*models.V1MachineReservationResponse, error) { + resp, err := m.cloud.Project.GetMachineReservation(project.NewGetMachineReservationParams().WithID(id), nil) if err != nil { - return nil, fmt.Errorf("unable to fetch machine reservations: %w", err) - } - - for _, rv := range all { - if id[m.projectIndex()] != pointer.SafeDeref(rv.Projectid) { - continue - } - if id[m.sizeIndex()] != pointer.SafeDeref(rv.Sizeid) { - continue - } - - return rv, nil + return nil, err } - return nil, fmt.Errorf("no reservation found with size %q for project %q", id[m.sizeIndex()], id[m.projectIndex()]) + return resp.Payload, nil } func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, error) { @@ -220,10 +207,3 @@ func (m machineReservationsCmd) machineReservationsUsage() error { return m.listPrinter.Print(resp.Payload) } - -func (m machineReservationsCmd) projectIndex() int { - return 0 -} -func (m machineReservationsCmd) sizeIndex() int { - return 1 -} diff --git a/cmd/project-reservations_test.go b/cmd/project-reservations_test.go index 585b9ec..854dc10 100644 --- a/cmd/project-reservations_test.go +++ b/cmd/project-reservations_test.go @@ -17,6 +17,7 @@ import ( var ( machineReservation1 = &models.V1MachineReservationResponse{ + ID: pointer.Pointer("1"), Amount: pointer.Pointer(int32(3)), Description: "for firewalls", Labels: map[string]string{ @@ -29,6 +30,7 @@ var ( Tenant: pointer.Pointer("fits"), } machineReservation2 = &models.V1MachineReservationResponse{ + ID: pointer.Pointer("2"), Amount: pointer.Pointer(int32(3)), Description: "for machines", Labels: map[string]string{ @@ -63,15 +65,15 @@ func Test_ProjectMachineReservationsCmd_MultiResult(t *testing.T) { machineReservation2, }, wantTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION -fits project-a size-a 3 partition-a for firewalls -fits project-b size-b 3 partition-a,partition-b for machines +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +1 fits project-a size-a 3 partition-a for firewalls +2 fits project-b size-b 3 partition-a,partition-b for machines `), wantWideTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS -fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z - size.metal-stack.io/reserved-by=fits -fits project-b size-b 3 partition-a,partition-b for machines for machines size.metal-stack.io/reserved-by=fits +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +1 fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits +2 fits project-b size-b 3 partition-a,partition-b for machines for machines size.metal-stack.io/reserved-by=fits `), template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), wantTemplate: pointer.Pointer(` @@ -79,10 +81,10 @@ size-a project-a size-b project-b `), wantMarkdown: pointer.Pointer(` -| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | -|--------|-----------|--------|--------|-------------------------|---------------| -| fits | project-a | size-a | 3 | partition-a | for firewalls | -| fits | project-b | size-b | 3 | partition-a,partition-b | for machines | +| ID | TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|----|--------|-----------|--------|--------|-------------------------|---------------| +| 1 | fits | project-a | size-a | 3 | partition-a | for firewalls | +| 2 | fits | project-b | size-b | 3 | partition-a,partition-b | for machines | `), }, { @@ -108,25 +110,6 @@ size-b project-b want: []*models.V1MachineReservationResponse{ machineReservation1, }, - wantTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION -fits project-a size-a 3 partition-a for firewalls - `), - wantWideTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS -fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z - size.metal-stack.io/reserved-by=fits - - `), - template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), - wantTemplate: pointer.Pointer(` -size-a project-a - `), - wantMarkdown: pointer.Pointer(` -| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | -|--------|-----------|--------|--------|-------------|---------------| -| fits | project-a | size-a | 3 | partition-a | for firewalls | - `), }, { name: "apply", @@ -203,8 +186,7 @@ size-a project-a }, mocks: &testclient.CloudMockFns{ Project: func(mock *mock.Mock) { - mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams(). - WithProject(machineReservation1.Projectid).WithSize(machineReservation1.Sizeid)), nil). + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams().WithID(*machineReservation1.ID)), nil). Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) }, }, @@ -223,47 +205,43 @@ func Test_ProjectMachineReservationsCmd_SingleResult(t *testing.T) { { name: "describe", cmd: func(want *models.V1MachineReservationResponse) []string { - return []string{"project", "machine-reservation", "describe", *want.Projectid, *want.Sizeid} + return []string{"project", "machine-reservation", "describe", *want.ID} }, mocks: &testclient.CloudMockFns{ Project: func(mock *mock.Mock) { - mock.On("ListMachineReservations", testcommon.MatchIgnoreContext(t, project.NewListMachineReservationsParams().WithBody(&models.V1MachineReservationFindRequest{})), nil).Return(&project.ListMachineReservationsOK{ - Payload: []*models.V1MachineReservationResponse{ - machineReservation2, - machineReservation1, - }, + mock.On("GetMachineReservation", testcommon.MatchIgnoreContext(t, project.NewGetMachineReservationParams().WithID(*machineReservation1.ID)), nil).Return(&project.GetMachineReservationOK{ + Payload: machineReservation1, }, nil) }, }, want: machineReservation1, wantTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION -fits project-a size-a 3 partition-a for firewalls +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION +1 fits project-a size-a 3 partition-a for firewalls `), wantWideTable: pointer.Pointer(` -TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS -fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z - size.metal-stack.io/reserved-by=fits +ID TENANT PROJECT SIZE AMOUNT PARTITIONS DESCRIPTION LABELS +1 fits project-a size-a 3 partition-a for firewalls for firewalls size.metal-stack.io/reserved-at=2024-09-19T08:57:40Z + size.metal-stack.io/reserved-by=fits `), template: pointer.Pointer("{{ .sizeid }} {{ .projectid }}"), wantTemplate: pointer.Pointer(` size-a project-a `), wantMarkdown: pointer.Pointer(` -| TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | -|--------|-----------|--------|--------|-------------|---------------| -| fits | project-a | size-a | 3 | partition-a | for firewalls | +| ID | TENANT | PROJECT | SIZE | AMOUNT | PARTITIONS | DESCRIPTION | +|----|--------|-----------|--------|--------|-------------|---------------| +| 1 | fits | project-a | size-a | 3 | partition-a | for firewalls | `), }, { name: "delete", cmd: func(want *models.V1MachineReservationResponse) []string { - return []string{"project", "machine-reservation", "rm", *want.Projectid, *want.Sizeid} + return []string{"project", "machine-reservation", "rm", *want.ID} }, mocks: &testclient.CloudMockFns{ Project: func(mock *mock.Mock) { - mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams(). - WithProject(machineReservation1.Projectid).WithSize(machineReservation1.Sizeid)), nil). + mock.On("DeleteMachineReservation", testcommon.MatchIgnoreContext(t, project.NewDeleteMachineReservationParams().WithID(*machineReservation1.ID)), nil). Return(&project.DeleteMachineReservationOK{Payload: machineReservation1}, nil) }, }, diff --git a/cmd/sorters/project-reservations.go b/cmd/sorters/project-reservations.go index 7e9598a..db29ba8 100644 --- a/cmd/sorters/project-reservations.go +++ b/cmd/sorters/project-reservations.go @@ -8,6 +8,9 @@ import ( func MachineReservationsSorter() *multisort.Sorter[*models.V1MachineReservationResponse] { return multisort.New(multisort.FieldMap[*models.V1MachineReservationResponse]{ + "id": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, "tenant": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) }, @@ -20,11 +23,14 @@ func MachineReservationsSorter() *multisort.Sorter[*models.V1MachineReservationR "amount": func(a, b *models.V1MachineReservationResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Amount), p.SafeDeref(b.Amount), descending) }, - }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "size"}, {ID: "amount"}}) + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "size"}, {ID: "id"}}) } func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReservationUsageResponse] { return multisort.New(multisort.FieldMap[*models.V1MachineReservationUsageResponse]{ + "id": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, "tenant": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) }, @@ -43,5 +49,5 @@ func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReserva "unused-reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Usedreservations), p.SafeDeref(b.Usedreservations), descending) }, - }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "reservations"}}) + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}}) } diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index e52ea8b..9db829d 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -14,7 +14,7 @@ import ( func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { var ( - header = []string{"Tenant", "Project", "Size", "Amount", "Partitions", "Description"} + header = []string{"ID", "Tenant", "Project", "Size", "Amount", "Partitions", "Description"} rows [][]string ) @@ -26,6 +26,7 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat sort.Strings(rv.Partitionids) row := []string{ + pointer.SafeDeref(rv.ID), pointer.SafeDeref(rv.Tenant), pointer.SafeDeref(rv.Projectid), pointer.SafeDeref(rv.Sizeid), diff --git a/go.mod b/go.mod index b5d630b..faa7f20 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578 + github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 diff --git a/go.sum b/go.sum index 29d41cf..05eb43f 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578 h1:TS+trg3j7YOZJatONv97bg3uCKKu+FytwoWY1RLJB2s= -github.com/fi-ts/cloud-go v0.28.1-0.20240919083529-3d0d03132578/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= +github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf h1:F+wEF712gk9zkviHOT57bP1mYWjmXwtuv0Y5fCPAqMo= +github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= From f96daf46d284c7235681859212cc3d64fc16f9fa Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 26 Sep 2024 13:05:26 +0200 Subject: [PATCH 13/19] Update. --- cmd/project-reservations.go | 3 +++ go.mod | 2 +- go.sum | 4 ++-- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 0548933..2356d47 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -33,9 +33,11 @@ func newMachineReservationsCmd(c *config) *cobra.Command { DescribePrinter: func() printers.Printer { return c.describePrinter }, ListPrinter: func() printers.Printer { return c.listPrinter }, ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "show reservations of given id") cmd.Flags().String("project", "", "show reservations of given project") cmd.Flags().String("size", "", "show reservations of given size") cmd.Flags().String("tenant", "", "show reservations of given tenant") + genericcli.Must(cmd.RegisterFlagCompletionFunc("id", c.comp.MachineReservationListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("project", c.comp.ProjectListCompletion)) genericcli.Must(cmd.RegisterFlagCompletionFunc("size", c.comp.SizeListCompletion)) @@ -171,6 +173,7 @@ func (m machineReservationsCmd) List() ([]*models.V1MachineReservationResponse, Projectid: pointer.PointerOrNil(viper.GetString("project")), Sizeid: pointer.PointerOrNil(viper.GetString("size")), Tenant: pointer.PointerOrNil(viper.GetString("tenant")), + ID: pointer.PointerOrNil(viper.GetString("id")), }), nil) if err != nil { return nil, err diff --git a/go.mod b/go.mod index faa7f20..e9b22b0 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf + github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224 github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 diff --git a/go.sum b/go.sum index 05eb43f..937da5e 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf h1:F+wEF712gk9zkviHOT57bP1mYWjmXwtuv0Y5fCPAqMo= -github.com/fi-ts/cloud-go v0.28.1-0.20240925082728-63ee8d3526bf/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= +github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224 h1:nur9xTZKDxMsCjlQEmAYnrSJkdQVTGO25CgVXoU+O4c= +github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= From 669d9de998480a0642ee5a0c3ca23d68bc4656b6 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 26 Sep 2024 13:24:19 +0200 Subject: [PATCH 14/19] Fix test. --- cmd/project-reservations_test.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/project-reservations_test.go b/cmd/project-reservations_test.go index 854dc10..3aae751 100644 --- a/cmd/project-reservations_test.go +++ b/cmd/project-reservations_test.go @@ -90,7 +90,7 @@ size-b project-b { name: "list with filters", cmd: func(want []*models.V1MachineReservationResponse) []string { - args := []string{"project", "machine-reservation", "list", "--tenant", *want[0].Tenant, "--project", *want[0].Projectid, "--size", *want[0].Sizeid} + args := []string{"project", "machine-reservation", "list", "--tenant", *want[0].Tenant, "--project", *want[0].Projectid, "--size", *want[0].Sizeid, "--id", *want[0].ID} assertExhaustiveArgs(t, args, "sort-by") return args }, @@ -100,6 +100,7 @@ size-b project-b Projectid: pointer.Pointer("project-a"), Sizeid: pointer.Pointer("size-a"), Tenant: pointer.Pointer("fits"), + ID: pointer.Pointer("1"), })), nil).Return(&project.ListMachineReservationsOK{ Payload: []*models.V1MachineReservationResponse{ machineReservation1, From 834f683514229e6e3c0cbe3cc76e0411ed441e0c Mon Sep 17 00:00:00 2001 From: Gerrit Date: Thu, 26 Sep 2024 14:35:09 +0200 Subject: [PATCH 15/19] Billing commands. --- cmd/billing.go | 77 +++++++++++++++++++++++ cmd/project-reservations.go | 7 ++- cmd/sorters/project-reservations.go | 32 ++++++++++ cmd/tableprinters/printer.go | 2 + cmd/tableprinters/project-reservations.go | 54 ++++++++++++++++ go.mod | 2 +- go.sum | 4 +- 7 files changed, 174 insertions(+), 4 deletions(-) diff --git a/cmd/billing.go b/cmd/billing.go index b29a30d..4f384ff 100644 --- a/cmd/billing.go +++ b/cmd/billing.go @@ -6,6 +6,7 @@ import ( "github.com/fi-ts/cloud-go/api/client/accounting" "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/sorters" "github.com/go-openapi/strfmt" "github.com/go-playground/validator/v10" "github.com/jinzhu/now" @@ -123,6 +124,24 @@ export CLOUDCTL_COSTS_HOUR=0.01 # costs per hour return c.machineUsage() }, } + machineReservationBillingCmd := &cobra.Command{ + Use: "machine-reservation", + Short: "look at machine reservation bills", + Long: ` +You may want to convert the usage to a price in Euro by using the prices from your contract. You can use the following environment variables: + +export CLOUDCTL_COSTS_HOUR=0.01 # costs per hour + +⚠ Please be aware that any costs calculated in this fashion can still be different from the final bill as it does not include contract specific details like minimum purchase, discounts, etc. +`, + RunE: func(cmd *cobra.Command, args []string) error { + err := initBillingOpts() + if err != nil { + return err + } + return c.machineReservationUsage() + }, + } productOptionBillingCmd := &cobra.Command{ Use: "product-option", Short: "look at product option bills", @@ -227,6 +246,7 @@ export CLOUDCTL_COSTS_STORAGE_GI_HOUR=0.01 # Costs per capacity hour billingCmd.AddCommand(volumeBillingCmd) billingCmd.AddCommand(postgresBillingCmd) billingCmd.AddCommand(machineBillingCmd) + billingCmd.AddCommand(machineReservationBillingCmd) billingCmd.AddCommand(productOptionBillingCmd) billingOpts = &BillingOpts{} @@ -282,6 +302,23 @@ export CLOUDCTL_COSTS_STORAGE_GI_HOUR=0.01 # Costs per capacity hour genericcli.Must(viper.BindPFlags(machineBillingCmd.Flags())) + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") + machineReservationBillingCmd.Flags().StringP("time-format", "", "2006-01-02", "the time format used to parse the arguments 'from' and 'to'") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.FromString, "from", "", "", "the start time in the accounting window to look at (optional, defaults to start of the month") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.ToString, "to", "", "", "the end time in the accounting window to look at (optional, defaults to current system time)") + machineReservationBillingCmd.Flags().StringVarP(&billingOpts.ProjectID, "project-id", "p", "", "the project to account") + machineReservationBillingCmd.Flags().String("id", "", "the id to account") + machineReservationBillingCmd.Flags().String("size-id", "", "the size-id to account") + machineReservationBillingCmd.Flags().String("partition-id", "", "the partition-id to account") + + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("tenant", c.comp.TenantListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("project-id", c.comp.ProjectListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("partition-id", c.comp.PartitionListCompletion)) + genericcli.Must(machineReservationBillingCmd.RegisterFlagCompletionFunc("size-id", c.comp.SizeListCompletion)) + genericcli.AddSortFlag(machineReservationBillingCmd, sorters.MachineReservationsBillingUsageSorter()) + + genericcli.Must(viper.BindPFlags(machineReservationBillingCmd.Flags())) + productOptionBillingCmd.Flags().StringVarP(&billingOpts.Tenant, "tenant", "t", "", "the tenant to account") productOptionBillingCmd.Flags().StringP("time-format", "", "2006-01-02", "the time format used to parse the arguments 'from' and 'to'") productOptionBillingCmd.Flags().StringVarP(&billingOpts.FromString, "from", "", "", "the start time in the accounting window to look at (optional, defaults to start of the month") @@ -495,6 +532,46 @@ func (c *config) machineUsage() error { return c.listPrinter.Print(response.Payload) } +func (c *config) machineReservationUsage() error { + from := strfmt.DateTime(billingOpts.From) + cur := models.V1MachineReservationUsageRequest{ + From: &from, + To: strfmt.DateTime(billingOpts.To), + } + if billingOpts.Tenant != "" { + cur.Tenant = billingOpts.Tenant + } + if billingOpts.ProjectID != "" { + cur.Projectid = billingOpts.ProjectID + } + if viper.IsSet("id") { + cur.ID = viper.GetString("id") + } + if viper.IsSet("size-id") { + cur.Sizeid = viper.GetString("size-id") + } + if viper.IsSet("partition-id") { + cur.Partition = viper.GetString("partition-id") + } + + response, err := c.cloud.Accounting.MachineReservationUsage(accounting.NewMachineReservationUsageParams().WithBody(&cur), nil) + if err != nil { + return err + } + + keys, err := genericcli.ParseSortFlags() + if err != nil { + return err + } + + err = sorters.MachineReservationsBillingUsageSorter().SortBy(response.Payload.Usage, keys...) + if err != nil { + return err + } + + return c.listPrinter.Print(response.Payload) +} + func (c *config) productOptionUsage() error { from := strfmt.DateTime(billingOpts.From) cur := models.V1ProductOptionUsageRequest{ diff --git a/cmd/project-reservations.go b/cmd/project-reservations.go index 2356d47..5d3e12c 100644 --- a/cmd/project-reservations.go +++ b/cmd/project-reservations.go @@ -203,7 +203,12 @@ func (m machineReservationsCmd) machineReservationsUsage() error { return err } - err = sorters.MachineReservationsUsageSorter().SortBy(resp.Payload) + keys, err := genericcli.ParseSortFlags() + if err != nil { + return err + } + + err = sorters.MachineReservationsUsageSorter().SortBy(resp.Payload, keys...) if err != nil { return err } diff --git a/cmd/sorters/project-reservations.go b/cmd/sorters/project-reservations.go index db29ba8..c30c97c 100644 --- a/cmd/sorters/project-reservations.go +++ b/cmd/sorters/project-reservations.go @@ -1,6 +1,8 @@ package sorters import ( + "strconv" + "github.com/fi-ts/cloud-go/api/models" "github.com/metal-stack/metal-lib/pkg/multisort" p "github.com/metal-stack/metal-lib/pkg/pointer" @@ -51,3 +53,33 @@ func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReserva }, }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}}) } + +func MachineReservationsBillingUsageSorter() *multisort.Sorter[*models.V1MachineReservationUsage] { + return multisort.New(multisort.FieldMap[*models.V1MachineReservationUsage]{ + "id": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.ID), p.SafeDeref(b.ID), descending) + }, + "tenant": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Tenant), p.SafeDeref(b.Tenant), descending) + }, + "project": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Projectid), p.SafeDeref(b.Projectid), descending) + }, + "size": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Sizeid), p.SafeDeref(b.Sizeid), descending) + }, + "partition": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + return multisort.Compare(p.SafeDeref(a.Partition), p.SafeDeref(b.Partition), descending) + }, + "reservation-seconds": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + aSeconds, _ := strconv.ParseInt(p.SafeDeref(a.Reservationseconds), 10, 64) + bSeconds, _ := strconv.ParseInt(p.SafeDeref(b.Reservationseconds), 10, 64) + return multisort.Compare(aSeconds, bSeconds, descending) + }, + "average": func(a, b *models.V1MachineReservationUsage, descending bool) multisort.CompareResult { + aSeconds, _ := strconv.ParseFloat(p.SafeDeref(a.Average), 64) + bSeconds, _ := strconv.ParseFloat(p.SafeDeref(b.Average), 64) + return multisort.Compare(aSeconds, bSeconds, descending) + }, + }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}}) +} diff --git a/cmd/tableprinters/printer.go b/cmd/tableprinters/printer.go index edaa41e..179f4dc 100644 --- a/cmd/tableprinters/printer.go +++ b/cmd/tableprinters/printer.go @@ -43,6 +43,8 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin return t.MachineReservationsUsageTable(pointer.WrapInSlice(d), wide) case []*models.V1MachineReservationUsageResponse: return t.MachineReservationsUsageTable(d, wide) + case *models.V1MachineReservationBillingUsageResponse: + return t.MachineReservationsBillingTable(d, wide) default: // fallback to old printer for as long as the migration takes: diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index 9db829d..8a61b2b 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -5,11 +5,14 @@ import ( "sort" "strconv" "strings" + "time" "github.com/fi-ts/cloud-go/api/models" + "github.com/fi-ts/cloudctl/cmd/helper" "github.com/metal-stack/metal-lib/pkg/genericcli" "github.com/metal-stack/metal-lib/pkg/pointer" "github.com/olekukonko/tablewriter" + "github.com/spf13/viper" ) func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservationResponse, wide bool) ([]string, [][]string, error) { @@ -101,3 +104,54 @@ func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineRes return header, rows, nil } + +func (t *TablePrinter) MachineReservationsBillingTable(data *models.V1MachineReservationBillingUsageResponse, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"Tenant", "From", "To", "ProjectID", "ProjectName", "Partition", "Size", "ID", "Reservations * Time", "Average"} + rows [][]string + ) + + for _, rv := range data.Usage { + row := []string{ + pointer.SafeDeref(rv.Tenant), + time.Time(pointer.SafeDeref(data.From)).String(), + time.Time(data.To).String(), + pointer.SafeDeref(rv.Projectid), + pointer.SafeDeref(rv.Projectname), + pointer.SafeDeref(rv.Partition), + pointer.SafeDeref(rv.Sizeid), + pointer.SafeDeref(rv.ID), + humanizeSeconds(pointer.SafeDeref(rv.Reservationseconds)), + pointer.SafeDeref(rv.Average), + } + + rows = append(rows, row) + } + + rows = append(rows, []string{"Total", "", "", "", "", "", "", "", + humanizeSeconds(pointer.SafeDeref(data.Accumulatedusage.Reservationseconds)) + secondsCosts(pointer.SafeDeref(data.Accumulatedusage.Reservationseconds)), + pointer.SafeDeref(data.Accumulatedusage.Average), + }) + + return header, rows, nil +} + +func humanizeSeconds(seconds string) string { + duration, err := strconv.ParseInt(seconds, 10, 64) + if err == nil { + return helper.HumanizeDuration(time.Duration(duration) * time.Second) + } + return "" +} + +func secondsCosts(seconds string) string { + costsPerHour := viper.GetFloat64("costs-hour") + if costsPerHour <= 0 { + return "" + } + duration, err := strconv.ParseInt(seconds, 10, 64) + if err == nil { + return fmt.Sprintf(" (%.2f €)", float64(duration/3600)*costsPerHour) + } + return "" +} diff --git a/go.mod b/go.mod index e9b22b0..0cefad6 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224 + github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 diff --git a/go.sum b/go.sum index 937da5e..859ca5d 100644 --- a/go.sum +++ b/go.sum @@ -90,8 +90,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224 h1:nur9xTZKDxMsCjlQEmAYnrSJkdQVTGO25CgVXoU+O4c= -github.com/fi-ts/cloud-go v0.28.1-0.20240926110310-7a45ce24c224/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= +github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c h1:9ESEl2gizrHyypb6vkLkk2EAaCUdHrxkGn/tJMLBl0c= +github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= From 4fc1bfc398030e8c5391c1be7f3bd3387c291915 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 27 Sep 2024 09:30:06 +0200 Subject: [PATCH 16/19] Pin metal-lib. --- go.mod | 33 ++++++++-------- go.sum | 118 +++++++++++++++++++++++++++++++-------------------------- 2 files changed, 82 insertions(+), 69 deletions(-) diff --git a/go.mod b/go.mod index 0cefad6..d61c951 100644 --- a/go.mod +++ b/go.mod @@ -20,7 +20,7 @@ require ( github.com/jinzhu/now v1.1.5 github.com/metal-stack/duros-go v0.5.1 github.com/metal-stack/metal-go v0.34.0 - github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1 + github.com/metal-stack/metal-lib v0.18.3 github.com/metal-stack/updater v1.2.2 github.com/metal-stack/v v1.0.3 github.com/olekukonko/tablewriter v0.0.5 @@ -32,8 +32,8 @@ require ( github.com/undefinedlabs/go-mpatch v1.0.7 golang.org/x/sync v0.8.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/api v0.29.8 - k8s.io/apimachinery v0.29.8 + k8s.io/api v0.30.3 + k8s.io/apimachinery v0.31.0 sigs.k8s.io/yaml v1.4.0 ) @@ -58,11 +58,12 @@ require ( github.com/aws/aws-sdk-go-v2/service/ssooidc v1.22.0 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 // indirect github.com/aws/smithy-go v1.20.0 // indirect + github.com/bits-and-blooms/bitset v1.13.0 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cheggaaa/pb/v3 v3.1.5 // indirect - github.com/coreos/go-iptables v0.7.0 // indirect + github.com/coder/websocket v1.8.12 // indirect + github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect github.com/coreos/go-oidc/v3 v3.11.0 // indirect - github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dblohm7/wingoes v0.0.0-20240801171404-fc12d7c70140 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect @@ -70,7 +71,9 @@ require ( github.com/fsnotify/fsnotify v1.7.0 // indirect github.com/fxamacker/cbor/v2 v2.7.0 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gaissmai/bart v0.11.1 // indirect github.com/go-jose/go-jose/v4 v4.0.4 // indirect + github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 // indirect github.com/go-logr/logr v1.4.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-ole/go-ole v1.3.0 // indirect @@ -95,7 +98,7 @@ require ( github.com/google/go-github/v56 v56.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/google/gofuzz v1.2.0 // indirect - github.com/google/nftables v0.2.0 // indirect + github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect github.com/google/uuid v1.6.0 // indirect github.com/gorilla/csrf v1.7.2 // indirect github.com/gorilla/mux v1.8.1 // indirect @@ -144,6 +147,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.2 // indirect github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/prometheus-community/pro-bing v0.4.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/safchain/ethtool v0.3.0 // indirect github.com/sagikazarmark/locafero v0.4.0 // indirect @@ -153,14 +157,15 @@ require ( github.com/spf13/cast v1.7.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff // indirect + github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect - github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 // indirect + github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 // indirect github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 // indirect - github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 // indirect - github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90 // indirect + github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 // indirect + github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 // indirect + github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 // indirect github.com/tcnksm/go-httpstat v0.2.0 // indirect github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect github.com/vishvananda/netlink v1.2.1-beta.2 // indirect @@ -193,12 +198,10 @@ require ( gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect - gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c // indirect - inet.af/peercred v0.0.0-20210906144145-0893ea02156a // indirect - k8s.io/klog/v2 v2.120.1 // indirect + gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 // indirect + k8s.io/klog/v2 v2.130.1 // indirect k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 // indirect - nhooyr.io/websocket v1.8.10 // indirect sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect - tailscale.com v1.54.0 // indirect + tailscale.com v1.72.1 // indirect ) diff --git a/go.sum b/go.sum index 859ca5d..1bfd8ff 100644 --- a/go.sum +++ b/go.sum @@ -50,24 +50,26 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.27.0 h1:cjTRjh700H36MQ8M0LnDn33W3Jmw github.com/aws/aws-sdk-go-v2/service/sts v1.27.0/go.mod h1:nXfOBMWPokIbOY+Gi7a1psWMSvskUCemZzI+SMB7Akc= github.com/aws/smithy-go v1.20.0 h1:6+kZsCXZwKxZS9RfISnPc4EXlHoyAkm2hPuM8X2BrrQ= github.com/aws/smithy-go v1.20.0/go.mod h1:uo5RKksAl4PzhqaAbjd4rLgFoq5koTsQKYuGe7dklGc= +github.com/bits-and-blooms/bitset v1.13.0 h1:bAQ9OPNFYbGHV6Nez0tmNI0RiEu7/hxlYJRUA0wFAVE= +github.com/bits-and-blooms/bitset v1.13.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM= github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cheggaaa/pb/v3 v3.1.5 h1:QuuUzeM2WsAqG2gMqtzaWithDJv0i+i6UlnwSCI4QLk= github.com/cheggaaa/pb/v3 v3.1.5/go.mod h1:CrxkeghYTXi1lQBEI7jSn+3svI3cuc19haAj6jM60XI= -github.com/cilium/ebpf v0.12.3 h1:8ht6F9MquybnY97at+VDZb3eQQr8ev79RueWeVaEcG4= -github.com/cilium/ebpf v0.12.3/go.mod h1:TctK1ivibvI3znr66ljgi4hqOT8EYQjz1KWBfb1UVgM= +github.com/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk= +github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8= -github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= +github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo= +github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0= +github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-oidc/v3 v3.11.0 h1:Ia3MxdwpSw702YW0xgfmP1GVCMA9aEFWu12XUZ3/OtI= github.com/coreos/go-oidc/v3 v3.11.0/go.mod h1:gE3LgjOgFoHi9a4ce4/tJczr0Ai2/BoDhf0r5lltWI0= -github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= -github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= -github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= +github.com/creack/pty v1.1.23 h1:4M6+isWdcStXEf15G/RbrMPOQj1dZ7HPZCGwE4kOeP0= +github.com/creack/pty v1.1.23/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= @@ -80,6 +82,10 @@ github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnN github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e h1:vUmf0yezR0y7jJ5pceLHthLaYf4bA5T14B6q39S4q2Q= github.com/digitalocean/go-smbios v0.0.0-20180907143718-390a4f403a8e/go.mod h1:YTIHhz/QFSYnu/EhlF2SpU2Uk+32abacUYA5ZPljz1A= +github.com/djherbis/times v1.6.0 h1:w2ctJ92J8fBvWPxugmXIv7Nz7Q3iDMKNx9v5ocVH20c= +github.com/djherbis/times v1.6.0/go.mod h1:gOHeRAz2h+VJNZ5Gmc/o7iD9k4wW7NMVqieYCY99oc0= +github.com/dsnet/try v0.0.3 h1:ptR59SsrcFUYbT/FhAbKTV6iLkeD6O18qfIWRml2fqI= +github.com/dsnet/try v0.0.3/go.mod h1:WBM8tRpUmnXXhY1U6/S8dt6UWdHTQ7y8A5YSkRCkq40= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= @@ -100,6 +106,8 @@ github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gaissmai/bart v0.11.1 h1:5Uv5XwsaFBRo4E5VBcb9TzY8B7zxFf+U7isDxqOrRfc= +github.com/gaissmai/bart v0.11.1/go.mod h1:KHeYECXQiBjTzQz/om2tqn3sZF1J7hw9m6z41ftj3fg= github.com/gardener/gardener v1.91.0 h1:m+0jtmS1ANN+jm+Y+IyhW6ofZbPtRL18gZ+lgAA8mQw= github.com/gardener/gardener v1.91.0/go.mod h1:3h8gSsr05ABuLGnGLB4bEYRn8ot42APkIa2E3f+nGc0= github.com/gardener/machine-controller-manager v0.53.1 h1:4P9qtzoD+989Lhc8XaI6Zo3X2TaQVXgHHrbEpuhJcrI= @@ -109,6 +117,8 @@ github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I= github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo= github.com/go-jose/go-jose/v4 v4.0.4 h1:VsjPI33J0SB9vQM6PLmNjoHqMQNGPiZ0rHL7Ni7Q6/E= github.com/go-jose/go-jose/v4 v4.0.4/go.mod h1:NKb5HO1EZccyMpiZNbdUw/14tiXNyUJh188dfnMCAfc= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0 h1:ymLjT4f35nQbASLnvxEde4XOBL+Sn7rFuV+FOJqkljg= +github.com/go-json-experiment/json v0.0.0-20231102232822-2e55bd4e08b0/go.mod h1:6daplAwHHGbUGib4990V3Il26O0OC4aRyvewaaAihaA= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= @@ -144,15 +154,12 @@ github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJn github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= -github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.12.0 h1:/1WHjnMsI1dlIBQutrvSMGZRQufVO3asrHfTwfACoPM= github.com/goccy/go-yaml v1.12.0/go.mod h1:wKnAMd44+9JAAnGQpWVEgBzGt3YuTaQ4uXoHvE4m7WU= -github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466 h1:sQspH8M4niEijh3PFscJRLDnkL547IeP7kpPe3uUhEg= github.com/godbus/dbus/v5 v5.1.1-0.20230522191255-76236955d466/go.mod h1:ZiQxhyQ+bbbfxUKVvjfO498oPYvtYhZzycal3G/NHmU= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= @@ -183,10 +190,10 @@ github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17 github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/nftables v0.2.0 h1:PbJwaBmbVLzpeldoeUKGkE2RjstrjPKMl6oLrfEJ6/8= -github.com/google/nftables v0.2.0/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 h1:K6RDEckDVWvDI9JAJYCmNdQXq6neHJOYx3V6jnqNEec= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI= +github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2Rrd27c3VGxi6a/6HNq8QmHRKM= +github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -214,6 +221,8 @@ github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2 github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1 h1:L3pm9Kf2G6gJVYawz2SrI5QnV1wzHYbqmKnSHHXJAb8= github.com/insomniacslk/dhcp v0.0.0-20240204152450-ca2dc33955c1/go.mod h1:izxuNQZeFrbx2nK2fAyN5iNUB34Fe9j0nK4PwLzAkKw= +github.com/jellydator/ttlcache/v3 v3.1.0 h1:0gPFG0IHHP6xyUyXq+JaD8fwkDCqgqwohXNJBcYE71g= +github.com/jellydator/ttlcache/v3 v3.1.0/go.mod h1:hi7MGFdMAwZna5n2tuvh63DvFLzVKySzCVW6+0gA2n4= github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ= github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= @@ -279,8 +288,8 @@ github.com/metal-stack/duros-go v0.5.1 h1:baE/c0AKy9sTOztPhILJLaoMmT17Ajsb+xRMfH github.com/metal-stack/duros-go v0.5.1/go.mod h1:Z9mzI9ds2gI8zHC03PUCQvmlWa7WAPukDCUhowtVeOk= github.com/metal-stack/metal-go v0.34.0 h1:X4Wlt2OAhsu3Lq+rHSWnWeASmX6CYvOxnL6DxmjnzbU= github.com/metal-stack/metal-go v0.34.0/go.mod h1:3MJTYCS4YJz8D8oteTKhjpaAKNMMjMKYDrIy9awHGtQ= -github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1 h1:nzv+w8mFFnZO3KqEflMlAKlyJjIWYGIgMBq2R4ZCq00= -github.com/metal-stack/metal-lib v0.18.3-0.20240919130905-c0fc9fa493e1/go.mod h1:GJjipRpHmpd2vjBtsaw9gGk5ZFan7NlShyjIsTdY1x4= +github.com/metal-stack/metal-lib v0.18.3 h1:bovFiJPB9SMvuGLqcXVWz6jFB8HrdzwnCX7TFlen4r0= +github.com/metal-stack/metal-lib v0.18.3/go.mod h1:Ctyi6zaXFr2NVrQZLFsDLnFCzupKnYErTtgRFKAsnbw= github.com/metal-stack/security v0.8.1 h1:4zmVUxZvDWShVvVIxM3XhIv7pTmPe9DvACRIHW6YTsk= github.com/metal-stack/security v0.8.1/go.mod h1:OO8ZilZO6fUV5QEmwc7HP/RAjqYrGQxXoYIddJ9TvqE= github.com/metal-stack/updater v1.2.2 h1:gnUrnQgfT20QFMDtFBY89opKoBAkdeI/8T2iwMHNdxs= @@ -310,8 +319,8 @@ github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= -github.com/onsi/ginkgo/v2 v2.17.0 h1:kdnunFXpBjbzN56hcJHrXZ8M+LOkenKA7NnBzTNigTI= -github.com/onsi/ginkgo/v2 v2.17.0/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= +github.com/onsi/ginkgo/v2 v2.19.0 h1:9Cnnf7UHo57Hy3k6/m5k3dRfGTMXGvxhHFvkDTCTpvA= +github.com/onsi/ginkgo/v2 v2.19.0/go.mod h1:rlwLi9PilAFJ8jCg9UE1QP6VBpd6/xj3SRC0d6TU0To= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= github.com/onsi/gomega v1.32.0/go.mod h1:a4x4gW6Pz2yK1MAmvluYme5lvYTn61afQ2ETw/8n4Lg= github.com/opentracing/opentracing-go v1.2.0 h1:uEJPy/1a5RIPAJ0Ov+OIO8OxWu77jEv+1B0VhjKrZUs= @@ -325,13 +334,15 @@ github.com/pkg/sftp v1.13.6/go.mod h1:tz1ryNURKu77RL+GuCzmoJYxQczL3wLNNpPWagdg4Q github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4= +github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0= github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs= @@ -367,28 +378,34 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff h1:vnxdYZUJbsSRcIcduDW3DcQqfqaiv4FUgy25q8X+vfI= -github.com/tailscale/certstore v0.1.1-0.20231020161753-77811a65f4ff/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ= +github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4= github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780 h1:U0J2CUrrTcc2wmr9tSLYEo+USfwNikRRsmxVLD4eZ7E= -github.com/tailscale/golang-x-crypto v0.0.0-20240108194725-7ce1f622c780/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4 h1:rXZGgEa+k2vJM8xT0PoSKfVXwFGPQ3z3CJfmnHJkZZw= +github.com/tailscale/golang-x-crypto v0.0.0-20240604161659-3fde5e568aa4/go.mod h1:ikbF+YT089eInTp9f2vmvy4+ZVnW5hzX1q2WknxSprQ= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio= github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw= github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85 h1:zrsUcqrG2uQSPhaUPjUQwozcRdDdSxxqhNgNZ3drZFk= github.com/tailscale/netlink v1.1.1-0.20211101221916-cabfb018fe85/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0= -github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61 h1:G6/VUGQkHbBffO0s3f51DThcHCWrShlWklcS4Zxh5BU= -github.com/tailscale/web-client-prebuilt v0.0.0-20240208184856-443a64766f61/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= -github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90 h1:lMGYrokOq9NKDw1UMBH7AsS4boZ41jcduvYaRIdedhE= -github.com/tailscale/wireguard-go v0.0.0-20231101022006-db7604d1aa90/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4 h1:Gz0rz40FvFVLTBk/K8UNAenb36EbDSnh+q7Z9ldcC8w= +github.com/tailscale/peercred v0.0.0-20240214030740-b535050b2aa4/go.mod h1:phI29ccmHQBc+wvroosENp1IF9195449VDnFDhJ4rJU= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1 h1:tdUdyPqJ0C97SJfjB9tW6EylTtreyee9C44de+UBG0g= +github.com/tailscale/web-client-prebuilt v0.0.0-20240226180453-5db17b287bf1/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6 h1:l10Gi6w9jxvinoiq15g8OToDdASBni4CyJOdHY1Hr8M= +github.com/tailscale/wf v0.0.0-20240214030419-6fbb0a674ee6/go.mod h1:ZXRML051h7o4OcI0d3AaILDIad/Xw0IkXaHM17dic1Y= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98 h1:RNpJrXfI5u6e+uzyIzvmnXbhmhdRkVf//90sMBH3lso= +github.com/tailscale/wireguard-go v0.0.0-20240731203015-71393c576b98/go.mod h1:BOm5fXUBFM+m9woLNBoxI9TaBXXhGNP50LX/TGIvGb4= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e h1:zOGKqN5D5hHhiYUp091JqK7DPCqSARyUfduhGUY8Bek= +github.com/tailscale/xnet v0.0.0-20240729143630-8497ac4dab2e/go.mod h1:orPd6JZXXRyuDusYilywte7k094d7dycXXU5YnWsrwg= github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA= github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk= github.com/tcnksm/go-httpstat v0.2.0 h1:rP7T5e5U2HfmOBmZzGgGZjBQ5/GluWUylujl0tJ04I0= github.com/tcnksm/go-httpstat v0.2.0/go.mod h1:s3JVJFtQxtBEBC9dwcdTTXS9xFnM3SXAZwPG41aurT8= -github.com/u-root/u-root v0.11.0 h1:6gCZLOeRyevw7gbTwMj3fKxnr9+yHFlgF3N7udUVNO8= -github.com/u-root/u-root v0.11.0/go.mod h1:DBkDtiZyONk9hzVEdB/PWI9B4TxDkElWlVTHseglrZY= +github.com/u-root/u-root v0.12.0 h1:K0AuBFriwr0w/PGS3HawiAw89e3+MU7ks80GpghAsNs= +github.com/u-root/u-root v0.12.0/go.mod h1:FYjTOh4IkIZHhjsd17lb8nYW6udgXdJhG1c0r6u0arI= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= github.com/undefinedlabs/go-mpatch v1.0.7 h1:943FMskd9oqfbZV0qRVKOUsXQhTLXL0bQTVbQSpzmBs= @@ -426,10 +443,10 @@ golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt7 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9 h1:j3D9DvWRpUfIyFfDPws7LoIZ2MAI1OJHdQXtTnYtN+k= -golang.org/x/exp/typeparams v0.0.0-20230905200255-921286631fa9/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= -golang.org/x/image v0.12.0 h1:w13vZbU4o5rKOFFR8y7M+c4A5jXDC0uXTdHYRP8X2DQ= -golang.org/x/image v0.12.0/go.mod h1:Lu90jvHG7GfemOIcldsh9A2hS01ocl6oNO7ype5mEnk= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a h1:8qmSSA8Gz/1kTrCe0nqR0R3Gb/NDhykzWw2q2mWZydM= +golang.org/x/exp/typeparams v0.0.0-20240119083558-1b970713d09a/go.mod h1:AbB0pIl9nAr9wVwH+Z2ZpaocVmF5I4GyWCDIsVjR0bk= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -469,7 +486,6 @@ golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210301091718-77cc2087c03b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220817070843-5a390386f1f2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -537,35 +553,29 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c h1:bYb98Ra11fJ8F2xFbZx0zg2VQ28lYqC1JxfaaF53xqY= -gvisor.dev/gvisor v0.0.0-20230928000133-4fe30062272c/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987 h1:TU8z2Lh3Bbq77w0t1eG8yRlLcNHzZu3x6mhoH2Mk0c8= +gvisor.dev/gvisor v0.0.0-20240722211153-64c016c92987/go.mod h1:sxc3Uvk/vHcd3tj7/DHVBoR5wvWT/MmRq2pj7HRJnwU= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.4.6 h1:oFEHCKeID7to/3autwsWfnuv69j3NsfcXbvJKuIcep8= honnef.co/go/tools v0.4.6/go.mod h1:+rnGS1THNh8zMwnd2oVOTL9QF6vmfyG6ZXBULae2uc0= howett.net/plist v1.0.0 h1:7CrbWYbPPO/PyNy38b2EB/+gYbjCe2DXBxgtOOZbSQM= howett.net/plist v1.0.0/go.mod h1:lqaXoTrLY4hg8tnEzNru53gicrbv7rrk+2xJA/7hw9g= -inet.af/peercred v0.0.0-20210906144145-0893ea02156a h1:qdkS8Q5/i10xU2ArJMKYhVa1DORzBfYS/qA2UK2jheg= -inet.af/peercred v0.0.0-20210906144145-0893ea02156a/go.mod h1:FjawnflS/udxX+SvpsMgZfdqx2aykOlkISeAsADi5IU= -inet.af/wf v0.0.0-20221017222439-36129f591884 h1:zg9snq3Cpy50lWuVqDYM7AIRVTtU50y5WXETMFohW/Q= -inet.af/wf v0.0.0-20221017222439-36129f591884/go.mod h1:bSAQ38BYbY68uwpasXOTZo22dKGy9SNvI6PZFeKomZE= -k8s.io/api v0.29.8 h1:ZBKg9clWnIGtQ5yGhNwMw2zyyrsIAQaXhZACcYNflQE= -k8s.io/api v0.29.8/go.mod h1:XlGIpmpzKGrtVca7GlgNryZJ19SvQdI808NN7fy1SgQ= -k8s.io/apimachinery v0.29.8 h1:uBHc9WuKiTHClIspJqtR84WNpG0aOGn45HWqxgXkk8Y= -k8s.io/apimachinery v0.29.8/go.mod h1:i3FJVwhvSp/6n8Fl4K97PJEP8C+MM+aoDq4+ZJBf70Y= -k8s.io/klog/v2 v2.120.1 h1:QXU6cPEOIslTGvZaXvFWiP9VKyeet3sawzTOvdXb4Vw= -k8s.io/klog/v2 v2.120.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= +k8s.io/api v0.30.3 h1:ImHwK9DCsPA9uoU3rVh4QHAHHK5dTSv1nxJUapx8hoQ= +k8s.io/api v0.30.3/go.mod h1:GPc8jlzoe5JG3pb0KJCSLX5oAFIW3/qNJITlDj8BH04= +k8s.io/apimachinery v0.31.0 h1:m9jOiSr3FoSSL5WO9bjm1n6B9KROYYgNZOb4tyZ1lBc= +k8s.io/apimachinery v0.31.0/go.mod h1:rsPdaZJfTfLsNJSQzNHQvYoTmxhoOEofxtOsF3rtsMo= +k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= +k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3 h1:b2FmK8YH+QEwq/Sy2uAEhmqL5nPfGYbJOcaqjeYYZoA= k8s.io/utils v0.0.0-20240902221715-702e33fdd3c3/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= -nhooyr.io/websocket v1.8.10 h1:mv4p+MnGrLDcPlBoWsvPP7XCzTYMXP9F9eIGoKbgx7Q= -nhooyr.io/websocket v1.8.10/go.mod h1:rN9OFWIUwuxg4fR5tELlYC04bXYowCP9GX47ivo2l+c= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo= sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0= sigs.k8s.io/structured-merge-diff/v4 v4.4.1 h1:150L+0vs/8DA78h1u02ooW1/fFq/Lwr+sGiqlzvrtq4= sigs.k8s.io/structured-merge-diff/v4 v4.4.1/go.mod h1:N8hJocpFajUSSeSJ9bOZ77VzejKZaXsTtZo4/u7Io08= sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E= sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= -software.sslmate.com/src/go-pkcs12 v0.2.1 h1:tbT1jjaeFOF230tzOIRJ6U5S1jNqpsSyNjzDd58H3J8= -software.sslmate.com/src/go-pkcs12 v0.2.1/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= -tailscale.com v1.54.0 h1:Dri5BTKkHYpl+/t8ofY+tyvoTDbH/FpP7iB4B0cAQOY= -tailscale.com v1.54.0/go.mod h1:MnLFoCRwzFWr3qtkSW2nZdQpK7wQRZEk1KtcEGAuZYw= +software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k= +software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI= +tailscale.com v1.72.1 h1:hk82jek36ph2S3Tfsh57NVWKEm/pZ9nfUonvlowpfaA= +tailscale.com v1.72.1/go.mod h1:v7OHtg0KLAnhOVf81Z8WrjNefj238QbFhgkWJQoKxbs= From d408da93816207370e2ccaf1118e844ee470ec21 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Fri, 27 Sep 2024 14:25:36 +0200 Subject: [PATCH 17/19] Add ID to usage. --- cmd/tableprinters/project-reservations.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cmd/tableprinters/project-reservations.go b/cmd/tableprinters/project-reservations.go index 8a61b2b..a77ab3a 100644 --- a/cmd/tableprinters/project-reservations.go +++ b/cmd/tableprinters/project-reservations.go @@ -60,7 +60,7 @@ func (t *TablePrinter) MachineReservationsTable(data []*models.V1MachineReservat func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineReservationUsageResponse, wide bool) ([]string, [][]string, error) { var ( - header = []string{"Tenant", "Project", "Partition", "Size", "Reservations"} + header = []string{"ID", "Tenant", "Project", "Partition", "Size", "Reservations"} rows [][]string ) @@ -76,6 +76,7 @@ func (t *TablePrinter) MachineReservationsUsageTable(data []*models.V1MachineRes } row := []string{ + pointer.SafeDeref(rv.ID), pointer.SafeDeref(rv.Tenant), pointer.SafeDeref(rv.Projectid), pointer.SafeDeref(rv.Partitionid), From 72049a4445310e3d54e84289b857dd77cbdaa846 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 2 Oct 2024 14:16:25 +0200 Subject: [PATCH 18/19] Pin cloud-go. --- go.mod | 2 +- go.sum | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index d61c951..6229e5c 100644 --- a/go.mod +++ b/go.mod @@ -8,7 +8,7 @@ require ( github.com/dustin/go-humanize v1.0.1 github.com/fatih/color v1.17.0 github.com/fi-ts/accounting-go v0.10.0 - github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c + github.com/fi-ts/cloud-go v0.29.0 github.com/gardener/gardener v1.91.0 github.com/gardener/machine-controller-manager v0.53.1 github.com/go-openapi/runtime v0.28.0 diff --git a/go.sum b/go.sum index 1bfd8ff..82119b1 100644 --- a/go.sum +++ b/go.sum @@ -96,8 +96,8 @@ github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= github.com/fi-ts/accounting-go v0.10.0 h1:vbPgTWq1iicyBWFRajX0bawZ1ADbhKGuJyNEtXjpr08= github.com/fi-ts/accounting-go v0.10.0/go.mod h1:ARKouuFYUV44xUKytAlczpzoti/S+o+PnXCN5BQA6nQ= -github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c h1:9ESEl2gizrHyypb6vkLkk2EAaCUdHrxkGn/tJMLBl0c= -github.com/fi-ts/cloud-go v0.28.1-0.20240926120644-8d097ad6845c/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= +github.com/fi-ts/cloud-go v0.29.0 h1:0MSgs4BiBBcCDWEXTwg3h15r0yRf1mGV/17XQ/LGSec= +github.com/fi-ts/cloud-go v0.29.0/go.mod h1:pcGGl+M2OmtvwyuTEOimqSHrZngDotG69lmBzEbx6cc= github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= From 7817931b2736109bb494537b7433fe48d44d8f86 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 2 Oct 2024 14:51:40 +0200 Subject: [PATCH 19/19] Fix. --- cmd/sorters/project-reservations.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/sorters/project-reservations.go b/cmd/sorters/project-reservations.go index c30c97c..3dd3f3b 100644 --- a/cmd/sorters/project-reservations.go +++ b/cmd/sorters/project-reservations.go @@ -48,7 +48,7 @@ func MachineReservationsUsageSorter() *multisort.Sorter[*models.V1MachineReserva "reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Reservations), p.SafeDeref(b.Reservations), descending) }, - "unused-reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { + "used-reservations": func(a, b *models.V1MachineReservationUsageResponse, descending bool) multisort.CompareResult { return multisort.Compare(p.SafeDeref(a.Usedreservations), p.SafeDeref(b.Usedreservations), descending) }, }, multisort.Keys{{ID: "tenant"}, {ID: "project"}, {ID: "partition"}, {ID: "size"}, {ID: "id"}})