diff --git a/cmd/admin/v2/commands.go b/cmd/admin/v2/commands.go index ff90b69..277027b 100644 --- a/cmd/admin/v2/commands.go +++ b/cmd/admin/v2/commands.go @@ -17,6 +17,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd.AddCommand(newAuditCmd(c)) adminCmd.AddCommand(newComponentCmd(c)) adminCmd.AddCommand(newImageCmd(c)) + adminCmd.AddCommand(newPartitionCmd(c)) adminCmd.AddCommand(newProjectCmd(c)) adminCmd.AddCommand(newSizeCmd(c)) adminCmd.AddCommand(newSwitchCmd(c)) diff --git a/cmd/admin/v2/partition.go b/cmd/admin/v2/partition.go new file mode 100644 index 0000000..715ce4c --- /dev/null +++ b/cmd/admin/v2/partition.go @@ -0,0 +1,317 @@ +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[*adminv2.PartitionServiceCreateRequest, *adminv2.PartitionServiceUpdateRequest, *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, + genericcli.CreateCmd, + genericcli.UpdateCmd, + genericcli.DeleteCmd, + genericcli.EditCmd, + ), + CreateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "the id of the partition to create") + genericcli.Must(cmd.MarkFlagRequired("id")) + partitionMutableFlags(cmd) + }, + CreateRequestFromCLI: func() (*adminv2.PartitionServiceCreateRequest, error) { + return &adminv2.PartitionServiceCreateRequest{ + Partition: &apiv2.Partition{ + Id: viper.GetString("id"), + Description: viper.GetString("description"), + BootConfiguration: partitionBootConfigurationFromCLI(), + DnsServers: dnsServersFromCLI(viper.GetStringSlice("dns-servers")), + NtpServers: ntpServersFromCLI(viper.GetStringSlice("ntp-servers")), + MgmtServiceAddresses: viper.GetStringSlice("mgmt-service-addresses"), + }, + }, nil + }, + UpdateCmdMutateFn: partitionMutableFlags, + UpdateRequestFromCLI: w.updateRequestFromCLI, + DescribeCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().String("id", "", "id of the partition") + cmd.RunE = func(cmd *cobra.Command, args []string) error { + id, err := cmd.Flags().GetString("id") + if err != nil { + return err + } + if id == "" && len(args) > 0 { + id = args[0] + } + p, err := w.Get(id) + if err != nil { + return err + } + return w.c.DescribePrinter.Print(p) + } + }, + ValidArgsFn: c.Completion.PartitionListCompletion, + } + + 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) +} + +// Create and Update share these mutable flags +func partitionMutableFlags(cmd *cobra.Command) { + cmd.Flags().String("description", "", "the description of the partition") + cmd.Flags().String("image-url", "", "the url of the boot image used by metal-hammer") + cmd.Flags().String("kernel-url", "", "the url of the kernel used by metal-hammer") + cmd.Flags().String("commandline", "", "the kernel commandline used by metal-hammer") + cmd.Flags().StringSlice("dns-servers", nil, "the dns servers of this partition") + cmd.Flags().StringSlice("ntp-servers", nil, "the ntp servers of this partition") + cmd.Flags().StringSlice("mgmt-service-addresses", nil, "the management service addresses of this partition, each in the form :") +} + +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 *adminv2.PartitionServiceCreateRequest) (*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Partition().Create(ctx, rq) + if err != nil { + return nil, fmt.Errorf("failed to create partition: %w", err) + } + + return resp.Partition, nil +} + +func (c *partition) Delete(id string) (*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &adminv2.PartitionServiceDeleteRequest{Id: id} + + resp, err := c.c.Client.Adminv2().Partition().Delete(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to delete partition: %w", err) + } + + return resp.Partition, nil +} + +func (c *partition) Convert(r *apiv2.Partition) (string, *adminv2.PartitionServiceCreateRequest, *adminv2.PartitionServiceUpdateRequest, error) { + return r.Id, &adminv2.PartitionServiceCreateRequest{ + Partition: r, + }, &adminv2.PartitionServiceUpdateRequest{ + Id: r.Id, + Description: new(r.Description), + BootConfiguration: r.BootConfiguration, + DnsServers: r.DnsServers, + NtpServers: r.NtpServers, + MgmtServiceAddresses: r.MgmtServiceAddresses, + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, + UpdatedAt: r.Meta.GetUpdatedAt(), + }, + }, nil +} + +func (c *partition) Update(rq *adminv2.PartitionServiceUpdateRequest) (*apiv2.Partition, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Partition().Update(ctx, rq) + if err != nil { + return nil, fmt.Errorf("failed to update partition: %w", err) + } + + return resp.Partition, nil +} + +func (c *partition) updateRequestFromCLI(args []string) (*adminv2.PartitionServiceUpdateRequest, error) { + id, err := genericcli.GetExactlyOneArg(args) + if err != nil { + return nil, err + } + + current, err := c.Get(id) + if err != nil { + return nil, fmt.Errorf("unable to retrieve partition: %w", err) + } + + req := &adminv2.PartitionServiceUpdateRequest{ + Id: id, + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, + UpdatedAt: current.Meta.GetUpdatedAt(), + }, + } + + if viper.IsSet("description") { + req.Description = new(viper.GetString("description")) + } + if bc := patchPartitionBootConfiguration(current.BootConfiguration); bc != nil { + req.BootConfiguration = bc + } + if viper.IsSet("dns-servers") { + req.DnsServers = dnsServersFromCLI(viper.GetStringSlice("dns-servers")) + } + if viper.IsSet("ntp-servers") { + req.NtpServers = ntpServersFromCLI(viper.GetStringSlice("ntp-servers")) + } + if viper.IsSet("mgmt-service-addresses") { + req.MgmtServiceAddresses = viper.GetStringSlice("mgmt-service-addresses") + } + + return req, nil +} + +func partitionBootConfigurationFromCLI() *apiv2.PartitionBootConfiguration { + if !viper.IsSet("image-url") && !viper.IsSet("kernel-url") && !viper.IsSet("commandline") { + return nil + } + + return &apiv2.PartitionBootConfiguration{ + ImageUrl: viper.GetString("image-url"), + KernelUrl: viper.GetString("kernel-url"), + Commandline: viper.GetString("commandline"), + } +} + +func patchPartitionBootConfiguration(current *apiv2.PartitionBootConfiguration) *apiv2.PartitionBootConfiguration { + if !viper.IsSet("image-url") && !viper.IsSet("kernel-url") && !viper.IsSet("commandline") { + return nil + } + + patched := &apiv2.PartitionBootConfiguration{} + if current != nil { + patched.ImageUrl = current.ImageUrl + patched.KernelUrl = current.KernelUrl + patched.Commandline = current.Commandline + } + + if viper.IsSet("image-url") { + patched.ImageUrl = viper.GetString("image-url") + } + if viper.IsSet("kernel-url") { + patched.KernelUrl = viper.GetString("kernel-url") + } + if viper.IsSet("commandline") { + patched.Commandline = viper.GetString("commandline") + } + + return patched +} + +func dnsServersFromCLI(ips []string) []*apiv2.DNSServer { + var servers []*apiv2.DNSServer + for _, ip := range ips { + servers = append(servers, &apiv2.DNSServer{Ip: ip}) + } + return servers +} + +func ntpServersFromCLI(addresses []string) []*apiv2.NTPServer { + var servers []*apiv2.NTPServer + for _, address := range addresses { + servers = append(servers, &apiv2.NTPServer{Address: address}) + } + return servers +} diff --git a/cmd/api/v2/commands.go b/cmd/api/v2/commands.go index 3f4278a..5e72d47 100644 --- a/cmd/api/v2/commands.go +++ b/cmd/api/v2/commands.go @@ -11,6 +11,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { cmd.AddCommand(newImageCmd(c)) cmd.AddCommand(newIPCmd(c)) cmd.AddCommand(newMethodsCmd(c)) + cmd.AddCommand(newPartitionCmd(c)) cmd.AddCommand(newProjectCmd(c)) cmd.AddCommand(newSizeCmd(c)) cmd.AddCommand(newTenantCmd(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/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 ead65ec..a0f5ec3 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -90,6 +90,15 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case *adminv2.TaskServiceQueuesResponse: return t.TaskQueueTable(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 df68a11..309ad53 100644 --- a/docs/metalctlv2.md +++ b/docs/metalctlv2.md @@ -28,6 +28,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 size](metalctlv2_size.md) - manage size entities * [metalctlv2 tenant](metalctlv2_tenant.md) - manage tenant entities diff --git a/docs/metalctlv2_partition.md b/docs/metalctlv2_partition.md new file mode 100644 index 0000000..cbebf79 --- /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 (for apply, create, update, delete commands from file the raw output formatters must be used). (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..1d694d1 --- /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 (for apply, create, update, delete commands from file the raw output formatters must be used). (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..8e0e030 --- /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 (for apply, create, update, delete commands from file the raw output formatters must be used). (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 229d67d..6b1f272 100644 --- a/go.mod +++ b/go.mod @@ -14,6 +14,7 @@ require ( github.com/metal-stack/v v1.0.3 github.com/spf13/afero v1.15.0 github.com/spf13/cobra v1.10.2 + github.com/spf13/pflag v1.0.10 github.com/spf13/viper v1.21.0 github.com/stretchr/testify v1.11.1 google.golang.org/grpc v1.81.1 @@ -58,7 +59,6 @@ require ( github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sagikazarmark/locafero v0.12.0 // indirect github.com/spf13/cast v1.10.0 // indirect - github.com/spf13/pflag v1.0.10 // indirect github.com/stretchr/objx v0.5.3 // indirect github.com/subosito/gotenv v1.6.0 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect @@ -71,6 +71,6 @@ require ( google.golang.org/genproto/googleapis/rpc v0.0.0-20260622175928-b703f567277d // indirect gopkg.in/inf.v0 v0.9.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect - k8s.io/apimachinery v0.35.1 // indirect + k8s.io/apimachinery v0.35.3 // indirect sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect ) diff --git a/go.sum b/go.sum index 7a487ae..bc136d3 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYW github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw= github.com/brianvoe/gofakeit/v6 v6.28.0 h1:Xib46XXuQfmlLS2EXRuJpqcw8St6qSZz75OUo0tgAW4= github.com/brianvoe/gofakeit/v6 v6.28.0/go.mod h1:Xj58BMSnFqcn/fAQeSK+/PLtC5kSb7FJIq4JyGa8vEs= +github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw= +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.10.0 h1:GhBG8WuerxjFQQYeuZAeVTuyxuX+UraiZGD4HJQ3Y8g= @@ -146,8 +148,8 @@ gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -k8s.io/apimachinery v0.35.1 h1:yxO6gV555P1YV0SANtnTjXYfiivaTPvCTKX6w6qdDsU= -k8s.io/apimachinery v0.35.1/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/apimachinery v0.35.3 h1:MeaUwQCV3tjKP4bcwWGgZ/cp/vpsRnQzqO6J6tJyoF8= +k8s.io/apimachinery v0.35.3/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= diff --git a/pkg/test/common.go b/pkg/test/common.go new file mode 100644 index 0000000..50b3879 --- /dev/null +++ b/pkg/test/common.go @@ -0,0 +1,313 @@ +package test + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "os" + "strings" + "testing" + + "slices" + + "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" + "github.com/metal-stack/metal-lib/pkg/testcommon" + "github.com/spf13/afero" + "github.com/spf13/pflag" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "google.golang.org/protobuf/runtime/protoimpl" + "sigs.k8s.io/yaml" +) + +type Test[R any] struct { + Name string + Cmd func(want R) []string + + ClientMocks *apitests.ClientMockFns + FsMocks func(fs afero.Fs, want R) + MockStdin *bytes.Buffer + + DisableMockClient bool // can switch off mock client creation + + 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") + + if c.WantErr != nil { + _, _, conf := c.newMockConfig(t) + + cmd := cmd.NewRootCmd(conf) + os.Args = append([]string{config.BinaryName}, c.Cmd(c.Want)...) + + err := cmd.Execute() + if diff := cmp.Diff(c.WantErr, err, testcommon.IgnoreUnexported(), testcommon.ErrorStringComparer()); diff != "" { + t.Errorf("error diff (+got -want):\n %s", diff) + } + } + + for _, format := range outputFormats(c) { + t.Run(fmt.Sprintf("%v", format.Args()), func(t *testing.T) { + _, out, conf := c.newMockConfig(t) + + cmd := cmd.NewRootCmd(conf) + os.Args = append([]string{config.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) (any, *bytes.Buffer, *config.Config) { + mock := apitests.New(t) + + fs := afero.NewMemMapFs() + if c.FsMocks != nil { + c.FsMocks(fs, c.Want) + } + + var in io.Reader + if c.MockStdin != nil { + in = bytes.NewReader(c.MockStdin.Bytes()) + } + + var ( + out bytes.Buffer + config = &config.Config{ + Fs: fs, + Out: &out, + In: in, + PromptOut: io.Discard, + Completion: &completion.Completion{}, + Client: mock.Client(c.ClientMocks), + } + ) + + if c.DisableMockClient { + config.Client = nil + } + + return nil, &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 %q", prefix) + } + + root := cmd.NewRootCmd(&config.Config{}) + 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", "jsonraw"} +} + +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.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); 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", "yamlraw"} +} + +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.IgnoreUnexported(), cmpopts.IgnoreTypes(protoimpl.MessageState{})); 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]) + } + } +} diff --git a/testing/e2e/test_cmd.go b/testing/e2e/test_cmd.go index c8a24ca..ba36594 100644 --- a/testing/e2e/test_cmd.go +++ b/testing/e2e/test_cmd.go @@ -15,6 +15,7 @@ import ( e2e_test "github.com/metal-stack/metal-lib/pkg/genericcli/e2e" "github.com/spf13/afero" "github.com/spf13/cobra" + "github.com/spf13/viper" "github.com/stretchr/testify/require" ) @@ -53,6 +54,8 @@ func NewRootCmd(t *testing.T, c *TestConfig) e2e_test.NewRootCmdFunc { var out bytes.Buffer + viper.Reset() + return cmd.NewRootCmd(&config.Config{ Fs: fs, Out: &out, diff --git a/tests/e2e/admin/partition_test.go b/tests/e2e/admin/partition_test.go new file mode 100644 index 0000000..dad2935 --- /dev/null +++ b/tests/e2e/admin/partition_test.go @@ -0,0 +1,191 @@ +package admin_e2e + +import ( + "testing" + + "connectrpc.com/connect" + "github.com/metal-stack/api/go/client" + + adminv2 "github.com/metal-stack/api/go/metalstack/admin/v2" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + e2erootcmd "github.com/metal-stack/cli/testing/e2e" + "github.com/metal-stack/cli/tests/e2e/testresources" + e2e "github.com/metal-stack/metal-lib/pkg/genericcli/e2e" +) + +func Test_AdminPartitionCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.PartitionServiceListResponse, apiv2.Partition]{ + { + Name: "list", + CmdArgs: []string{"admin", "partition", "list"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.PartitionServiceListRequest{ + Query: &apiv2.PartitionQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.PartitionServiceListResponse{ + Partitions: []*apiv2.Partition{ + testresources.Partition1(), + testresources.Partition2(), + }, + }) + }, + }, + }, + }), + WantTable: new(` + ID DESCRIPTION + partition-1 partition 1 + partition-2 partition 2 + `), + WantWideTable: new(` + ID DESCRIPTION LABELS + partition-1 partition 1 a=b + partition-2 partition 2 + `), + Template: new("{{ .id }} {{ .description }}"), + WantTemplate: new(` +partition-1 partition 1 +partition-2 partition 2 + `), + WantMarkdown: new(` + | ID | DESCRIPTION | + |-------------|-------------| + | partition-1 | partition 1 | + | partition-2 | partition 2 | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminPartitionCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.PartitionServiceGetResponse, *apiv2.Partition]{ + { + Name: "describe", + CmdArgs: []string{"admin", "partition", "describe", testresources.Partition1().Id}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.PartitionServiceGetRequest{ + Id: testresources.Partition1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.PartitionServiceGetResponse{ + Partition: testresources.Partition1(), + }) + }, + }, + }, + }), + WantObject: testresources.Partition1(), + WantProtoObject: testresources.Partition1(), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminPartitionCmd_Capacity(t *testing.T) { + tests := []*e2e.Test[adminv2.PartitionServiceCapacityResponse, adminv2.PartitionCapacity]{ + { + Name: "capacity", + CmdArgs: []string{"admin", "partition", "capacity"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.PartitionServiceCapacityRequest{}, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&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, + }, + }, + }, + }, + }) + }, + }, + }, + }), + 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 + `), + WantWideTable: new(` + PARTITION SIZE ALLOCATED FREE UNAVAILABLE RESERVATIONS | TOTAL | FAULTY PHONED HOME WAITING OTHER + partition-1 size-1 1 3 0 2 (1/3 used) | 5 | 2 0 0 0 + Total 1 3 0 2 | 5 | 2 0 0 0 + `), + Template: new("{{ .partition }} {{ (index .machine_size_capacities 0).size }}"), + WantTemplate: new(` +partition-1 size-1 + `), + WantMarkdown: 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", + CmdArgs: []string{"admin", "partition", "capacity", "--id", "partition-1", "--size", "size-1", "--project", "project-123"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.PartitionServiceCapacityRequest{ + Id: new("partition-1"), + Size: new("size-1"), + Project: new("project-123"), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&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, + }, + }, + }, + }, + }) + }, + }, + }, + }), + 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) + } +} diff --git a/tests/e2e/admin/switch_test.go b/tests/e2e/admin/switch_test.go index 10c13b0..9f734b2 100644 --- a/tests/e2e/admin/switch_test.go +++ b/tests/e2e/admin/switch_test.go @@ -167,11 +167,11 @@ func Test_AdminSwitchCmd_Update(t *testing.T) { }, }), WantTable: new(` - ID PARTITION RACK OS STATUS LAST SYNC + ID PARTITION RACK OS STATUS LAST SYNC leaf02 fra-equ01 rack-1 🦔 ● `), WantWideTable: new(` - ID PARTITION RACK OS METALCORE IP MODE LAST SYNC SYNC DURATION LAST ERROR + ID PARTITION RACK OS METALCORE IP MODE LAST SYNC SYNC DURATION LAST ERROR leaf02 fra-equ01 rack-1 SONiC (4.2.0) v0.9.1 (abc1234) 10.0.0.2 operational 200ms `), Template: new("{{ .id }} {{ .os.metal_core_version }}"), @@ -209,8 +209,8 @@ func Test_AdminSwitchCmd_ConnectedMachines(t *testing.T) { }, }), WantTable: new(` - ID NIC NAME IDENTIFIER PARTITION RACK SIZE PRODUCT SERIAL CHASSIS SERIAL - leaf01 fra-equ01 rack-1 + ID NIC NAME IDENTIFIER PARTITION RACK SIZE PRODUCT SERIAL CHASSIS SERIAL + leaf01 fra-equ01 rack-1 └─╴id1 Ethernet0 (up) oid:0x1000000000001 fra-equ01 rack-1 m1-small ps-1 cs-1 `), }, @@ -244,8 +244,8 @@ func Test_AdminSwitchCmd_Detail(t *testing.T) { }, }), WantTable: new(` - PARTITION RACK SWITCH PORT MACHINE VNI - FILTER CIDR - FILTER - fra-equ01 rack-1 leaf01 Ethernet0 id1 10001 10.0.0.0/24 + PARTITION RACK SWITCH PORT MACHINE VNI - FILTER CIDR - FILTER + fra-equ01 rack-1 leaf01 Ethernet0 id1 10001 10.0.0.0/24 10002 192.168.100.0/24 `), Template: new("{{ .id }} {{ .partition }} {{ .rack }}"), diff --git a/tests/e2e/testresources/partitions.go b/tests/e2e/testresources/partitions.go new file mode 100644 index 0000000..600baff --- /dev/null +++ b/tests/e2e/testresources/partitions.go @@ -0,0 +1,48 @@ +package testresources + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" +) + +var ( + Partition1 = func() *apiv2.Partition { + return &apiv2.Partition{ + Id: "partition-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", + }, + }, + }, + } + } + Partition2 = func() *apiv2.Partition { + return &apiv2.Partition{ + Id: "partition-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, + }, + }, + } + } +)