From 76347b20511ab8f87b2f5c140bbb526977bd6543 Mon Sep 17 00:00:00 2001 From: Gerrit Date: Wed, 1 Jul 2026 13:41:39 +0200 Subject: [PATCH] Add size API cmds. --- cmd/admin/v2/commands.go | 1 + cmd/admin/v2/image.go | 3 +- cmd/admin/v2/size.go | 143 ++++++++++++++++++++ cmd/api/v2/commands.go | 1 + cmd/api/v2/size.go | 94 ++++++++++++++ cmd/completion/size.go | 19 +++ cmd/sorters/size.go | 21 +++ cmd/tableprinters/common.go | 5 + cmd/tableprinters/size.go | 44 +++++++ docs/metalctlv2.md | 1 + docs/metalctlv2_size.md | 33 +++++ docs/metalctlv2_size_describe.md | 31 +++++ docs/metalctlv2_size_list.md | 35 +++++ tests/e2e/admin/size_test.go | 215 +++++++++++++++++++++++++++++++ tests/e2e/api/size_test.go | 110 ++++++++++++++++ tests/e2e/testresources/size.go | 71 ++++++++++ 16 files changed, 825 insertions(+), 2 deletions(-) create mode 100644 cmd/admin/v2/size.go create mode 100644 cmd/api/v2/size.go create mode 100644 cmd/completion/size.go create mode 100644 cmd/sorters/size.go create mode 100644 cmd/tableprinters/size.go create mode 100644 docs/metalctlv2_size.md create mode 100644 docs/metalctlv2_size_describe.md create mode 100644 docs/metalctlv2_size_list.md create mode 100644 tests/e2e/admin/size_test.go create mode 100644 tests/e2e/api/size_test.go create mode 100644 tests/e2e/testresources/size.go diff --git a/cmd/admin/v2/commands.go b/cmd/admin/v2/commands.go index 70667cb..ff90b69 100644 --- a/cmd/admin/v2/commands.go +++ b/cmd/admin/v2/commands.go @@ -18,6 +18,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { adminCmd.AddCommand(newComponentCmd(c)) adminCmd.AddCommand(newImageCmd(c)) adminCmd.AddCommand(newProjectCmd(c)) + adminCmd.AddCommand(newSizeCmd(c)) adminCmd.AddCommand(newSwitchCmd(c)) adminCmd.AddCommand(newTaskCmd(c)) adminCmd.AddCommand(newTenantCmd(c)) diff --git a/cmd/admin/v2/image.go b/cmd/admin/v2/image.go index 4b5019d..a554238 100644 --- a/cmd/admin/v2/image.go +++ b/cmd/admin/v2/image.go @@ -128,10 +128,9 @@ func (c *image) Delete(id string) (*apiv2.Image, error) { } func (c *image) List() ([]*apiv2.Image, error) { panic("unimplemented") - } -func (c *image) Convert(r *apiv2.Image) (string, *adminv2.ImageServiceCreateRequest, *adminv2.ImageServiceUpdateRequest, error) { +func (c *image) Convert(r *apiv2.Image) (string, *adminv2.ImageServiceCreateRequest, *adminv2.ImageServiceUpdateRequest, error) { return r.Id, &adminv2.ImageServiceCreateRequest{ Image: &apiv2.Image{ Id: r.Id, diff --git a/cmd/admin/v2/size.go b/cmd/admin/v2/size.go new file mode 100644 index 0000000..b51a91d --- /dev/null +++ b/cmd/admin/v2/size.go @@ -0,0 +1,143 @@ +package v2 + +import ( + "fmt" + + 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 size struct { + c *config.Config +} + +func newSizeCmd(c *config.Config) *cobra.Command { + w := &size{ + c: c, + } + + cmdsConfig := &genericcli.CmdsConfig[*adminv2.SizeServiceCreateRequest, *adminv2.SizeServiceUpdateRequest, *apiv2.Size]{ + BinaryName: config.BinaryName, + GenericCLI: genericcli.NewGenericCLI(w).WithFS(c.Fs), + Singular: "size", + Plural: "sizes", + Description: "manage sizes which defines the cpu, gpu, memory and storage properties of machines", + Sorter: sorters.SizeSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ValidArgsFn: c.Completion.SizeListCompletion, + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("id", "", "", "size id to filter for") + cmd.Flags().StringP("name", "", "", "size name to filter for") + cmd.Flags().StringP("description", "", "", "size description to filter for") + }, + UpdateCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("id", "", "", "size id to update") + cmd.Flags().StringP("name", "", "", "size name to update") + cmd.Flags().StringP("description", "", "", "size description to update") + }, + CreateCmdMutateFn: func(cmd *cobra.Command) { + + }, + // TODO: create from CLI might be nice to have? + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *size) Get(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Size().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) Create(rq *adminv2.SizeServiceCreateRequest) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Size().Create(ctx, rq) + if err != nil { + return nil, fmt.Errorf("failed to create size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) Delete(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Size().Delete(ctx, &adminv2.SizeServiceDeleteRequest{Id: id}) + if err != nil { + return nil, fmt.Errorf("failed to delete size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) List() ([]*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceListRequest{Query: &apiv2.SizeQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + }} + + resp, err := c.c.Client.Apiv2().Size().List(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get sizes: %w", err) + } + + return resp.Sizes, nil +} + +func (c *size) Update(rq *adminv2.SizeServiceUpdateRequest) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + resp, err := c.c.Client.Adminv2().Size().Update(ctx, rq) + if err != nil { + return nil, fmt.Errorf("failed to update size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) Convert(r *apiv2.Size) (string, *adminv2.SizeServiceCreateRequest, *adminv2.SizeServiceUpdateRequest, error) { + return r.Id, &adminv2.SizeServiceCreateRequest{ + Size: &apiv2.Size{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Meta: r.Meta, + Constraints: r.Constraints, + }, + }, &adminv2.SizeServiceUpdateRequest{ + Id: r.Id, + Name: r.Name, + Description: r.Description, + Constraints: r.Constraints, + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, + UpdatedAt: r.Meta.UpdatedAt, + }, + Labels: &apiv2.UpdateLabels{}, + }, nil +} diff --git a/cmd/api/v2/commands.go b/cmd/api/v2/commands.go index d79e964..3f4278a 100644 --- a/cmd/api/v2/commands.go +++ b/cmd/api/v2/commands.go @@ -12,6 +12,7 @@ func AddCmds(cmd *cobra.Command, c *config.Config) { cmd.AddCommand(newIPCmd(c)) cmd.AddCommand(newMethodsCmd(c)) cmd.AddCommand(newProjectCmd(c)) + cmd.AddCommand(newSizeCmd(c)) cmd.AddCommand(newTenantCmd(c)) cmd.AddCommand(newTokenCmd(c)) cmd.AddCommand(newUserCmd(c)) diff --git a/cmd/api/v2/size.go b/cmd/api/v2/size.go new file mode 100644 index 0000000..be7e7c8 --- /dev/null +++ b/cmd/api/v2/size.go @@ -0,0 +1,94 @@ +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/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 size struct { + c *config.Config +} + +func newSizeCmd(c *config.Config) *cobra.Command { + w := &size{ + c: c, + } + + gcli := genericcli.NewGenericCLI(w).WithFS(c.Fs) + + cmdsConfig := &genericcli.CmdsConfig[any, any, *apiv2.Size]{ + BinaryName: config.BinaryName, + GenericCLI: gcli, + Singular: "size", + Plural: "sizes", + Description: "manage sizes which defines the cpu, gpu, memory and storage properties of machines", + Sorter: sorters.SizeSorter(), + DescribePrinter: func() printers.Printer { return c.DescribePrinter }, + ListPrinter: func() printers.Printer { return c.ListPrinter }, + ValidArgsFn: c.Completion.SizeListCompletion, + OnlyCmds: genericcli.OnlyCmds(genericcli.DescribeCmd, genericcli.ListCmd), + ListCmdMutateFn: func(cmd *cobra.Command) { + cmd.Flags().StringP("id", "", "", "size id to filter for") + cmd.Flags().StringP("name", "", "", "size name to filter for") + cmd.Flags().StringP("description", "", "", "size description to filter for") + }, + } + + return genericcli.NewCmds(cmdsConfig) +} + +func (c *size) Get(id string) (*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceGetRequest{Id: id} + + resp, err := c.c.Client.Apiv2().Size().Get(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get size: %w", err) + } + + return resp.Size, nil +} + +func (c *size) List() ([]*apiv2.Size, error) { + ctx, cancel := c.c.NewRequestContext() + defer cancel() + + req := &apiv2.SizeServiceListRequest{Query: &apiv2.SizeQuery{ + Id: pointer.PointerOrNil(viper.GetString("id")), + Name: pointer.PointerOrNil(viper.GetString("name")), + Description: pointer.PointerOrNil(viper.GetString("description")), + }} + + resp, err := c.c.Client.Apiv2().Size().List(ctx, req) + if err != nil { + return nil, fmt.Errorf("failed to get sizes: %w", err) + } + + return resp.Sizes, nil +} + +func (c *size) Create(rq any) (*apiv2.Size, error) { + panic("unimplemented") +} + +func (c *size) Delete(id string) (*apiv2.Size, error) { + panic("unimplemented") +} + +func (t *size) Convert(r *apiv2.Size) (string, any, any, error) { + panic("unimplemented") +} + +func (t *size) Update(rq any) (*apiv2.Size, error) { + panic("unimplemented") +} 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/sorters/size.go b/cmd/sorters/size.go new file mode 100644 index 0000000..025570f --- /dev/null +++ b/cmd/sorters/size.go @@ -0,0 +1,21 @@ +package sorters + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/multisort" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func SizeSorter() *multisort.Sorter[*apiv2.Size] { + return multisort.New(multisort.FieldMap[*apiv2.Size]{ + "id": func(a, b *apiv2.Size, descending bool) multisort.CompareResult { + return multisort.Compare(a.Id, b.Id, descending) + }, + "name": func(a, b *apiv2.Size, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Name), pointer.SafeDeref(b.Name), descending) + }, + "description": func(a, b *apiv2.Size, descending bool) multisort.CompareResult { + return multisort.Compare(pointer.SafeDeref(a.Description), pointer.SafeDeref(b.Description), descending) + }, + }, multisort.Keys{{ID: "id"}}) +} diff --git a/cmd/tableprinters/common.go b/cmd/tableprinters/common.go index 933e6f9..ead65ec 100644 --- a/cmd/tableprinters/common.go +++ b/cmd/tableprinters/common.go @@ -113,6 +113,11 @@ func (t *TablePrinter) ToHeaderAndRows(data any, wide bool) ([]string, [][]strin case []*apiv2.Health: return t.HealthTable(d, wide) + case *apiv2.Size: + return t.SizeTable(pointer.WrapInSlice(d), wide) + case []*apiv2.Size: + return t.SizeTable(d, wide) + case []*apiv2.Switch: return t.SwitchTable(d, wide) case []SwitchDetail: diff --git a/cmd/tableprinters/size.go b/cmd/tableprinters/size.go new file mode 100644 index 0000000..4a7894a --- /dev/null +++ b/cmd/tableprinters/size.go @@ -0,0 +1,44 @@ +package tableprinters + +import ( + "fmt" + + "github.com/dustin/go-humanize" + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/metal-stack/metal-lib/pkg/pointer" +) + +func (t *TablePrinter) SizeTable(data []*apiv2.Size, wide bool) ([]string, [][]string, error) { + var ( + rows [][]string + header = []string{"ID", "Name", "Description", "CPU Range", "Memory Range", "Storage Range", "GPU Range"} + ) + + for _, size := range data { + var ( + cpu string + memory string + storage string + gpu string + ) + + for _, c := range size.Constraints { + switch c.Type { + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_CORES: + cpu = fmt.Sprintf("%d - %d", c.Min, c.Max) + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_MEMORY: + memory = fmt.Sprintf("%s - %s", humanize.Bytes(uint64(c.Min)), humanize.Bytes(uint64(c.Max))) //nolint:gosec + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_STORAGE: + storage = fmt.Sprintf("%s - %s", humanize.Bytes(uint64(c.Min)), humanize.Bytes(uint64(c.Max))) //nolint:gosec + case apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_GPU: + gpu = fmt.Sprintf("%s: %d - %d", pointer.SafeDeref(c.Identifier), c.Min, c.Max) + } + } + + rows = append(rows, []string{size.Id, pointer.SafeDeref(size.Name), pointer.SafeDeref(size.Description), cpu, memory, storage, gpu}) + } + + t.t.DisableAutoWrap(false) + + return header, rows, nil +} diff --git a/docs/metalctlv2.md b/docs/metalctlv2.md index 1187792..df68a11 100644 --- a/docs/metalctlv2.md +++ b/docs/metalctlv2.md @@ -29,6 +29,7 @@ cli for managing entities in metal-stack * [metalctlv2 logout](metalctlv2_logout.md) - logout * [metalctlv2 markdown](metalctlv2_markdown.md) - create markdown documentation * [metalctlv2 project](metalctlv2_project.md) - manage project entities +* [metalctlv2 size](metalctlv2_size.md) - manage size entities * [metalctlv2 tenant](metalctlv2_tenant.md) - manage tenant entities * [metalctlv2 token](metalctlv2_token.md) - manage token entities * [metalctlv2 user](metalctlv2_user.md) - manage user entities diff --git a/docs/metalctlv2_size.md b/docs/metalctlv2_size.md new file mode 100644 index 0000000..02880f6 --- /dev/null +++ b/docs/metalctlv2_size.md @@ -0,0 +1,33 @@ +## metalctlv2 size + +manage size entities + +### Synopsis + +manage sizes which defines the cpu, gpu, memory and storage properties of machines + +### Options + +``` + -h, --help help for size +``` + +### 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 size describe](metalctlv2_size_describe.md) - describes the size +* [metalctlv2 size list](metalctlv2_size_list.md) - list all sizes + diff --git a/docs/metalctlv2_size_describe.md b/docs/metalctlv2_size_describe.md new file mode 100644 index 0000000..fe0978d --- /dev/null +++ b/docs/metalctlv2_size_describe.md @@ -0,0 +1,31 @@ +## metalctlv2 size describe + +describes the size + +``` +metalctlv2 size 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 size](metalctlv2_size.md) - manage size entities + diff --git a/docs/metalctlv2_size_list.md b/docs/metalctlv2_size_list.md new file mode 100644 index 0000000..184b636 --- /dev/null +++ b/docs/metalctlv2_size_list.md @@ -0,0 +1,35 @@ +## metalctlv2 size list + +list all sizes + +``` +metalctlv2 size list [flags] +``` + +### Options + +``` + --description string size description to filter for + -h, --help help for list + --id string size id to filter for + --name string size name to filter for + --sort-by strings sort by (comma separated) column(s), sort direction can be changed by appending :asc or :desc behind the column identifier. possible values: description|id|name +``` + +### 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 size](metalctlv2_size.md) - manage size entities + diff --git a/tests/e2e/admin/size_test.go b/tests/e2e/admin/size_test.go new file mode 100644 index 0000000..3b26c51 --- /dev/null +++ b/tests/e2e/admin/size_test.go @@ -0,0 +1,215 @@ +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" + "github.com/spf13/afero" + "github.com/stretchr/testify/require" +) + +func Test_AdminSizeCmd_Delete(t *testing.T) { + tests := []*e2e.Test[adminv2.SizeServiceDeleteResponse, *apiv2.Size]{ + { + Name: "delete", + CmdArgs: []string{"admin", "size", "delete", testresources.Size1().Id}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.SizeServiceDeleteRequest{ + Id: testresources.Size1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SizeServiceDeleteResponse{ + Size: testresources.Size1(), + }) + }, + }, + }, + }), + WantObject: testresources.Size1(), + WantProtoObject: testresources.Size1(), + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` + v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|-----------| + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminSizeCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.SizeServiceListResponse, []*apiv2.Size]{ + { + Name: "list", + CmdArgs: []string{"admin", "size", "list"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.SizeServiceListRequest{ + Query: &apiv2.SizeQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.SizeServiceListResponse{ + Sizes: []*apiv2.Size{ + testresources.Size2(), + testresources.Size1(), + }, + }) + }, + }, + }, + }), + WantObject: []*apiv2.Size{ + testresources.Size2(), + testresources.Size1(), + }, + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + g1-medium-x86 g1-medium-x86 A medium sized GPU server 32 - 32 275 GB - 275 GB 1.8 TB - 1.8 TB AD102GL [RTX 6000 Ada Generation]: 1 - 1 + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + g1-medium-x86 g1-medium-x86 A medium sized GPU server 32 - 32 275 GB - 275 GB 1.8 TB - 1.8 TB AD102GL [RTX 6000 Ada Generation]: 1 - 1 + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` +g1-medium-x86 g1-medium-x86 +v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|------------------------------------------| + | g1-medium-x86 | g1-medium-x86 | A medium sized GPU server | 32 - 32 | 275 GB - 275 GB | 1.8 TB - 1.8 TB | AD102GL [RTX 6000 Ada Generation]: 1 - 1 | + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminSizeCmd_Create(t *testing.T) { + tests := []*e2e.Test[adminv2.SizeServiceCreateResponse, *apiv2.Size]{ + { + Name: "create from file", + CmdArgs: append([]string{"admin", "size", "create"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Size1()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.SizeServiceCreateRequest{ + Size: testresources.Size1(), + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SizeServiceCreateResponse{ + Size: testresources.Size1(), + }) + }, + }, + }, + }), + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` + v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|-----------| + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_AdminSizeCmd_Update(t *testing.T) { + tests := []*e2e.Test[adminv2.SizeServiceUpdateResponse, *apiv2.Size]{ + { + Name: "update from file", + CmdArgs: append([]string{"admin", "size", "update"}, e2e.AppendFromFileCommonArgs()...), + NewRootCmd: e2erootcmd.NewRootCmd(t, + &e2erootcmd.TestConfig{ + FsMocks: func(fs *afero.Afero) { + require.NoError(t, fs.WriteFile(e2e.InputFilePath, e2e.MustMarshal(t, testresources.Size1()), 0755)) + }, + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.SizeServiceUpdateRequest{ + Id: testresources.Size1().Id, + Name: testresources.Size1().Name, + Description: testresources.Size1().Description, + UpdateMeta: &apiv2.UpdateMeta{ + LockingStrategy: apiv2.OptimisticLockingStrategy_OPTIMISTIC_LOCKING_STRATEGY_CLIENT, + }, + Labels: &apiv2.UpdateLabels{}, + Constraints: testresources.Size1().Constraints, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.SizeServiceUpdateResponse{ + Size: testresources.Size1(), + }) + }, + }, + }, + }), + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` + v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|-----------| + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/api/size_test.go b/tests/e2e/api/size_test.go new file mode 100644 index 0000000..70ca9ee --- /dev/null +++ b/tests/e2e/api/size_test.go @@ -0,0 +1,110 @@ +package api_e2e + +import ( + "testing" + + "connectrpc.com/connect" + "github.com/metal-stack/api/go/client" + 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_SizeCmd_List(t *testing.T) { + tests := []*e2e.Test[apiv2.SizeServiceListResponse, []*apiv2.Size]{ + { + Name: "list", + CmdArgs: []string{"size", "list"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.SizeServiceListRequest{ + Query: &apiv2.SizeQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.SizeServiceListResponse{ + Sizes: []*apiv2.Size{ + testresources.Size2(), + testresources.Size1(), + }, + }) + }, + }, + }, + }), + WantObject: []*apiv2.Size{ + testresources.Size2(), + testresources.Size1(), + }, + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + g1-medium-x86 g1-medium-x86 A medium sized GPU server 32 - 32 275 GB - 275 GB 1.8 TB - 1.8 TB AD102GL [RTX 6000 Ada Generation]: 1 - 1 + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + g1-medium-x86 g1-medium-x86 A medium sized GPU server 32 - 32 275 GB - 275 GB 1.8 TB - 1.8 TB AD102GL [RTX 6000 Ada Generation]: 1 - 1 + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` +g1-medium-x86 g1-medium-x86 +v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|------------------------------------------| + | g1-medium-x86 | g1-medium-x86 | A medium sized GPU server | 32 - 32 | 275 GB - 275 GB | 1.8 TB - 1.8 TB | AD102GL [RTX 6000 Ada Generation]: 1 - 1 | + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + +func Test_SizeCmd_Describe(t *testing.T) { + tests := []*e2e.Test[apiv2.SizeServiceGetResponse, *apiv2.Size]{ + { + Name: "get", + CmdArgs: []string{"size", "describe", testresources.Size1().Id}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &apiv2.SizeServiceGetRequest{ + Id: testresources.Size1().Id, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&apiv2.SizeServiceGetResponse{ + Size: testresources.Size1(), + }) + }, + }, + }, + }), + WantObject: testresources.Size1(), + WantTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + WantWideTable: new(` + ID NAME DESCRIPTION CPU RANGE MEMORY RANGE STORAGE RANGE GPU RANGE + v1-medium-x86 v1-medium-x86 Virtual size for mini-lab 4 - 4 500 MB - 4.0 GB 1.0 GB - 100 GB + `), + Template: new("{{ .id }} {{ .name }}"), + WantTemplate: new(` + v1-medium-x86 v1-medium-x86 + `), + WantMarkdown: new(` + | ID | NAME | DESCRIPTION | CPU RANGE | MEMORY RANGE | STORAGE RANGE | GPU RANGE | + |---------------|---------------|---------------------------|-----------|-----------------|-----------------|-----------| + | v1-medium-x86 | v1-medium-x86 | Virtual size for mini-lab | 4 - 4 | 500 MB - 4.0 GB | 1.0 GB - 100 GB | | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} diff --git a/tests/e2e/testresources/size.go b/tests/e2e/testresources/size.go new file mode 100644 index 0000000..678fb94 --- /dev/null +++ b/tests/e2e/testresources/size.go @@ -0,0 +1,71 @@ +package testresources + +import apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + +var ( + Size1 = func() *apiv2.Size { + return &apiv2.Size{ + Id: "v1-medium-x86", + Name: new("v1-medium-x86"), + Description: new("Virtual size for mini-lab"), + Meta: &apiv2.Meta{ + Labels: &apiv2.Labels{ + Labels: map[string]string{ + "a": "b", + }, + }, + }, + Constraints: []*apiv2.SizeConstraint{ + { + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_CORES, + Min: 4, + Max: 4, + }, + { + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_MEMORY, + Min: 500000000, + Max: 4000000000, + }, + { + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_STORAGE, + Min: 1000000000, + Max: 100000000000, + }, + }, + } + } + Size2 = func() *apiv2.Size { + return &apiv2.Size{ + Id: "g1-medium-x86", + Name: new("g1-medium-x86"), + Description: new("A medium sized GPU server"), + Meta: &apiv2.Meta{}, + Constraints: []*apiv2.SizeConstraint{ + { + Identifier: new("Intel(R) Xeon(R) Gold 6426Y"), + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_CORES, + Min: 32, + Max: 32, + }, + { + + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_MEMORY, + Min: 274877906944, + Max: 274877906944, + }, + { + Identifier: new("/dev/*"), + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_STORAGE, + Min: 1840378724352, + Max: 1840378724352, + }, + { + Identifier: new("AD102GL [RTX 6000 Ada Generation]"), + Type: apiv2.SizeConstraintType_SIZE_CONSTRAINT_TYPE_GPU, + Min: 1, + Max: 1, + }, + }, + } + } +)