diff --git a/rover-ctl/pkg/commands/get-info/cmd.go b/rover-ctl/pkg/commands/get-info/cmd.go index c900b10a..34d30ae4 100644 --- a/rover-ctl/pkg/commands/get-info/cmd.go +++ b/rover-ctl/pkg/commands/get-info/cmd.go @@ -31,14 +31,14 @@ func NewCommand() *cobra.Command { baseCmd := base.NewFileCommand( "get-info", "Get information about a resource", - "Get detailed information about a resource using its metadata", + `Get detailed information about a specific resource by name or multiple resources from the server. +If no name or file is provided, information about all resources of the specified type will be retrieved.`, ) cmd := &Command{ FileCommand: baseCmd, } cmd.Cmd.Flags().StringVarP(&cmd.Name, "name", "n", "", "Name of the resource to get information about") - cmd.Cmd.MarkFlagsOneRequired("name", "file") cmd.Cmd.MarkFlagsMutuallyExclusive("name", "file") cmd.Cmd.Flags().BoolVarP(&cmd.Shallow, "shallow", "s", false, "Get only basic information without details") @@ -75,10 +75,42 @@ func (c *Command) Run(cmd *cobra.Command, args []string) error { } } + if c.Name == "" && c.FilePath == "" { + return c.getInfoMany() + } + c.Logger().V(1).Info("Completed get-info command") return nil } +func (c *Command) getInfoMany() error { + roverHandler, err := handlers.GetHandler(kind, apiVersion) + if err != nil { + return errors.Wrap(err, "failed to get rover handler") + } + + c.Logger().V(1).Info("Getting info for multiple resources") + + infoList, err := roverHandler.InfoMany(c.Cmd.Context(), nil) + if err != nil { + return c.HandleError(err, "get info for Rovers") + } + + prettyString, err := util.FormatOutput(infoList, viper.GetString("output.format")) + if err != nil { + return errors.Wrap(err, "failed to format output") + } + + _, err = c.Cmd.OutOrStdout().Write([]byte(prettyString)) + if err != nil { + return errors.Wrap(err, "failed to write output") + } + + c.Logger().V(1).Info("Successfully retrieved info for multiple resources") + + return nil +} + func (c *Command) getInfoFor(name string) error { roverHandler, err := handlers.GetHandler(kind, apiVersion) if err != nil { diff --git a/rover-ctl/pkg/handlers/common/base_handler.go b/rover-ctl/pkg/handlers/common/base_handler.go index 2a367b31..69422224 100644 --- a/rover-ctl/pkg/handlers/common/base_handler.go +++ b/rover-ctl/pkg/handlers/common/base_handler.go @@ -180,7 +180,6 @@ func (h *BaseHandler) Get(ctx context.Context, name string) (any, error) { token := h.Setup(ctx) url := h.GetRequestUrl(token.Group, token.Team, name) - // Send the request (no obj, so no hooks will be executed) resp, err := h.SendRequest(ctx, nil, http.MethodGet, url) if err != nil { return nil, err @@ -233,7 +232,6 @@ func (h *BaseHandler) ListWithCursor(ctx context.Context, cursor string) (*ListR url += "?cursor=" + cursor } - // Send the request (no obj, so no hooks will be executed) resp, err := h.SendRequest(ctx, nil, http.MethodGet, url) if err != nil { return nil, err @@ -259,7 +257,6 @@ func (h *BaseHandler) Status(ctx context.Context, name string) (types.ObjectStat token := h.Setup(ctx) url := h.GetRequestUrl(token.Group, token.Team, name, "status") - // Send the request (no obj, so no hooks will be executed) resp, err := h.SendRequest(ctx, nil, http.MethodGet, url) if err != nil { return nil, err @@ -296,7 +293,32 @@ func (h *BaseHandler) Info(ctx context.Context, name string) (any, error) { token := h.Setup(ctx) url := h.GetRequestUrl(token.Group, token.Team, name, "info") - // Send the request (no obj, so no hooks will be executed) + return h.execInfoRequest(ctx, url) +} + +func (h *BaseHandler) InfoMany(ctx context.Context, names []string) (any, error) { + if !h.SupportsInfo { + return nil, errors.Errorf("info operation is not supported for %s", h.Resource) + } + token := h.Setup(ctx) + reqUrl := h.GetRequestUrl(token.Group, token.Team, "", "info") + queryParams := "" + for _, name := range names { + if queryParams == "" { + queryParams = "?name=" + name + } else { + queryParams += "&name=" + name + } + } + reqUrl += queryParams + + return h.execInfoRequest(ctx, reqUrl) +} + +func (h *BaseHandler) execInfoRequest(ctx context.Context, url string) (any, error) { + + h.logger.V(1).Info("Executing info request", "url", url) + resp, err := h.SendRequest(ctx, nil, http.MethodGet, url) if err != nil { return nil, err @@ -339,11 +361,8 @@ func (h *BaseHandler) RunHooks(stage HandlerHookStage, ctx context.Context, obj // SendRequest handles common request operations including running hooks func (h *BaseHandler) SendRequest(ctx context.Context, obj types.Object, method, url string) (*http.Response, error) { - // Run pre-request hooks if object is provided - if obj != nil { - if err := h.RunHooks(PreRequestHook, ctx, obj); err != nil { - return nil, err - } + if err := h.RunHooks(PreRequestHook, ctx, obj); err != nil { + return nil, err } var body io.ReadWriter @@ -382,11 +401,8 @@ func (h *BaseHandler) SendRequest(ctx context.Context, obj types.Object, method, h.logger.V(1).Info("Received response", "status", resp.Status) - // Run post-request hooks if object is provided - if obj != nil { - if err := h.RunHooks(PostRequestHook, ctx, obj); err != nil { - return nil, err - } + if err := h.RunHooks(PostRequestHook, ctx, obj); err != nil { + return nil, err } return resp, nil diff --git a/rover-ctl/pkg/handlers/interface.go b/rover-ctl/pkg/handlers/interface.go index 6d1f5586..b4f32472 100644 --- a/rover-ctl/pkg/handlers/interface.go +++ b/rover-ctl/pkg/handlers/interface.go @@ -35,7 +35,12 @@ type ResourceHandler interface { // List retrieves all resources of this type from the server List(ctx context.Context) ([]any, error) + // Info retrieves detailed information about a resource by name from the server Info(ctx context.Context, name string) (any, error) + + // InfoMany retrieves detailed information about multiple resources by their names from the server + // If names is empty, all resources of this type should be returned + InfoMany(ctx context.Context, names []string) (any, error) } type Waiter interface { diff --git a/rover-ctl/pkg/handlers/v0/apispec.go b/rover-ctl/pkg/handlers/v0/apispec.go index 4110cb30..defe85e5 100644 --- a/rover-ctl/pkg/handlers/v0/apispec.go +++ b/rover-ctl/pkg/handlers/v0/apispec.go @@ -26,6 +26,9 @@ func NewApiSpecHandlerInstance() *ApiSpecHandler { } func PatchApiSpecificationRequest(ctx context.Context, obj types.Object) error { + if obj == nil { + return nil + } content := map[string]any{ "specification": obj.GetContent(), } diff --git a/rover-ctl/pkg/handlers/v0/rover.go b/rover-ctl/pkg/handlers/v0/rover.go index dc2ba70b..e0177e74 100644 --- a/rover-ctl/pkg/handlers/v0/rover.go +++ b/rover-ctl/pkg/handlers/v0/rover.go @@ -32,6 +32,10 @@ func NewRoverHandlerInstance() *RoverHandler { } func PatchRoverRequest(ctx context.Context, obj types.Object) error { + if obj == nil { + return nil + } + content := obj.GetContent() spec, ok := content["spec"].(map[string]any) if !ok { diff --git a/rover-ctl/test/mocks/mock_ResetSecretHandler.go b/rover-ctl/test/mocks/mock_ResetSecretHandler.go index f1994afa..e32bb0e2 100644 --- a/rover-ctl/test/mocks/mock_ResetSecretHandler.go +++ b/rover-ctl/test/mocks/mock_ResetSecretHandler.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package mocks @@ -239,6 +239,65 @@ func (_c *MockResetSecretHandler_Info_Call) RunAndReturn(run func(context.Contex return _c } +// InfoMany provides a mock function with given fields: ctx, names +func (_m *MockResetSecretHandler) InfoMany(ctx context.Context, names []string) (interface{}, error) { + ret := _m.Called(ctx, names) + + if len(ret) == 0 { + panic("no return value specified for InfoMany") + } + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) (interface{}, error)); ok { + return rf(ctx, names) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) interface{}); ok { + r0 = rf(ctx, names) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, names) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockResetSecretHandler_InfoMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoMany' +type MockResetSecretHandler_InfoMany_Call struct { + *mock.Call +} + +// InfoMany is a helper method to define mock.On call +// - ctx context.Context +// - names []string +func (_e *MockResetSecretHandler_Expecter) InfoMany(ctx interface{}, names interface{}) *MockResetSecretHandler_InfoMany_Call { + return &MockResetSecretHandler_InfoMany_Call{Call: _e.mock.On("InfoMany", ctx, names)} +} + +func (_c *MockResetSecretHandler_InfoMany_Call) Run(run func(ctx context.Context, names []string)) *MockResetSecretHandler_InfoMany_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string)) + }) + return _c +} + +func (_c *MockResetSecretHandler_InfoMany_Call) Return(_a0 interface{}, _a1 error) *MockResetSecretHandler_InfoMany_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockResetSecretHandler_InfoMany_Call) RunAndReturn(run func(context.Context, []string) (interface{}, error)) *MockResetSecretHandler_InfoMany_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function with given fields: ctx func (_m *MockResetSecretHandler) List(ctx context.Context) ([]interface{}, error) { ret := _m.Called(ctx) diff --git a/rover-ctl/test/mocks/mock_ResourceHandler.go b/rover-ctl/test/mocks/mock_ResourceHandler.go index 0c212dd4..db6b9141 100644 --- a/rover-ctl/test/mocks/mock_ResourceHandler.go +++ b/rover-ctl/test/mocks/mock_ResourceHandler.go @@ -2,7 +2,7 @@ // // SPDX-License-Identifier: Apache-2.0 -// Code generated by mockery v2.53.4. DO NOT EDIT. +// Code generated by mockery v2.53.5. DO NOT EDIT. package mocks @@ -239,6 +239,65 @@ func (_c *MockResourceHandler_Info_Call) RunAndReturn(run func(context.Context, return _c } +// InfoMany provides a mock function with given fields: ctx, names +func (_m *MockResourceHandler) InfoMany(ctx context.Context, names []string) (interface{}, error) { + ret := _m.Called(ctx, names) + + if len(ret) == 0 { + panic("no return value specified for InfoMany") + } + + var r0 interface{} + var r1 error + if rf, ok := ret.Get(0).(func(context.Context, []string) (interface{}, error)); ok { + return rf(ctx, names) + } + if rf, ok := ret.Get(0).(func(context.Context, []string) interface{}); ok { + r0 = rf(ctx, names) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(interface{}) + } + } + + if rf, ok := ret.Get(1).(func(context.Context, []string) error); ok { + r1 = rf(ctx, names) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// MockResourceHandler_InfoMany_Call is a *mock.Call that shadows Run/Return methods with type explicit version for method 'InfoMany' +type MockResourceHandler_InfoMany_Call struct { + *mock.Call +} + +// InfoMany is a helper method to define mock.On call +// - ctx context.Context +// - names []string +func (_e *MockResourceHandler_Expecter) InfoMany(ctx interface{}, names interface{}) *MockResourceHandler_InfoMany_Call { + return &MockResourceHandler_InfoMany_Call{Call: _e.mock.On("InfoMany", ctx, names)} +} + +func (_c *MockResourceHandler_InfoMany_Call) Run(run func(ctx context.Context, names []string)) *MockResourceHandler_InfoMany_Call { + _c.Call.Run(func(args mock.Arguments) { + run(args[0].(context.Context), args[1].([]string)) + }) + return _c +} + +func (_c *MockResourceHandler_InfoMany_Call) Return(_a0 interface{}, _a1 error) *MockResourceHandler_InfoMany_Call { + _c.Call.Return(_a0, _a1) + return _c +} + +func (_c *MockResourceHandler_InfoMany_Call) RunAndReturn(run func(context.Context, []string) (interface{}, error)) *MockResourceHandler_InfoMany_Call { + _c.Call.Return(run) + return _c +} + // List provides a mock function with given fields: ctx func (_m *MockResourceHandler) List(ctx context.Context) ([]interface{}, error) { ret := _m.Called(ctx)