diff --git a/cmd/admin/v2/commands.go b/cmd/admin/v2/commands.go index 6ab7358..2af484d 100644 --- a/cmd/admin/v2/commands.go +++ b/cmd/admin/v2/commands.go @@ -16,6 +16,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd.AddCommand(newComponentCmd(c)) adminCmd.AddCommand(newImageCmd(c)) + adminCmd.AddCommand(newPartitionCmd(c)) adminCmd.AddCommand(newProjectCmd(c)) adminCmd.AddCommand(newTenantCmd(c)) adminCmd.AddCommand(newTokenCmd(c)) diff --git a/cmd/admin/v2/partition.go b/cmd/admin/v2/partition.go new file mode 100644 index 0000000..f621feb --- /dev/null +++ b/cmd/admin/v2/partition.go @@ -0,0 +1,137 @@ +package v2 + +import ( + "fmt" + "strings" + + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" + "github.com/metal-stack/cli/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" + "github.com/spf13/cobra" + "github.com/spf13/viper" +) + +type partition struct { + c *config.Config +} + +func newPartitionCmd(c *config.Config) *cobra.Command { + w := &partition{ + c: c, + } + + gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Partition]{ + BinaryName: config.BinaryName, + GenericCLI: gcli, + Singular: "partition", + Plural: "partitions", + Description: "manage partitions", + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + OnlyCmds: genericcli.OnlyCmds(genericcli.DescribeCmd, genericcli.ListCmd), + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.RunE = func(cmd *cobra.Command, args []string) error { + return gcli.DescribeAndPrint("", w.c.DescribePrinter) + } + }, + } + + capacityCmd := &cobra.Command{ + Use: "capacity", + Short: "show partition capacity", + RunE: func(cmd *cobra.Command, args []string) error { + return w.capacity() + }, + } + + capacityCmd.Flags().StringP("id", "", "", "filter on partition id.") + capacityCmd.Flags().StringP("size", "", "", "filter on size id.") + capacityCmd.Flags().StringP("project", "", "", "consider project-specific counts, e.g. size reservations.") + capacityCmd.Flags().StringSlice("sort-by", []string{}, fmt.Sprintf("order by (comma separated) column(s), sort direction can be changed by appending :asc or :desc behind the column identifier. possible values: %s", strings.Join(sorters.PartitionCapacitySorter().AvailableKeys(), "|"))) + genericcli.Must(capacityCmd.RegisterFlagCompletionFunc("id", c.Completion.PartitionListCompletion)) + genericcli.Must(capacityCmd.RegisterFlagCompletionFunc("project", c.Completion.ProjectListCompletion)) + genericcli.Must(capacityCmd.RegisterFlagCompletionFunc("size", c.Completion.SizeListCompletion)) + genericcli.Must(capacityCmd.RegisterFlagCompletionFunc("sort-by", cobra.FixedCompletions(sorters.PartitionCapacitySorter().AvailableKeys(), cobra.ShellCompDirectiveNoFileComp))) + + return genericcli.NewCmds(cmdsConfig, capacityCmd) +} + +func (c *partition) capacity() error { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &adminv2.PartitionServiceCapacityRequest{} + + if viper.IsSet("id") { + req.Id = new(viper.GetString("id")) + } + if viper.IsSet("size") { + req.Size = new(viper.GetString("size")) + } + if viper.IsSet("project") { + req.Project = new(viper.GetString("project")) + } + resp, err := c.c.Client.Adminv2().Partition().Capacity(ctx, req) + if err != nil { + return fmt.Errorf("failed to get partition capacity: %w", err) + } + + err = sorters.PartitionCapacitySorter().SortBy(resp.PartitionCapacity) + if err != nil { + return err + } + + return c.c.ListPrinter.Print(resp.PartitionCapacity) +} + +func (c *partition) Get(id string) (*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.PartitionServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Partition().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get partition: %w", err) + } + + return resp.Partition, nil +} + +func (c *partition) List() ([]*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.PartitionServiceListRequest{Query: &apiv2.PartitionQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + }} + + resp, err := c.c.Client.Apiv2().Partition().List(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get partitions: %w", err) + } + + return resp.Partitions, nil +} + +func (c *partition) Create(rq any) (*apiv2.Partition, error) { + panic("unimplemented") +} + +func (c *partition) Delete(id string) (*apiv2.Partition, error) { + panic("unimplemented") +} + +func (t *partition) Convert(r *apiv2.Partition) (string, any, any, error) { + panic("unimplemented") +} + +func (t *partition) Update(rq any) (*apiv2.Partition, error) { + panic("unimplemented") +} diff --git a/cmd/admin/v2/partition_test.go b/cmd/admin/v2/partition_test.go new file mode 100644 index 0000000..d20e695 --- /dev/null +++ b/cmd/admin/v2/partition_test.go @@ -0,0 +1,250 @@ +package v2_test + +import ( + "testing" + + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + apitests "github.com/metal-stack/api/go/tests" + "github.com/metal-stack/cli/pkg/test" + "github.com/stretchr/testify/mock" +) + +// Generated with AI + +var ( + testPartition1 = &apiv2.Partition{ + Id: "1", + Description: "partition 1", + MgmtServiceAddresses: []string{ + "192.168.1.1:1234", + }, + BootConfiguration: &apiv2.PartitionBootConfiguration{ + Commandline: "commandline", + ImageUrl: "imageurl", + KernelUrl: "kernelurl", + }, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + }, + } + testPartition2 = &apiv2.Partition{ + Id: "2", + Description: "partition 2", + MgmtServiceAddresses: []string{ + "192.168.1.2:1234", + }, + BootConfiguration: &apiv2.PartitionBootConfiguration{ + Commandline: "commandline", + ImageUrl: "imageurl", + KernelUrl: "kernelurl", + }, + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: nil, + }, + }, + } +) + +func Test_AdminPartitionCmd_List(t *testing.T) { + tests := []*test.Test[[]*apiv2.Partition]{ + { + Name: "list", + Cmd: func(want []*apiv2.Partition) []string { + return []string{"admin", "partition", "list"} + }, + ClientMocks: &apitests.ClientMockFns{ + Apiv2Mocks: &apitests.Apiv2MockFns{ + Partition: func(m *mock.Mock) { + m.On("List", mock.Anything, mock.Anything).Return(&apiv2.PartitionServiceListResponse{ + Partitions: []*apiv2.Partition{ + testPartition1, + testPartition2, + }, + }, nil) + }, + }, + }, + Want: []*apiv2.Partition{ + testPartition1, + testPartition2, + }, + WantTable: new(` +ID DESCRIPTION +1 partition 1 +2 partition 2 +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminPartitionCmd_Describe(t *testing.T) { + tests := []*test.Test[*apiv2.Partition]{ + { + Name: "describe", + Cmd: func(want *apiv2.Partition) []string { + return []string{"admin", "partition", "describe", want.Id} + }, + ClientMocks: &apitests.ClientMockFns{ + Apiv2Mocks: &apitests.Apiv2MockFns{ + Partition: func(m *mock.Mock) { + m.On("Get", mock.Anything, mock.Anything).Return(&apiv2.PartitionServiceGetResponse{ + Partition: testPartition1, + }, nil) + }, + }, + }, + Want: testPartition1, + WantTable: new(` +ID DESCRIPTION +1 partition 1 +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminPartitionCmd_Capacity(t *testing.T) { + tests := []*test.Test[[]*adminv2.PartitionCapacity]{ + { + Name: "capacity", + Cmd: func(want []*adminv2.PartitionCapacity) []string { + return []string{"admin", "partition", "capacity"} + }, + ClientMocks: &apitests.ClientMockFns{ + Adminv2Mocks: &apitests.Adminv2MockFns{ + Partition: func(m *mock.Mock) { + m.On("Capacity", mock.Anything, mock.Anything).Return(&adminv2.PartitionServiceCapacityResponse{ + PartitionCapacity: []*adminv2.PartitionCapacity{ + { + Partition: "partition-1", + MachineSizeCapacities: []*adminv2.MachineSizeCapacity{ + { + Size: "size-1", + Free: 3, + Allocated: 1, + Total: 5, + Faulty: 2, + Reservations: 3, + UsedReservations: 1, + }, + }, + }, + }, + }, nil) + }, + }, + }, + Want: []*adminv2.PartitionCapacity{ + { + Partition: "partition-1", + MachineSizeCapacities: []*adminv2.MachineSizeCapacity{ + { + Size: "size-1", + Free: 3, + Allocated: 1, + Total: 5, + Faulty: 2, + Reservations: 3, + UsedReservations: 1, + }, + }, + }, + }, + WantTable: new(` +PARTITION SIZE ALLOCATED FREE UNAVAILABLE RESERVATIONS | TOTAL | FAULTY +partition-1 size-1 1 3 0 2 (1/3 used) | 5 | 2 +Total 1 3 0 2 | 5 | 2 +`), + }, + { + Name: "capacity with filters", + Cmd: func(want []*adminv2.PartitionCapacity) []string { + return []string{"admin", "partition", "capacity", "--id", "partition-1", "--size", "size-1", "--project", "project-123", "--sort-by", "id"} + }, + ClientMocks: &apitests.ClientMockFns{ + Adminv2Mocks: &apitests.Adminv2MockFns{ + Partition: func(m *mock.Mock) { + m.On("Capacity", mock.Anything, mock.Anything).Return(&adminv2.PartitionServiceCapacityResponse{ + PartitionCapacity: []*adminv2.PartitionCapacity{ + { + Partition: "partition-1", + MachineSizeCapacities: []*adminv2.MachineSizeCapacity{ + { + Size: "size-1", + Free: 3, + Allocated: 1, + Total: 5, + Faulty: 2, + Reservations: 3, + UsedReservations: 1, + }, + }, + }, + }, + }, nil) + }, + }, + }, + Want: []*adminv2.PartitionCapacity{ + { + Partition: "partition-1", + MachineSizeCapacities: []*adminv2.MachineSizeCapacity{ + { + Size: "size-1", + Free: 3, + Allocated: 1, + Total: 5, + Faulty: 2, + Reservations: 3, + UsedReservations: 1, + }, + }, + }, + }, + WantTable: new(` +PARTITION SIZE ALLOCATED FREE UNAVAILABLE RESERVATIONS | TOTAL | FAULTY +partition-1 size-1 1 3 0 2 (1/3 used) | 5 | 2 +Total 1 3 0 2 | 5 | 2 +`), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminPartitionCmd_ExhaustiveArgs(t *testing.T) { + tests := []struct { + name string + args []string + }{ + { + name: "list", + args: []string{"admin", "partition", "list"}, + }, + { + name: "describe", + args: []string{"admin", "partition", "describe", "1"}, + }, + { + name: "capacity", + args: []string{"admin", "partition", "capacity", "--id", "partition-1", "--size", "size-1", "--project", "project-123", "--sort-by", "id"}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + test.AssertExhaustiveArgs(t, tt.args) + }) + } +} diff --git a/cmd/api/v2/commands.go b/cmd/api/v2/commands.go index be489cc..0476351 100644 --- a/cmd/api/v2/commands.go +++ b/cmd/api/v2/commands.go @@ -6,13 +6,14 @@ import ( ) func AddCmds(cmd *cobra.Command, c *config.Config) { - cmd.AddCommand(newVersionCmd(c)) cmd.AddCommand(newHealthCmd(c)) - cmd.AddCommand(newTokenCmd(c)) - cmd.AddCommand(newIPCmd(c)) cmd.AddCommand(newImageCmd(c)) + cmd.AddCommand(newIPCmd(c)) + cmd.AddCommand(newMethodsCmd(c)) + cmd.AddCommand(newPartitionCmd(c)) cmd.AddCommand(newProjectCmd(c)) cmd.AddCommand(newTenantCmd(c)) - cmd.AddCommand(newMethodsCmd(c)) + cmd.AddCommand(newTokenCmd(c)) cmd.AddCommand(newUserCmd(c)) + cmd.AddCommand(newVersionCmd(c)) } diff --git a/cmd/api/v2/partition.go b/cmd/api/v2/partition.go new file mode 100644 index 0000000..ead6d61 --- /dev/null +++ b/cmd/api/v2/partition.go @@ -0,0 +1,87 @@ +package v2 + +import ( + "fmt" + + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/cli/cmd/config" + "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 partition struct { + c *config.Config +} + +func newPartitionCmd(c *config.Config) *cobra.Command { + w := &partition{ + c: c, + } + + gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Partition]{ + BinaryName: config.BinaryName, + GenericCLI: gcli, + Singular: "partition", + Plural: "partitions", + Description: "list and get partitions", + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + OnlyCmds: genericcli.OnlyCmds(genericcli.DescribeCmd, genericcli.ListCmd), + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("id", "", "", "image id to filter for") + }, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *partition) Get(id string) (*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.PartitionServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Partition().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get partition: %w", err) + } + + return resp.Partition, nil +} + +func (c *partition) List() ([]*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.PartitionServiceListRequest{Query: &apiv2.PartitionQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + }} + + resp, err := c.c.Client.Apiv2().Partition().List(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get partitions: %w", err) + } + + return resp.Partitions, nil +} + +func (c *partition) Create(rq any) (*apiv2.Partition, error) { + panic("unimplemented") +} + +func (c *partition) Delete(id string) (*apiv2.Partition, error) { + panic("unimplemented") +} + +func (t *partition) Convert(r *apiv2.Partition) (string, any, any, error) { + panic("unimplemented") +} + +func (t *partition) Update(rq any) (*apiv2.Partition, error) { + panic("unimplemented") +} diff --git a/cmd/completion/partition.go b/cmd/completion/partition.go new file mode 100644 index 0000000..0e71613 --- /dev/null +++ b/cmd/completion/partition.go @@ -0,0 +1,19 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) PartitionListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + req := &apiv2.PartitionServiceListRequest{} + resp, err := c.Client.Apiv2().Partition().List(c.Ctx, req) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, p := range resp.Partitions { + names = append(names, p.Id+"\t"+p.Description) + } + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/completion/size.go b/cmd/completion/size.go new file mode 100644 index 0000000..8ebd814 --- /dev/null +++ b/cmd/completion/size.go @@ -0,0 +1,19 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) SizeListCompletion(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + req := &apiv2.SizeServiceListRequest{} + resp, err := c.Client.Apiv2().Size().List(c.Ctx, req) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + var names []string + for _, s := range resp.Sizes { + names = append(names, s.Id) + } + return names, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/root.go b/cmd/root.go index f561d97..fc27a8f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -28,7 +28,7 @@ func Execute() { Completion: &completion.Completion{}, } - cmd := newRootCmd(cfg) + cmd := NewRootCmd(cfg) err := cmd.Execute() if err != nil { @@ -40,7 +40,7 @@ func Execute() { } } -func newRootCmd(c *config.Config) *cobra.Command { +func NewRootCmd(c *config.Config) *cobra.Command { rootCmd := &cobra.Command{ Use: config.BinaryName, Aliases: []string{"m"}, diff --git a/cmd/sorters/partition.go b/cmd/sorters/partition.go new file mode 100644 index 0000000..c0f2c46 --- /dev/null +++ b/cmd/sorters/partition.go @@ -0,0 +1,30 @@ +package sorters + +import ( + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/multisort" +) + +func PartitionSorter() *multisort.Sorter[*apiv2.Partition] { + return multisort.New(multisort.FieldMap[*apiv2.Partition]{ + "id": func(a, b *apiv2.Partition, descending bool) multisort.CompareResult { + return multisort.Compare(a.Id, b.Id, descending) + }, + "description": func(a, b *apiv2.Partition, descending bool) multisort.CompareResult { + return multisort.Compare(a.Description, b.Description, descending) + }, + }, multisort.Keys{{ID: "id"}, {ID: "description"}}) +} + +func PartitionCapacitySorter() *multisort.Sorter[*adminv2.PartitionCapacity] { + return multisort.New(multisort.FieldMap[*adminv2.PartitionCapacity]{ + "id": func(a, b *adminv2.PartitionCapacity, descending bool) multisort.CompareResult { + return multisort.Compare(a.Partition, b.Partition, descending) + }, + "size": func(a, b *adminv2.PartitionCapacity, descending bool) multisort.CompareResult { + // FIXME implement + return multisort.Compare(a.Partition, b.Partition, descending) + }, + }, multisort.Keys{{ID: "id"}, {ID: "size"}}) +} diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index 78fad32..259da2b 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -6,6 +6,7 @@ import ( "strings" "time" + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/genericcli/printers" @@ -58,6 +59,15 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.ProjectMember: return t.ProjectMemberTable(d, wide) + case *apiv2.Partition: + return t.PartitionTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Partition: + return t.PartitionTable(d, wide) + case *adminv2.PartitionCapacity: + return t.PartitionCapacityTable(pointer.WrapInSlice(d), wide) + case []*adminv2.PartitionCapacity: + return t.PartitionCapacityTable(d, wide) + case *apiv2.Token: return t.TokenTable(pointer.WrapInSlice(d), wide) case []*apiv2.Token: diff --git a/cmd/tableprinters/partition.go b/cmd/tableprinters/partition.go new file mode 100644 index 0000000..0c99a83 --- /dev/null +++ b/cmd/tableprinters/partition.go @@ -0,0 +1,134 @@ +package tableprinters + +import ( + "fmt" + "sort" + "strings" + + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/genericcli" +) + +func (t *TablePrinter) PartitionTable(data []*apiv2.Partition, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"ID", "Description"} + rows [][]string + ) + + if wide { + header = []string{"ID", "Description", "Labels"} + } + + for _, p := range data { + row := []string{p.Id, p.Description} + + if wide { + labels := genericcli.MapToLabels(p.Meta.Labels.Labels) + sort.Strings(labels) + row = append(row, strings.Join(labels, "\n")) + } + + rows = append(rows, row) + } + + return header, rows, nil +} + +func (t *TablePrinter) PartitionCapacityTable(data []*adminv2.PartitionCapacity, wide bool) ([]string, [][]string, error) { + var ( + header = []string{"Partition", "Size", "Allocated", "Free", "Unavailable", "Reservations", "|", "Total", "|", "Faulty"} + rows [][]string + + allocatedCount int64 + faultyCount int64 + freeCount int64 + otherCount int64 + phonedHomeCount int64 + reservationCount int64 + totalCount int64 + unavailableCount int64 + usedReservationCount int64 + waitingCount int64 + ) + + if wide { + header = append(header, "Phoned Home", "Waiting", "Other") + } + + for _, pc := range data { + for _, c := range pc.MachineSizeCapacities { + id := c.Size + var ( + allocated = fmt.Sprintf("%d", c.Allocated) + faulty = fmt.Sprintf("%d", c.Faulty) + free = fmt.Sprintf("%d", c.Free) + other = fmt.Sprintf("%d", c.Other) + phonedHome = fmt.Sprintf("%d", c.PhonedHome) + reservations = "0" + total = fmt.Sprintf("%d", c.Total) + unavailable = fmt.Sprintf("%d", c.Unavailable) + waiting = fmt.Sprintf("%d", c.Waiting) + ) + + if c.Reservations > 0 { + reservations = fmt.Sprintf("%d (%d/%d used)", c.Reservations-c.UsedReservations, c.UsedReservations, c.Reservations) + } + + allocatedCount += c.Allocated + faultyCount += c.Faulty + freeCount += c.Free + otherCount += c.Other + phonedHomeCount += c.PhonedHome + reservationCount += c.Reservations + totalCount += c.Total + unavailableCount += c.Unavailable + usedReservationCount += c.UsedReservations + waitingCount += c.Waiting + + row := []string{pc.Partition, id, allocated, free, unavailable, reservations, "|", total, "|", faulty} + if wide { + row = append(row, phonedHome, waiting, other) + } + + rows = append(rows, row) + } + } + + footerRow := ([]string{ + "Total", + "", + fmt.Sprintf("%d", allocatedCount), + fmt.Sprintf("%d", freeCount), + fmt.Sprintf("%d", unavailableCount), + fmt.Sprintf("%d", reservationCount-usedReservationCount), + "|", + fmt.Sprintf("%d", totalCount), + "|", + fmt.Sprintf("%d", faultyCount), + }) + + if wide { + footerRow = append(footerRow, []string{ + fmt.Sprintf("%d", phonedHomeCount), + fmt.Sprintf("%d", waitingCount), + fmt.Sprintf("%d", otherCount), + }...) + } + + // if t.markdown { + // // for markdown we already have enough dividers, remove them + // removeDivider := func(e string) bool { + // return e == "|" + // } + // header = slices.DeleteFunc(header, removeDivider) + // footerRow = slices.DeleteFunc(footerRow, removeDivider) + // for i, row := range rows { + // rows[i] = slices.DeleteFunc(row, removeDivider) + // } + // } + + rows = append(rows, footerRow) + + return header, rows, nil +} diff --git a/docs/metalctlv2.md b/docs/metalctlv2.md index 4671daf..b96c43f 100644 --- a/docs/metalctlv2.md +++ b/docs/metalctlv2.md @@ -27,6 +27,7 @@ cli for managing entities in metal-stack * [metalctlv2 login](metalctlv2_login.md) - login * [metalctlv2 logout](metalctlv2_logout.md) - logout * [metalctlv2 markdown](metalctlv2_markdown.md) - create markdown documentation +* [metalctlv2 partition](metalctlv2_partition.md) - manage partition entities * [metalctlv2 project](metalctlv2_project.md) - manage project entities * [metalctlv2 tenant](metalctlv2_tenant.md) - manage tenant entities * [metalctlv2 token](metalctlv2_token.md) - manage token entities diff --git a/docs/metalctlv2_partition.md b/docs/metalctlv2_partition.md new file mode 100644 index 0000000..581fe57 --- /dev/null +++ b/docs/metalctlv2_partition.md @@ -0,0 +1,33 @@ +## metalctlv2 partition + +manage partition entities + +### Synopsis + +list and get partitions + +### Options + +``` + -h, --help help for partition +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api (default "https://api.metal-stack.io") + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2](metalctlv2.md) - cli for managing entities in metal-stack +* [metalctlv2 partition describe](metalctlv2_partition_describe.md) - describes the partition +* [metalctlv2 partition list](metalctlv2_partition_list.md) - list all partitions + diff --git a/docs/metalctlv2_partition_describe.md b/docs/metalctlv2_partition_describe.md new file mode 100644 index 0000000..4dc9186 --- /dev/null +++ b/docs/metalctlv2_partition_describe.md @@ -0,0 +1,31 @@ +## metalctlv2 partition describe + +describes the partition + +``` +metalctlv2 partition describe [flags] +``` + +### Options + +``` + -h, --help help for describe +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api (default "https://api.metal-stack.io") + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 partition](metalctlv2_partition.md) - manage partition entities + diff --git a/docs/metalctlv2_partition_list.md b/docs/metalctlv2_partition_list.md new file mode 100644 index 0000000..8967f09 --- /dev/null +++ b/docs/metalctlv2_partition_list.md @@ -0,0 +1,32 @@ +## metalctlv2 partition list + +list all partitions + +``` +metalctlv2 partition list [flags] +``` + +### Options + +``` + -h, --help help for list + --id string image id to filter for +``` + +### Options inherited from parent commands + +``` + --api-token string the token used for api requests + --api-url string the url to the metal-stack.io api (default "https://api.metal-stack.io") + -c, --config string alternative config file path, (default is ~/.metal-stack/config.yaml) + --debug debug output + --force-color force colored output even without tty + -o, --output-format string output format (table|wide|markdown|json|yaml|template|jsonraw|yamlraw), wide is a table with more columns, jsonraw and yamlraw do not translate proto enums into string types but leave the original int32 values intact. (default "table") + --template string output template for template output-format, go template format. For property names inspect the output of -o json or -o yaml for reference. + --timeout duration request timeout used for api requests +``` + +### SEE ALSO + +* [metalctlv2 partition](metalctlv2_partition.md) - manage partition entities + diff --git a/go.mod b/go.mod index 845411f..638da21 100644 --- a/go.mod +++ b/go.mod @@ -14,7 +14,6 @@ require ( github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 - golang.org/x/net v0.50.0 google.golang.org/protobuf v1.36.11 sigs.k8s.io/yaml v1.6.0 ) @@ -23,9 +22,8 @@ require ( buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1 // indirect connectrpc.com/connect v1.19.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/clipperhouse/displaywidth v0.6.2 // indirect - github.com/clipperhouse/stringish v0.1.1 // indirect - github.com/clipperhouse/uax29/v2 v2.3.0 // indirect + github.com/clipperhouse/displaywidth v0.11.0 // indirect + github.com/clipperhouse/uax29/v2 v2.7.0 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect @@ -33,6 +31,7 @@ require ( github.com/go-openapi/strfmt v0.25.0 // indirect github.com/go-task/slim-sprig/v3 v3.0.0 // indirect github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect github.com/golang-jwt/jwt/v5 v5.3.1 // indirect github.com/google/uuid v1.6.0 // indirect @@ -46,7 +45,7 @@ require ( github.com/oklog/ulid v1.3.1 // indirect github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 // indirect github.com/olekukonko/errors v1.2.0 // indirect - github.com/olekukonko/ll v0.1.6 // indirect + github.com/olekukonko/ll v0.1.7 // indirect github.com/olekukonko/tablewriter v1.1.3 // indirect github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect @@ -58,6 +57,7 @@ require ( go.mongodb.org/mongo-driver v1.17.9 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/net v0.50.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect gopkg.in/inf.v0 v0.9.1 // indirect diff --git a/go.sum b/go.sum index c106f0c..b097bef 100644 --- a/go.sum +++ b/go.sum @@ -6,12 +6,10 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/ github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/clipperhouse/displaywidth v0.6.2 h1:ZDpTkFfpHOKte4RG5O/BOyf3ysnvFswpyYrV7z2uAKo= -github.com/clipperhouse/displaywidth v0.6.2/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o= -github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs= -github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA= -github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4= -github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g= +github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSEFgwIwO+UVM8= +github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= +github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= +github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo= github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -35,6 +33,8 @@ github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1v github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= +github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= @@ -73,8 +73,8 @@ github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6 h1:zrbMGy9YXpIeTnGj github.com/olekukonko/cat v0.0.0-20250911104152-50322a0618f6/go.mod h1:rEKTHC9roVVicUIfZK7DYrdIoM0EOr8mK1Hj5s3JjH0= github.com/olekukonko/errors v1.2.0 h1:10Zcn4GeV59t/EGqJc8fUjtFT/FuUh5bTMzZ1XwmCRo= github.com/olekukonko/errors v1.2.0/go.mod h1:ppzxA5jBKcO1vIpCXQ9ZqgDh8iwODz6OXIGKU8r5m4Y= -github.com/olekukonko/ll v0.1.6 h1:lGVTHO+Qc4Qm+fce/2h2m5y9LvqaW+DCN7xW9hsU3uA= -github.com/olekukonko/ll v0.1.6/go.mod h1:NVUmjBb/aCtUpjKk75BhWrOlARz3dqsM+OtszpY4o88= +github.com/olekukonko/ll v0.1.7 h1:WyK1YZwOTUKHEXZz3VydBDT5t3zDqa9yI8iJg5PHon4= +github.com/olekukonko/ll v0.1.7/go.mod h1:RPRC6UcscfFZgjo1nulkfMH5IM0QAYim0LfnMvUuozw= github.com/olekukonko/tablewriter v1.1.3 h1:VSHhghXxrP0JHl+0NnKid7WoEmd9/urKRJLysb70nnA= github.com/olekukonko/tablewriter v1.1.3/go.mod h1:9VU0knjhmMkXjnMKrZ3+L2JhhtsQ/L38BbL3CRNE8tM= github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= diff --git a/cmd/common_test.go b/pkg/test/common.go similarity index 98% rename from cmd/common_test.go rename to pkg/test/common.go index 5befb1a..50b3879 100644 --- a/cmd/common_test.go +++ b/pkg/test/common.go @@ -1,4 +1,4 @@ -package cmd +package test import ( "bytes" @@ -14,6 +14,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" apitests "github.com/metal-stack/api/go/tests" + "github.com/metal-stack/cli/cmd" "github.com/metal-stack/cli/cmd/completion" "github.com/metal-stack/cli/cmd/config" "github.com/metal-stack/metal-lib/pkg/pointer" @@ -52,7 +53,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { if c.WantErr != nil { _, _, conf := c.newMockConfig(t) - cmd := newRootCmd(conf) + cmd := cmd.NewRootCmd(conf) os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) err := cmd.Execute() @@ -65,7 +66,7 @@ func (c *Test[R]) TestCmd(t *testing.T) { t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { _, out, conf := c.newMockConfig(t) - cmd := newRootCmd(conf) + cmd := cmd.NewRootCmd(conf) os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) os.Args = append(os.Args, format.Args()...) @@ -119,7 +120,7 @@ func AssertExhaustiveArgs(t *testing.T, args []string, exclude ...string) { return fmt.Errorf("not exhaustive: does not contain %q", prefix) } - root := newRootCmd(&config.Config{}) + root := cmd.NewRootCmd(&config.Config{}) cmd, args, err := root.Find(args) require.NoError(t, err)