From c1c04cfc8e4f823ab15fea158b9ad89599974a54 Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 17 Aug 2026 10:15:54 +0200 Subject: [PATCH 1/3] Prune components --- cmd/admin/v2/component.go | 59 +++++++++++++++++++++++++++++++++---- cmd/completion/component.go | 16 ++++++++++ 2 files changed, 69 insertions(+), 6 deletions(-) create mode 100644 cmd/completion/component.go diff --git a/cmd/admin/v2/component.go b/cmd/admin/v2/component.go index 002458e..0dfacca 100644 --- a/cmd/admin/v2/component.go +++ b/cmd/admin/v2/component.go @@ -1,9 +1,10 @@ package v2 import ( + "context" "fmt" + "time" - "github.com/metal-stack/api/go/enum" 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" @@ -36,10 +37,23 @@ func newComponentCmd(c *config.Config) *cobra.Command { cmd.Flags().String("uuid", "", "lists only component with this uuid") cmd.Flags().String("type", "", "lists only component of this type") cmd.Flags().String("identifier", "", "lists only component with this identifier") + genericcli.Must(cmd.RegisterFlagCompletionFunc("type", c.Completion.ComponentTypes)) }, } + pruneCmd := &cobra.Command{ + Use: "prune", + Short: "prune outdated component entries", + Long: "prune outdated component entries to shorten the component list", + RunE: func(cmd *cobra.Command, args []string) error { + return w.prune(cmd.Context()) + }, + ValidArgsFunction: c.Completion.Firewall, + } + pruneCmd.Flags().Duration("max-age", 12*time.Hour, "prune all components which are older than max-age and not active anymore") + pruneCmd.Flags().String("type", "", "prune only component of this type") + genericcli.Must(pruneCmd.RegisterFlagCompletionFunc("type", c.Completion.ComponentTypes)) - return genericcli.NewCmds(cmdsConfig) + return genericcli.NewCmds(cmdsConfig, pruneCmd) } func (c *component) Get(id string) (*apiv2.Component, error) { @@ -83,11 +97,12 @@ func (c *component) List() ([]*apiv2.Component, error) { } if viper.IsSet("type") { - t, err := enum.GetEnum[apiv2.ComponentType](viper.GetString("type")) - if err != nil { - return nil, fmt.Errorf("unable to get component type of string %q %w", viper.GetString("type"), err) + componentString := viper.GetString("type") + ct, ok := apiv2.ComponentType_value[componentString] + if !ok { + return nil, fmt.Errorf("unknown component type: %s", componentString) } - query.Type = &t + query.Type = new(apiv2.ComponentType(ct)) } req := &adminv2.ComponentServiceListRequest{Query: query} @@ -99,6 +114,38 @@ func (c *component) List() ([]*apiv2.Component, error) { return resp.Components, nil } + +func (c *component) prune(ctx context.Context) error { + components, err := c.List() + if err != nil { + return err + } + + maxAge := viper.GetDuration("max-age") + + for _, ct := range components { + if time.Since(ct.ReportedAt.AsTime()) < maxAge { + if time.Until(ct.Token.Expires.AsTime()) > 0 { + continue + } + } + + req := &adminv2.ComponentServiceDeleteRequest{ + Uuid: ct.Uuid, + } + resp, err := c.c.Client.Adminv2().Component().Delete(ctx, req) + if err != nil { + return fmt.Errorf("failed to prune components: %w", err) + } + _, err = fmt.Fprintf(c.c.Out, "deleted component %s:%s\n", resp.Component.Uuid, resp.Component.Identifier) + if err != nil { + return err + } + } + + return nil +} + func (c *component) Convert(r *apiv2.Component) (string, any, any, error) { panic("unimplemented") } diff --git a/cmd/completion/component.go b/cmd/completion/component.go new file mode 100644 index 0000000..9c6e686 --- /dev/null +++ b/cmd/completion/component.go @@ -0,0 +1,16 @@ +package completion + +import ( + apiv2 "github.com/metal-stack/api/go/metalstack/api/v2" + "github.com/spf13/cobra" +) + +func (c *Completion) ComponentTypes(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + var names []string + + for _, name := range apiv2.ComponentType_name { + names = append(names, name) + } + + return names, cobra.ShellCompDirectiveNoFileComp +} From 4b8dac8cd02204fb82fdb973beeef99d69ce179d Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 17 Aug 2026 10:36:59 +0200 Subject: [PATCH 2/3] Review comment --- cmd/admin/v2/component.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/cmd/admin/v2/component.go b/cmd/admin/v2/component.go index 0dfacca..e731c59 100644 --- a/cmd/admin/v2/component.go +++ b/cmd/admin/v2/component.go @@ -121,7 +121,10 @@ func (c *component) prune(ctx context.Context) error { return err } - maxAge := viper.GetDuration("max-age") + var ( + maxAge = viper.GetDuration("max-age") + prunedComponents []*apiv2.Component + ) for _, ct := range components { if time.Since(ct.ReportedAt.AsTime()) < maxAge { @@ -137,13 +140,10 @@ func (c *component) prune(ctx context.Context) error { if err != nil { return fmt.Errorf("failed to prune components: %w", err) } - _, err = fmt.Fprintf(c.c.Out, "deleted component %s:%s\n", resp.Component.Uuid, resp.Component.Identifier) - if err != nil { - return err - } + prunedComponents = append(prunedComponents, resp.Component) } - return nil + return c.c.ListPrinter.Print(prunedComponents) } func (c *component) Convert(r *apiv2.Component) (string, any, any, error) { From 5c83c78864039798755bb167e6f746a179c28d5b Mon Sep 17 00:00:00 2001 From: Stefan Majer Date: Mon, 17 Aug 2026 11:00:45 +0200 Subject: [PATCH 3/3] Add test --- tests/e2e/admin/component_test.go | 57 ++++++++++++++++++++++++++++ tests/e2e/testresources/component.go | 17 +++++++++ 2 files changed, 74 insertions(+) diff --git a/tests/e2e/admin/component_test.go b/tests/e2e/admin/component_test.go index c0d3be7..76d90f0 100644 --- a/tests/e2e/admin/component_test.go +++ b/tests/e2e/admin/component_test.go @@ -106,6 +106,63 @@ d2b3c4e5-f6a7-8901-bcde-f12345678901 pixiecore-1 } } +func Test_AdminComponentCmd_Prune(t *testing.T) { + tests := []*e2e.Test[adminv2.ComponentServiceListResponse, apiv2.Component]{ + { + Name: "prune", + CmdArgs: []string{"admin", "component", "prune"}, + NewRootCmd: e2erootcmd.NewRootCmd(t, &e2erootcmd.TestConfig{ + ClientCalls: []client.ClientCall{ + { + WantRequest: &adminv2.ComponentServiceListRequest{ + Query: &apiv2.ComponentQuery{}, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceListResponse{ + Components: []*apiv2.Component{ + testresources.Component1(), + testresources.Component2(), + testresources.Component3(), + }, + }) + }, + }, + { + WantRequest: &adminv2.ComponentServiceDeleteRequest{ + Uuid: testresources.Component3().Uuid, + }, + WantResponse: func() connect.AnyResponse { + return connect.NewResponse(&adminv2.ComponentServiceDeleteResponse{ + Component: testresources.Component3(), + }) + }, + }, + }, + }), + WantTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + d2b3c4e5-f6a7-8901-bcde-f12345678903 metal-console metal-console-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678903 -2d + `), + WantWideTable: new(` + ID TYPE IDENTIFIER STARTED AGE VERSION TOKEN TOKEN EXPIRES IN + d2b3c4e5-f6a7-8901-bcde-f12345678903 metal-console metal-console-1 0s 0s v2.0.0 t2b3c4e5-f6a7-8901-bcde-f12345678903 -2d + `), + Template: new("{{ .uuid }} {{ .identifier }}"), + WantTemplate: new(` +d2b3c4e5-f6a7-8901-bcde-f12345678903 metal-console-1 +`), + WantMarkdown: new(` + | ID | TYPE | IDENTIFIER | STARTED | AGE | VERSION | TOKEN | TOKEN EXPIRES IN | + |--------------------------------------|---------------|-----------------|---------|-----|---------|--------------------------------------|------------------| + | d2b3c4e5-f6a7-8901-bcde-f12345678903 | metal-console | metal-console-1 | 0s | 0s | v2.0.0 | t2b3c4e5-f6a7-8901-bcde-f12345678903 | -2d | + `), + }, + } + for _, tt := range tests { + tt.TestCmd(t) + } +} + func Test_AdminComponentCmd_Delete(t *testing.T) { tests := []*e2e.Test[adminv2.ComponentServiceDeleteResponse, *apiv2.Component]{ { diff --git a/tests/e2e/testresources/component.go b/tests/e2e/testresources/component.go index 2692988..4c651f9 100644 --- a/tests/e2e/testresources/component.go +++ b/tests/e2e/testresources/component.go @@ -44,4 +44,21 @@ var ( }, } } + Component3 = func() *apiv2.Component { + return &apiv2.Component{ + Uuid: "d2b3c4e5-f6a7-8901-bcde-f12345678903", + Type: apiv2.ComponentType_COMPONENT_TYPE_METAL_CONSOLE, + Identifier: "metal-console-1", + StartedAt: timestamppb.New(e2e.TimeBubbleStartTime()), + ReportedAt: timestamppb.New(e2e.TimeBubbleStartTime()), + Interval: durationpb.New(10 * time.Second), + Version: &apiv2.Version{ + Version: "v2.0.0", + }, + Token: &apiv2.Token{ + Uuid: "t2b3c4e5-f6a7-8901-bcde-f12345678903", + Expires: timestamppb.New(e2e.TimeBubbleStartTime().Add(-48 * time.Hour)), + }, + } + } )