From c5179e5a08b08f81d6579ad272c876b5c0b892bc Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 19:25:32 +0200 Subject: [PATCH 01/29] add kyma alpha module list stub --- internal/cmd/alpha/module/list.go | 20 ++++++++++++++++++++ internal/cmd/alpha/module/list_test.go | 14 ++++++++++++++ internal/cmd/alpha/module/module.go | 1 + 3 files changed, 35 insertions(+) create mode 100644 internal/cmd/alpha/module/list.go create mode 100644 internal/cmd/alpha/module/list_test.go diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go new file mode 100644 index 000000000..b467b8e62 --- /dev/null +++ b/internal/cmd/alpha/module/list.go @@ -0,0 +1,20 @@ +package module + +import ( + "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/kyma-project/cli.v3/internal/out" + "github.com/spf13/cobra" +) + +func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { + cmd := &cobra.Command{ + Use: "list [flags]", + Short: "Lists installed modules", + Long: `Use this command to list all installed Kyma modules.`, + Run: func(_ *cobra.Command, _ []string) { + out.Default.Msgln("functionality under construction") + }, + } + + return cmd +} diff --git a/internal/cmd/alpha/module/list_test.go b/internal/cmd/alpha/module/list_test.go new file mode 100644 index 000000000..b86b7b330 --- /dev/null +++ b/internal/cmd/alpha/module/list_test.go @@ -0,0 +1,14 @@ +package module + +import ( + "testing" + + "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/stretchr/testify/require" +) + +func TestListCmd_Exists(t *testing.T) { + cmd := NewListV2CMD(&cmdcommon.KymaConfig{}) + require.NotNil(t, cmd) + require.Equal(t, "list [flags]", cmd.Use) +} diff --git a/internal/cmd/alpha/module/module.go b/internal/cmd/alpha/module/module.go index ecb6f6f4d..3da7bdfed 100644 --- a/internal/cmd/alpha/module/module.go +++ b/internal/cmd/alpha/module/module.go @@ -15,6 +15,7 @@ func NewModuleCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd.AddCommand(NewCatalogV2CMD(kymaConfig)) cmd.AddCommand(NewPullV2CMD(kymaConfig)) + cmd.AddCommand(NewListV2CMD(kymaConfig)) return cmd } From 776b71238b02811e822cfe9f8b35bfb31c194763 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 19:38:16 +0200 Subject: [PATCH 02/29] add under construction note to list cmd help --- internal/cmd/alpha/module/list.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index b467b8e62..221ff77d0 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -10,7 +10,10 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd := &cobra.Command{ Use: "list [flags]", Short: "Lists installed modules", - Long: `Use this command to list all installed Kyma modules.`, + Long: `Use this command to list all installed Kyma modules. + +NOTE: functionality under construction + - listing installed modules: not implemented`, Run: func(_ *cobra.Command, _ []string) { out.Default.Msgln("functionality under construction") }, From dab68f2bd26f43483efa0df5fc38ba70ac27da55 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:12:24 +0200 Subject: [PATCH 03/29] add ListService with InstalledModulesRepository --- internal/modulesv2/dtos/listresult.go | 5 +++++ internal/modulesv2/fake/installedmodules.go | 16 ++++++++++++++ internal/modulesv2/list.go | 20 +++++++++++++++++ internal/modulesv2/list_test.go | 22 +++++++++++++++++++ .../modulesv2/repository/installedmodules.go | 11 ++++++++++ 5 files changed, 74 insertions(+) create mode 100644 internal/modulesv2/dtos/listresult.go create mode 100644 internal/modulesv2/fake/installedmodules.go create mode 100644 internal/modulesv2/list.go create mode 100644 internal/modulesv2/list_test.go create mode 100644 internal/modulesv2/repository/installedmodules.go diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go new file mode 100644 index 000000000..018528a09 --- /dev/null +++ b/internal/modulesv2/dtos/listresult.go @@ -0,0 +1,5 @@ +package dtos + +type ListResult struct { + Name string +} diff --git a/internal/modulesv2/fake/installedmodules.go b/internal/modulesv2/fake/installedmodules.go new file mode 100644 index 000000000..1307cf219 --- /dev/null +++ b/internal/modulesv2/fake/installedmodules.go @@ -0,0 +1,16 @@ +package fake + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" +) + +type InstalledModulesRepository struct { + ListInstalledModulesResult []kyma.ModuleStatus + ListInstalledModulesError error +} + +func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]kyma.ModuleStatus, error) { + return f.ListInstalledModulesResult, f.ListInstalledModulesError +} diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go new file mode 100644 index 000000000..627e16478 --- /dev/null +++ b/internal/modulesv2/list.go @@ -0,0 +1,20 @@ +package modulesv2 + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/modulesv2/dtos" + "github.com/kyma-project/cli.v3/internal/modulesv2/repository" +) + +type ListService struct { + installedModulesRepository repository.InstalledModulesRepository +} + +func NewListService(installedModulesRepository repository.InstalledModulesRepository) *ListService { + return &ListService{installedModulesRepository: installedModulesRepository} +} + +func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { + return []dtos.ListResult{}, nil +} diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go new file mode 100644 index 000000000..951397604 --- /dev/null +++ b/internal/modulesv2/list_test.go @@ -0,0 +1,22 @@ +package modulesv2 + +import ( + "context" + "testing" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" + modulesfake "github.com/kyma-project/cli.v3/internal/modulesv2/fake" + "github.com/stretchr/testify/require" +) + +func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.ModuleStatus{}, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + require.Empty(t, result) +} diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go new file mode 100644 index 000000000..377d46bcc --- /dev/null +++ b/internal/modulesv2/repository/installedmodules.go @@ -0,0 +1,11 @@ +package repository + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" +) + +type InstalledModulesRepository interface { + ListInstalledModules(ctx context.Context) ([]kyma.ModuleStatus, error) +} From e3f7919cf282a4c971c407d1ba1fe68cf2636d88 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:25:58 +0200 Subject: [PATCH 04/29] ListService returns core modules from InstalledModulesRepository --- internal/modulesv2/list.go | 12 +++++++++++- internal/modulesv2/list_test.go | 17 +++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 627e16478..b2852c8b7 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -16,5 +16,15 @@ func NewListService(installedModulesRepository repository.InstalledModulesReposi } func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { - return []dtos.ListResult{}, nil + installedModules, err := s.installedModulesRepository.ListInstalledModules(ctx) + if err != nil { + return nil, err + } + + results := make([]dtos.ListResult, 0, len(installedModules)) + for _, module := range installedModules { + results = append(results, dtos.ListResult{Name: module.Name}) + } + + return results, nil } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 951397604..88221f631 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -20,3 +20,20 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { require.NoError(t, err) require.Empty(t, result) } + +func TestListService_Run_ReturnsCoreModules(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.ModuleStatus{ + {Name: "api-gateway"}, + {Name: "istio"}, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, "api-gateway", result[0].Name) + require.Equal(t, "istio", result[1].Name) +} From 465586431416717986d67c437a849bf1b16f49f1 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:45:16 +0200 Subject: [PATCH 05/29] wire ListService to list command, print module names --- internal/cmd/alpha/module/list.go | 29 +++++++++++++-- internal/modulesv2/dependencies.go | 35 ++++++++++++++++--- .../modulesv2/repository/installedmodules.go | 17 +++++++++ .../repository/installedmodules_test.go | 32 +++++++++++++++++ 4 files changed, 105 insertions(+), 8 deletions(-) create mode 100644 internal/modulesv2/repository/installedmodules_test.go diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index 221ff77d0..de73ee7bd 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -1,8 +1,11 @@ package module import ( + "fmt" + + "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/kyma-project/cli.v3/internal/out" + "github.com/kyma-project/cli.v3/internal/modulesv2" "github.com/spf13/cobra" ) @@ -13,11 +16,31 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { Long: `Use this command to list all installed Kyma modules. NOTE: functionality under construction - - listing installed modules: not implemented`, + - listing installed modules: partial (names only)`, Run: func(_ *cobra.Command, _ []string) { - out.Default.Msgln("functionality under construction") + clierror.Check(listModulesV2(kymaConfig)) }, } return cmd } + +func listModulesV2(kymaConfig *cmdcommon.KymaConfig) clierror.Error { + moduleOperations := modulesv2.NewModuleOperations(kymaConfig) + + listService, err := moduleOperations.List() + if err != nil { + return clierror.Wrap(err, clierror.New("failed to execute the list command")) + } + + results, err := listService.Run(kymaConfig.Ctx) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to list installed modules")) + } + + for _, r := range results { + fmt.Println(r.Name) + } + + return nil +} diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index e0f3e6af8..25bddf1b3 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -12,11 +12,7 @@ import ( type ModuleOperations interface { Catalog() (*CatalogService, error) Pull() (*PullService, error) - // TODO - // Add() (*AddService, error) - // Install() (*InstallService, error) - // Pull() (*PullService, error) - // etc. + List() (*ListService, error) } type moduleOperations struct { @@ -38,6 +34,17 @@ func (m *moduleOperations) Catalog() (*CatalogService, error) { return catalogService, nil } +func (m *moduleOperations) List() (*ListService, error) { + c := setupDIContainer(m.kymaConfig) + + listService, err := di.GetTyped[*ListService](c) + if err != nil { + return nil, errors.New("failed to execute the list command") + } + + return listService, nil +} + func (m *moduleOperations) Pull() (*PullService, error) { c := setupDIContainer(m.kymaConfig) @@ -83,6 +90,15 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return repository.NewClusterMetadataRepository(kubeClient), nil }) + di.RegisterTyped(container, func(c *di.Container) (repository.InstalledModulesRepository, error) { + kubeClient, err := di.GetTyped[kube.Client](c) + if err != nil { + return nil, err + } + + return repository.NewInstalledModulesRepository(kubeClient.Kyma()), nil + }) + // Services: di.RegisterTyped(container, func(c *di.Container) (*CatalogService, error) { @@ -108,5 +124,14 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return NewPullService(moduleRepo), nil }) + di.RegisterTyped(container, func(c *di.Container) (*ListService, error) { + installedModulesRepo, err := di.GetTyped[repository.InstalledModulesRepository](c) + if err != nil { + return nil, err + } + + return NewListService(installedModulesRepo), nil + }) + return container } diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index 377d46bcc..7b9b20053 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -9,3 +9,20 @@ import ( type InstalledModulesRepository interface { ListInstalledModules(ctx context.Context) ([]kyma.ModuleStatus, error) } + +type installedModulesRepository struct { + kymaClient kyma.Interface +} + +func NewInstalledModulesRepository(kymaClient kyma.Interface) InstalledModulesRepository { + return &installedModulesRepository{kymaClient: kymaClient} +} + +func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]kyma.ModuleStatus, error) { + kymaCR, err := r.kymaClient.GetDefaultKyma(ctx) + if err != nil { + return nil, err + } + + return kymaCR.Status.Modules, nil +} diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go new file mode 100644 index 000000000..d56e437dd --- /dev/null +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -0,0 +1,32 @@ +package repository_test + +import ( + "context" + "testing" + + kubefake "github.com/kyma-project/cli.v3/internal/kube/fake" + "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/repository" + "github.com/stretchr/testify/require" +) + +func TestInstalledModulesRepository_ListInstalledModules(t *testing.T) { + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: kyma.Kyma{ + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{ + {Name: "api-gateway"}, + {Name: "istio"}, + }, + }, + }, + } + repo := repository.NewInstalledModulesRepository(kymaClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 2) + require.Equal(t, "api-gateway", result[0].Name) + require.Equal(t, "istio", result[1].Name) +} From cdbc3431a59847b32e7d630120923cec67b18b7f Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:56:23 +0200 Subject: [PATCH 06/29] add version and channel to ListResult --- internal/modulesv2/dtos/listresult.go | 4 +++- internal/modulesv2/list.go | 6 +++++- internal/modulesv2/list_test.go | 18 ++++++++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index 018528a09..0ba4cf176 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -1,5 +1,7 @@ package dtos type ListResult struct { - Name string + Name string + Version string + Channel string } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index b2852c8b7..ff096add1 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -23,7 +23,11 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { - results = append(results, dtos.ListResult{Name: module.Name}) + results = append(results, dtos.ListResult{ + Name: module.Name, + Version: module.Version, + Channel: module.Channel, + }) } return results, nil diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 88221f631..dd347a4f2 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -37,3 +37,21 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { require.Equal(t, "api-gateway", result[0].Name) require.Equal(t, "istio", result[1].Name) } + +func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.ModuleStatus{ + {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 1) + module := result[0] + require.Equal(t, "api-gateway", module.Name) + require.Equal(t, "3.5.1", module.Version) + require.Equal(t, "regular", module.Channel) +} From c144aa6b7a2eeb74375d888b64a30f3454148b92 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:04:05 +0200 Subject: [PATCH 07/29] render list as table with module, version, channel columns --- internal/cmd/alpha/module/list.go | 9 ++++----- internal/modulesv2/render.go | 11 +++++++++++ 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index de73ee7bd..6bd489ca5 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -1,8 +1,6 @@ package module import ( - "fmt" - "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" "github.com/kyma-project/cli.v3/internal/modulesv2" @@ -16,7 +14,7 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { Long: `Use this command to list all installed Kyma modules. NOTE: functionality under construction - - listing installed modules: partial (names only)`, + - listing installed core modules: partial (name, version, channel)`, Run: func(_ *cobra.Command, _ []string) { clierror.Check(listModulesV2(kymaConfig)) }, @@ -38,8 +36,9 @@ func listModulesV2(kymaConfig *cmdcommon.KymaConfig) clierror.Error { return clierror.Wrap(err, clierror.New("failed to list installed modules")) } - for _, r := range results { - fmt.Println(r.Name) + err = modulesv2.RenderList(results) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to render module list")) } return nil diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 65caf291c..fec66916b 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -12,6 +12,17 @@ import ( "gopkg.in/yaml.v3" ) +func RenderList(results []dtos.ListResult) error { + headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} + rows := make([][]interface{}, len(results)) + for i, r := range results { + rows[i] = []interface{}{r.Name, r.Version, r.Channel} + } + + render.Table(out.Default, headers, rows) + return nil +} + func RenderCatalog(results []dtos.CatalogResult, format types.Format) error { switch format { case types.JSONFormat: From 45ff5dcf9fcdaca2798233985f680acf9ff47ac3 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:05:58 +0200 Subject: [PATCH 08/29] update generated docs for alpha module list --- docs/user/gen-docs/_sidebar.ts | 1 + docs/user/gen-docs/kyma_alpha_module.md | 2 ++ docs/user/gen-docs/kyma_alpha_module_list.md | 28 ++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 docs/user/gen-docs/kyma_alpha_module_list.md diff --git a/docs/user/gen-docs/_sidebar.ts b/docs/user/gen-docs/_sidebar.ts index 1e401f51f..816d595ed 100755 --- a/docs/user/gen-docs/_sidebar.ts +++ b/docs/user/gen-docs/_sidebar.ts @@ -19,6 +19,7 @@ export default [ { text: 'kyma alpha kubeconfig generate', link: './gen-docs/kyma_alpha_kubeconfig_generate' }, { text: 'kyma alpha module', link: './gen-docs/kyma_alpha_module' }, { text: 'kyma alpha module catalog', link: './gen-docs/kyma_alpha_module_catalog' }, + { text: 'kyma alpha module list', link: './gen-docs/kyma_alpha_module_list' }, { text: 'kyma alpha module pull', link: './gen-docs/kyma_alpha_module_pull' }, { text: 'kyma alpha provision', link: './gen-docs/kyma_alpha_provision' }, { text: 'kyma alpha reference-instance', link: './gen-docs/kyma_alpha_reference-instance' }, diff --git a/docs/user/gen-docs/kyma_alpha_module.md b/docs/user/gen-docs/kyma_alpha_module.md index 1627c087c..fb0a80dc8 100644 --- a/docs/user/gen-docs/kyma_alpha_module.md +++ b/docs/user/gen-docs/kyma_alpha_module.md @@ -14,6 +14,7 @@ kyma alpha module [flags] ```text catalog - Lists modules catalog + list - Lists installed modules pull - Pulls a module from a remote repository ``` @@ -31,4 +32,5 @@ kyma alpha module [flags] * [kyma alpha](kyma_alpha.md) - Groups command prototypes for which the API may still change * [kyma alpha module catalog](kyma_alpha_module_catalog.md) - Lists modules catalog +* [kyma alpha module list](kyma_alpha_module_list.md) - Lists installed modules * [kyma alpha module pull](kyma_alpha_module_pull.md) - Pulls a module from a remote repository diff --git a/docs/user/gen-docs/kyma_alpha_module_list.md b/docs/user/gen-docs/kyma_alpha_module_list.md new file mode 100644 index 000000000..afb6b4bea --- /dev/null +++ b/docs/user/gen-docs/kyma_alpha_module_list.md @@ -0,0 +1,28 @@ +# kyma alpha module list + +Lists installed modules. + +## Synopsis + +Use this command to list all installed Kyma modules. + +NOTE: functionality under construction + - listing installed core modules: partial (name, version, channel) + +```bash +kyma alpha module list [flags] +``` + +## Flags + +```text + --context string The name of the kubeconfig context to use + -h, --help Help for the command + --kubeconfig string Path to the Kyma kubeconfig file + --show-extensions-error Prints a possible error when fetching extensions fails + --skip-extensions Skips fetching extensions from the target Kyma environment +``` + +## See also + +* [kyma alpha module](kyma_alpha_module.md) - Manages Kyma modules From 3bed8491b2285938b72d614404a9a8e34d15cfde Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:25:08 +0200 Subject: [PATCH 09/29] add output format support to list command (table, json) --- internal/cmd/alpha/module/list.go | 3 ++- internal/modulesv2/render.go | 31 +++++++++++++++++++++++--- internal/modulesv2/render_test.go | 36 +++++++++++++++++++++++++++++++ 3 files changed, 66 insertions(+), 4 deletions(-) create mode 100644 internal/modulesv2/render_test.go diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index 6bd489ca5..84dbe40bb 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -4,6 +4,7 @@ import ( "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" "github.com/kyma-project/cli.v3/internal/modulesv2" + "github.com/kyma-project/cli.v3/internal/out" "github.com/spf13/cobra" ) @@ -36,7 +37,7 @@ func listModulesV2(kymaConfig *cmdcommon.KymaConfig) clierror.Error { return clierror.Wrap(err, clierror.New("failed to list installed modules")) } - err = modulesv2.RenderList(results) + err = modulesv2.RenderList(results, "", out.Default) if err != nil { return clierror.Wrap(err, clierror.New("failed to render module list")) } diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index fec66916b..2fb24b0f8 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -12,14 +12,39 @@ import ( "gopkg.in/yaml.v3" ) -func RenderList(results []dtos.ListResult) error { +func RenderList(results []dtos.ListResult, format types.Format, printer *out.Printer) error { + switch format { + case types.JSONFormat: + return renderListJSON(results, printer) + default: + return renderListTable(results, printer) + } +} + +func renderListJSON(results []dtos.ListResult, printer *out.Printer) error { + output := make([]map[string]interface{}, len(results)) + for i, r := range results { + output[i] = map[string]interface{}{ + "name": r.Name, + "version": r.Version, + "channel": r.Channel, + } + } + obj, err := json.MarshalIndent(output, "", " ") + if err != nil { + return err + } + printer.Msgln(string(obj)) + return nil +} + +func renderListTable(results []dtos.ListResult, printer *out.Printer) error { headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} rows := make([][]interface{}, len(results)) for i, r := range results { rows[i] = []interface{}{r.Name, r.Version, r.Channel} } - - render.Table(out.Default, headers, rows) + render.Table(printer, headers, rows) return nil } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go new file mode 100644 index 000000000..b0e37a5b0 --- /dev/null +++ b/internal/modulesv2/render_test.go @@ -0,0 +1,36 @@ +package modulesv2 + +import ( + "bytes" + "testing" + + "github.com/kyma-project/cli.v3/internal/cmdcommon/types" + "github.com/kyma-project/cli.v3/internal/modulesv2/dtos" + "github.com/kyma-project/cli.v3/internal/out" + "github.com/stretchr/testify/require" +) + +func TestRenderList_Table(t *testing.T) { + results := []dtos.ListResult{ + {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + } + + var buf bytes.Buffer + err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) + + require.NoError(t, err) + require.Regexp(t, `MODULE.*VERSION.*CHANNEL`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1.*regular`, buf.String()) +} + +func TestRenderList_JSON(t *testing.T) { + results := []dtos.ListResult{ + {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + } + + var buf bytes.Buffer + err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) + + require.NoError(t, err) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular"}]`, buf.String()) +} From 20599deddaa4e9d0df5f74c8ae8d78b935964959 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:31:07 +0200 Subject: [PATCH 10/29] add yaml output format to list command --- internal/modulesv2/render.go | 19 +++++++++++++++++++ internal/modulesv2/render_test.go | 19 +++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 2fb24b0f8..a07e3c3c4 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -16,6 +16,8 @@ func RenderList(results []dtos.ListResult, format types.Format, printer *out.Pri switch format { case types.JSONFormat: return renderListJSON(results, printer) + case types.YAMLFormat: + return renderListYAML(results, printer) default: return renderListTable(results, printer) } @@ -38,6 +40,23 @@ func renderListJSON(results []dtos.ListResult, printer *out.Printer) error { return nil } +func renderListYAML(results []dtos.ListResult, printer *out.Printer) error { + output := make([]map[string]interface{}, len(results)) + for i, r := range results { + output[i] = map[string]interface{}{ + "name": r.Name, + "version": r.Version, + "channel": r.Channel, + } + } + obj, err := yaml.Marshal(output) + if err != nil { + return err + } + printer.Msgln(string(obj)) + return nil +} + func renderListTable(results []dtos.ListResult, printer *out.Printer) error { headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} rows := make([][]interface{}, len(results)) diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index b0e37a5b0..5ad2109ce 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -8,6 +8,7 @@ import ( "github.com/kyma-project/cli.v3/internal/modulesv2/dtos" "github.com/kyma-project/cli.v3/internal/out" "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" ) func TestRenderList_Table(t *testing.T) { @@ -34,3 +35,21 @@ func TestRenderList_JSON(t *testing.T) { require.NoError(t, err) require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular"}]`, buf.String()) } + +func TestRenderList_YAML(t *testing.T) { + results := []dtos.ListResult{ + {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + } + + var buf bytes.Buffer + err := RenderList(results, types.YAMLFormat, out.NewToWriter(&buf)) + + require.NoError(t, err) + var parsed []map[string]interface{} + require.NoError(t, yaml.Unmarshal(buf.Bytes(), &parsed)) + require.Len(t, parsed, 1) + module := parsed[0] + require.Equal(t, "api-gateway", module["name"]) + require.Equal(t, "3.5.1", module["version"]) + require.Equal(t, "regular", module["channel"]) +} From b506c6b3f4b6f6a4117e11bbc2e0307b86af1841 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:36:58 +0200 Subject: [PATCH 11/29] extract convertListToOutputFormat --- internal/modulesv2/render.go | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index a07e3c3c4..b1a7d90b8 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -24,14 +24,7 @@ func RenderList(results []dtos.ListResult, format types.Format, printer *out.Pri } func renderListJSON(results []dtos.ListResult, printer *out.Printer) error { - output := make([]map[string]interface{}, len(results)) - for i, r := range results { - output[i] = map[string]interface{}{ - "name": r.Name, - "version": r.Version, - "channel": r.Channel, - } - } + output := convertListToOutputFormat(results) obj, err := json.MarshalIndent(output, "", " ") if err != nil { return err @@ -41,6 +34,16 @@ func renderListJSON(results []dtos.ListResult, printer *out.Printer) error { } func renderListYAML(results []dtos.ListResult, printer *out.Printer) error { + output := convertListToOutputFormat(results) + obj, err := yaml.Marshal(output) + if err != nil { + return err + } + printer.Msgln(string(obj)) + return nil +} + +func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface{} { output := make([]map[string]interface{}, len(results)) for i, r := range results { output[i] = map[string]interface{}{ @@ -49,12 +52,7 @@ func renderListYAML(results []dtos.ListResult, printer *out.Printer) error { "channel": r.Channel, } } - obj, err := yaml.Marshal(output) - if err != nil { - return err - } - printer.Msgln(string(obj)) - return nil + return output } func renderListTable(results []dtos.ListResult, printer *out.Printer) error { From 647f66c63601f8a91004a6be32336fc5b984a222 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:48:07 +0200 Subject: [PATCH 12/29] extract convertListToRows --- internal/modulesv2/render.go | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index b1a7d90b8..916dd49a1 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -57,12 +57,17 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} + rows := convertListToRows(results) + render.Table(printer, headers, rows) + return nil +} + +func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { rows[i] = []interface{}{r.Name, r.Version, r.Channel} } - render.Table(printer, headers, rows) - return nil + return rows } func RenderCatalog(results []dtos.CatalogResult, format types.Format) error { From 0128efc3dfca289b0783d6fd9c2d32f3c6b7219c Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:53:32 +0200 Subject: [PATCH 13/29] add -o output format flag to list command --- docs/user/gen-docs/kyma_alpha_module_list.md | 1 + internal/cmd/alpha/module/list.go | 22 +++++++++++++++----- internal/cmd/alpha/module/list_test.go | 5 +++++ 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/docs/user/gen-docs/kyma_alpha_module_list.md b/docs/user/gen-docs/kyma_alpha_module_list.md index afb6b4bea..d3f03fc17 100644 --- a/docs/user/gen-docs/kyma_alpha_module_list.md +++ b/docs/user/gen-docs/kyma_alpha_module_list.md @@ -16,6 +16,7 @@ kyma alpha module list [flags] ## Flags ```text + -o, --output string Output format (Possible values: table, json, yaml) --context string The name of the kubeconfig context to use -h, --help Help for the command --kubeconfig string Path to the Kyma kubeconfig file diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index 84dbe40bb..d49e2def2 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -3,12 +3,22 @@ package module import ( "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/kyma-project/cli.v3/internal/cmdcommon/types" "github.com/kyma-project/cli.v3/internal/modulesv2" "github.com/kyma-project/cli.v3/internal/out" "github.com/spf13/cobra" ) +type listConfig struct { + *cmdcommon.KymaConfig + outputFormat types.Format +} + func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { + cfg := listConfig{ + KymaConfig: kymaConfig, + } + cmd := &cobra.Command{ Use: "list [flags]", Short: "Lists installed modules", @@ -17,27 +27,29 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { NOTE: functionality under construction - listing installed core modules: partial (name, version, channel)`, Run: func(_ *cobra.Command, _ []string) { - clierror.Check(listModulesV2(kymaConfig)) + clierror.Check(listModulesV2(&cfg)) }, } + cmd.Flags().VarP(&cfg.outputFormat, "output", "o", "Output format (Possible values: table, json, yaml)") + return cmd } -func listModulesV2(kymaConfig *cmdcommon.KymaConfig) clierror.Error { - moduleOperations := modulesv2.NewModuleOperations(kymaConfig) +func listModulesV2(cfg *listConfig) clierror.Error { + moduleOperations := modulesv2.NewModuleOperations(cfg.KymaConfig) listService, err := moduleOperations.List() if err != nil { return clierror.Wrap(err, clierror.New("failed to execute the list command")) } - results, err := listService.Run(kymaConfig.Ctx) + results, err := listService.Run(cfg.Ctx) if err != nil { return clierror.Wrap(err, clierror.New("failed to list installed modules")) } - err = modulesv2.RenderList(results, "", out.Default) + err = modulesv2.RenderList(results, cfg.outputFormat, out.Default) if err != nil { return clierror.Wrap(err, clierror.New("failed to render module list")) } diff --git a/internal/cmd/alpha/module/list_test.go b/internal/cmd/alpha/module/list_test.go index b86b7b330..5bbf74f10 100644 --- a/internal/cmd/alpha/module/list_test.go +++ b/internal/cmd/alpha/module/list_test.go @@ -12,3 +12,8 @@ func TestListCmd_Exists(t *testing.T) { require.NotNil(t, cmd) require.Equal(t, "list [flags]", cmd.Use) } + +func TestListCmd_HasOutputFlag(t *testing.T) { + cmd := NewListV2CMD(&cmdcommon.KymaConfig{}) + require.NotNil(t, cmd.Flags().Lookup("output")) +} From 4ee57623cfdf9694c7e3db205f331b16ac15ebcf Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:59:26 +0200 Subject: [PATCH 14/29] sort list results by name --- internal/modulesv2/render.go | 7 +++++++ internal/modulesv2/render_test.go | 13 +++++++++++++ 2 files changed, 20 insertions(+) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 916dd49a1..df243035e 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -56,12 +56,19 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface } func renderListTable(results []dtos.ListResult, printer *out.Printer) error { + sortListResults(results) headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil } +func sortListResults(results []dtos.ListResult) { + sort.Slice(results, func(i, j int) bool { + return results[i].Name < results[j].Name + }) +} + func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index 5ad2109ce..f8c914d8d 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -36,6 +36,19 @@ func TestRenderList_JSON(t *testing.T) { require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular"}]`, buf.String()) } +func TestRenderList_Table_SortedByName(t *testing.T) { + results := []dtos.ListResult{ + {Name: "istio"}, + {Name: "api-gateway"}, + } + + var buf bytes.Buffer + err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) + + require.NoError(t, err) + require.Regexp(t, `(?s)api-gateway.*istio`, buf.String()) +} + func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, From 9575398c50343314a7bab5d2aebd4aeff387e736 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 09:15:28 +0200 Subject: [PATCH 15/29] add state field to ListResult --- internal/modulesv2/dtos/listresult.go | 1 + internal/modulesv2/list.go | 1 + internal/modulesv2/list_test.go | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index 0ba4cf176..f0a256ca9 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -4,4 +4,5 @@ type ListResult struct { Name string Version string Channel string + State string } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index ff096add1..f05b38a08 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -27,6 +27,7 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { Name: module.Name, Version: module.Version, Channel: module.Channel, + State: module.State, }) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index dd347a4f2..2f2809309 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -41,7 +41,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []kyma.ModuleStatus{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, }, } svc := NewListService(installedModulesRepo) @@ -54,4 +54,5 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { require.Equal(t, "api-gateway", module.Name) require.Equal(t, "3.5.1", module.Version) require.Equal(t, "regular", module.Channel) + require.Equal(t, "Ready", module.State) } From 0b618c86f72a37510e38c5d797904092c4fe384a Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:28:41 +0200 Subject: [PATCH 16/29] add state field to list rendering --- internal/modulesv2/render.go | 5 +++-- internal/modulesv2/render_test.go | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index df243035e..2842d670a 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -50,6 +50,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface "name": r.Name, "version": r.Version, "channel": r.Channel, + "state": r.State, } } return output @@ -57,7 +58,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { sortListResults(results) - headers := []interface{}{"MODULE", "VERSION", "CHANNEL"} + headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil @@ -72,7 +73,7 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, r.Version, r.Channel} + rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State} } return rows } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index f8c914d8d..5ace73bed 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -13,27 +13,27 @@ import ( func TestRenderList_Table(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, } var buf bytes.Buffer err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `MODULE.*VERSION.*CHANNEL`, buf.String()) - require.Regexp(t, `api-gateway.*3\.5\.1.*regular`, buf.String()) + require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready`, buf.String()) } func TestRenderList_JSON(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, } var buf bytes.Buffer err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular"}]`, buf.String()) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready"}]`, buf.String()) } func TestRenderList_Table_SortedByName(t *testing.T) { @@ -51,7 +51,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, } var buf bytes.Buffer @@ -65,4 +65,5 @@ func TestRenderList_YAML(t *testing.T) { require.Equal(t, "api-gateway", module["name"]) require.Equal(t, "3.5.1", module["version"]) require.Equal(t, "regular", module["channel"]) + require.Equal(t, "Ready", module["state"]) } From d2909738ed1fb476029a39f98e68b06d2476d693 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:40:17 +0200 Subject: [PATCH 17/29] change InstalledModulesRepository to return KymaModuleInfo --- internal/modulesv2/fake/installedmodules.go | 4 ++-- internal/modulesv2/list.go | 8 ++++---- internal/modulesv2/list_test.go | 12 ++++++------ .../modulesv2/repository/installedmodules.go | 16 +++++++++++++--- .../repository/installedmodules_test.go | 4 ++-- 5 files changed, 27 insertions(+), 17 deletions(-) diff --git a/internal/modulesv2/fake/installedmodules.go b/internal/modulesv2/fake/installedmodules.go index 1307cf219..3cc69cf18 100644 --- a/internal/modulesv2/fake/installedmodules.go +++ b/internal/modulesv2/fake/installedmodules.go @@ -7,10 +7,10 @@ import ( ) type InstalledModulesRepository struct { - ListInstalledModulesResult []kyma.ModuleStatus + ListInstalledModulesResult []kyma.KymaModuleInfo ListInstalledModulesError error } -func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]kyma.ModuleStatus, error) { +func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]kyma.KymaModuleInfo, error) { return f.ListInstalledModulesResult, f.ListInstalledModulesError } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index f05b38a08..4512011d1 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -24,10 +24,10 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { results = append(results, dtos.ListResult{ - Name: module.Name, - Version: module.Version, - Channel: module.Channel, - State: module.State, + Name: module.Status.Name, + Version: module.Status.Version, + Channel: module.Status.Channel, + State: module.Status.State, }) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 2f2809309..db95739d5 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -11,7 +11,7 @@ import ( func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.ModuleStatus{}, + ListInstalledModulesResult: []kyma.KymaModuleInfo{}, } svc := NewListService(installedModulesRepo) @@ -23,9 +23,9 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { func TestListService_Run_ReturnsCoreModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.ModuleStatus{ - {Name: "api-gateway"}, - {Name: "istio"}, + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + {Status: kyma.ModuleStatus{Name: "api-gateway"}}, + {Status: kyma.ModuleStatus{Name: "istio"}}, }, } svc := NewListService(installedModulesRepo) @@ -40,8 +40,8 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.ModuleStatus{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + {Status: kyma.ModuleStatus{Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}}, }, } svc := NewListService(installedModulesRepo) diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index 7b9b20053..a66cb5bd3 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -7,7 +7,7 @@ import ( ) type InstalledModulesRepository interface { - ListInstalledModules(ctx context.Context) ([]kyma.ModuleStatus, error) + ListInstalledModules(ctx context.Context) ([]kyma.KymaModuleInfo, error) } type installedModulesRepository struct { @@ -18,11 +18,21 @@ func NewInstalledModulesRepository(kymaClient kyma.Interface) InstalledModulesRe return &installedModulesRepository{kymaClient: kymaClient} } -func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]kyma.ModuleStatus, error) { +func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]kyma.KymaModuleInfo, error) { kymaCR, err := r.kymaClient.GetDefaultKyma(ctx) if err != nil { return nil, err } - return kymaCR.Status.Modules, nil + modules := make([]kyma.KymaModuleInfo, len(kymaCR.Status.Modules)) + for i, status := range kymaCR.Status.Modules { + modules[i] = kyma.KymaModuleInfo{Status: status} + for _, spec := range kymaCR.Spec.Modules { + if spec.Name == status.Name { + modules[i].Spec = spec + break + } + } + } + return modules, nil } diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index d56e437dd..b2c25fd82 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -27,6 +27,6 @@ func TestInstalledModulesRepository_ListInstalledModules(t *testing.T) { require.NoError(t, err) require.Len(t, result, 2) - require.Equal(t, "api-gateway", result[0].Name) - require.Equal(t, "istio", result[1].Name) + require.Equal(t, "api-gateway", result[0].Status.Name) + require.Equal(t, "istio", result[1].Status.Name) } From af5a6de6167d031f844689e45aaabe1e6a82f7ee Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:45:16 +0200 Subject: [PATCH 18/29] add managed field to ListResult --- internal/modulesv2/dtos/listresult.go | 1 + internal/modulesv2/list.go | 1 + internal/modulesv2/list_test.go | 58 +++++++++++++++++++++++++++ 3 files changed, 60 insertions(+) diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index f0a256ca9..a075acbee 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -5,4 +5,5 @@ type ListResult struct { Version string Channel string State string + Managed bool } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 4512011d1..4b3c6b873 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -28,6 +28,7 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { Version: module.Status.Version, Channel: module.Status.Channel, State: module.Status.State, + Managed: module.Spec.Managed == nil || *module.Spec.Managed, }) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index db95739d5..1c2b121b8 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -56,3 +56,61 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { require.Equal(t, "regular", module.Channel) require.Equal(t, "Ready", module.State) } + +func TestListService_Run_ReturnsManaged(t *testing.T) { + managed := true + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + { + Spec: kyma.Module{Name: "api-gateway", Managed: &managed}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + }, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 1) + module := result[0] + require.Equal(t, "api-gateway", module.Name) + require.True(t, module.Managed) +} + +func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + { + Spec: kyma.Module{Name: "api-gateway", Managed: nil}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + }, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + module := result[0] + require.True(t, module.Managed) +} + +func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { + managed := false + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + { + Spec: kyma.Module{Name: "api-gateway", Managed: &managed}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + }, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + module := result[0] + require.False(t, module.Managed) +} From 2099c34504722d92fad6688c3ec95eea5e591be2 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:49:30 +0200 Subject: [PATCH 19/29] add managed field to list rendering --- internal/modulesv2/render.go | 5 +++-- internal/modulesv2/render_test.go | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 2842d670a..d2d247ccf 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -51,6 +51,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface "version": r.Version, "channel": r.Channel, "state": r.State, + "managed": r.Managed, } } return output @@ -58,7 +59,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { sortListResults(results) - headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE"} + headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE", "MANAGED"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil @@ -73,7 +74,7 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State} + rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State, r.Managed} } return rows } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index 5ace73bed..ca0ad8806 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -13,27 +13,27 @@ import ( func TestRenderList_Table(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, } var buf bytes.Buffer err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE`, buf.String()) - require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready`, buf.String()) + require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE.*MANAGED`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready.*true`, buf.String()) } func TestRenderList_JSON(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, } var buf bytes.Buffer err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready"}]`, buf.String()) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true}]`, buf.String()) } func TestRenderList_Table_SortedByName(t *testing.T) { @@ -51,7 +51,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, } var buf bytes.Buffer @@ -66,4 +66,5 @@ func TestRenderList_YAML(t *testing.T) { require.Equal(t, "3.5.1", module["version"]) require.Equal(t, "regular", module["channel"]) require.Equal(t, "Ready", module["state"]) + require.Equal(t, true, module["managed"]) } From 8a17cf7bf79c3df71263bb935a636a03a8422c87 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:22:36 +0200 Subject: [PATCH 20/29] add customResourcePolicy field to ListResult --- internal/modulesv2/dtos/listresult.go | 11 ++++++----- internal/modulesv2/list.go | 11 ++++++----- internal/modulesv2/list_test.go | 18 ++++++++++++++++++ 3 files changed, 30 insertions(+), 10 deletions(-) diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index a075acbee..a89b21864 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -1,9 +1,10 @@ package dtos type ListResult struct { - Name string - Version string - Channel string - State string - Managed bool + Name string + Version string + Channel string + State string + Managed bool + CustomResourcePolicy string } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 4b3c6b873..7680891c2 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -24,11 +24,12 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { results = append(results, dtos.ListResult{ - Name: module.Status.Name, - Version: module.Status.Version, - Channel: module.Status.Channel, - State: module.Status.State, - Managed: module.Spec.Managed == nil || *module.Spec.Managed, + Name: module.Status.Name, + Version: module.Status.Version, + Channel: module.Status.Channel, + State: module.Status.State, + Managed: module.Spec.Managed == nil || *module.Spec.Managed, + CustomResourcePolicy: module.Spec.CustomResourcePolicy, }) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 1c2b121b8..fc2437840 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -114,3 +114,21 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { module := result[0] require.False(t, module.Managed) } + +func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + { + Spec: kyma.Module{Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + }, + }, + } + svc := NewListService(installedModulesRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) +} From b080993302e2816ef8b5a4da98f4c5bb57bb7254 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:28:22 +0200 Subject: [PATCH 21/29] add customResourcePolicy field to list rendering --- internal/modulesv2/render.go | 15 ++++++++------- internal/modulesv2/render_test.go | 13 +++++++------ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index d2d247ccf..c5504a148 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -47,11 +47,12 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface output := make([]map[string]interface{}, len(results)) for i, r := range results { output[i] = map[string]interface{}{ - "name": r.Name, - "version": r.Version, - "channel": r.Channel, - "state": r.State, - "managed": r.Managed, + "name": r.Name, + "version": r.Version, + "channel": r.Channel, + "state": r.State, + "managed": r.Managed, + "customResourcePolicy": r.CustomResourcePolicy, } } return output @@ -59,7 +60,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { sortListResults(results) - headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE", "MANAGED"} + headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE", "MANAGED", "CUSTOM RESOURCE POLICY"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil @@ -74,7 +75,7 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State, r.Managed} + rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State, r.Managed, r.CustomResourcePolicy} } return rows } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index ca0ad8806..4a3dcc6a5 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -13,27 +13,27 @@ import ( func TestRenderList_Table(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, } var buf bytes.Buffer err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE.*MANAGED`, buf.String()) - require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready.*true`, buf.String()) + require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE.*MANAGED.*CUSTOM RESOURCE POLICY`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready.*true.*CreateAndDelete`, buf.String()) } func TestRenderList_JSON(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, } var buf bytes.Buffer err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true}]`, buf.String()) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true,"customResourcePolicy":"CreateAndDelete"}]`, buf.String()) } func TestRenderList_Table_SortedByName(t *testing.T) { @@ -51,7 +51,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, } var buf bytes.Buffer @@ -67,4 +67,5 @@ func TestRenderList_YAML(t *testing.T) { require.Equal(t, "regular", module["channel"]) require.Equal(t, "Ready", module["state"]) require.Equal(t, true, module["managed"]) + require.Equal(t, "CreateAndDelete", module["customResourcePolicy"]) } From a068923c76092930048aac8c894084f26ef62d37 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:39:43 +0200 Subject: [PATCH 22/29] align list table columns with ADR-003 --- internal/modulesv2/render.go | 12 ++++++++++-- internal/modulesv2/render_test.go | 4 ++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index c5504a148..25af8bb50 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -2,6 +2,7 @@ package modulesv2 import ( "encoding/json" + "fmt" "sort" "strings" @@ -60,7 +61,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { sortListResults(results) - headers := []interface{}{"MODULE", "VERSION", "CHANNEL", "STATE", "MANAGED", "CUSTOM RESOURCE POLICY"} + headers := []interface{}{"MODULE", "VERSION", "CR POLICY", "MANAGED", "MODULE STATUS"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil @@ -75,11 +76,18 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, r.Version, r.Channel, r.State, r.Managed, r.CustomResourcePolicy} + rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State} } return rows } +func versionWithChannel(r dtos.ListResult) string { + if r.Channel == "" { + return r.Version + } + return fmt.Sprintf("%s(%s)", r.Version, r.Channel) +} + func RenderCatalog(results []dtos.CatalogResult, format types.Format) error { switch format { case types.JSONFormat: diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index 4a3dcc6a5..dfbad6721 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -20,8 +20,8 @@ func TestRenderList_Table(t *testing.T) { err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `MODULE.*VERSION.*CHANNEL.*STATE.*MANAGED.*CUSTOM RESOURCE POLICY`, buf.String()) - require.Regexp(t, `api-gateway.*3\.5\.1.*regular.*Ready.*true.*CreateAndDelete`, buf.String()) + require.Regexp(t, `MODULE.*VERSION.*CR POLICY.*MANAGED.*MODULE STATUS`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1\(regular\).*CreateAndDelete.*true.*Ready`, buf.String()) } func TestRenderList_JSON(t *testing.T) { From 94f45ef9268307eb34db374a92c516d0fbd2e21a Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:55:03 +0200 Subject: [PATCH 23/29] add installation state via ModuleInstallationStateRepository --- internal/modulesv2/dependencies.go | 16 ++- internal/modulesv2/dtos/listresult.go | 1 + .../modulesv2/fake/moduleinstallationstate.go | 16 +++ internal/modulesv2/list.go | 16 ++- internal/modulesv2/list_test.go | 35 ++++- .../repository/moduleinstallationstate.go | 121 ++++++++++++++++++ 6 files changed, 194 insertions(+), 11 deletions(-) create mode 100644 internal/modulesv2/fake/moduleinstallationstate.go create mode 100644 internal/modulesv2/repository/moduleinstallationstate.go diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index 25bddf1b3..a90fcce06 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -99,6 +99,15 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return repository.NewInstalledModulesRepository(kubeClient.Kyma()), nil }) + di.RegisterTyped(container, func(c *di.Container) (repository.ModuleInstallationStateRepository, error) { + kubeClient, err := di.GetTyped[kube.Client](c) + if err != nil { + return nil, err + } + + return repository.NewModuleInstallationStateRepository(kubeClient), nil + }) + // Services: di.RegisterTyped(container, func(c *di.Container) (*CatalogService, error) { @@ -130,7 +139,12 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - return NewListService(installedModulesRepo), nil + installationStateRepo, err := di.GetTyped[repository.ModuleInstallationStateRepository](c) + if err != nil { + return nil, err + } + + return NewListService(installedModulesRepo, installationStateRepo), nil }) return container diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index a89b21864..5a5c4c1d7 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -7,4 +7,5 @@ type ListResult struct { State string Managed bool CustomResourcePolicy string + InstallationState string } diff --git a/internal/modulesv2/fake/moduleinstallationstate.go b/internal/modulesv2/fake/moduleinstallationstate.go new file mode 100644 index 000000000..19cf2a9cd --- /dev/null +++ b/internal/modulesv2/fake/moduleinstallationstate.go @@ -0,0 +1,16 @@ +package fake + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" +) + +type ModuleInstallationStateRepository struct { + GetInstallationStateResult string + GetInstallationStateError error +} + +func (f *ModuleInstallationStateRepository) GetInstallationState(_ context.Context, _ kyma.ModuleStatus, _ kyma.Module) (string, error) { + return f.GetInstallationStateResult, f.GetInstallationStateError +} diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 7680891c2..4cdc60bce 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -8,11 +8,15 @@ import ( ) type ListService struct { - installedModulesRepository repository.InstalledModulesRepository + installedModulesRepository repository.InstalledModulesRepository + installationStateRepository repository.ModuleInstallationStateRepository } -func NewListService(installedModulesRepository repository.InstalledModulesRepository) *ListService { - return &ListService{installedModulesRepository: installedModulesRepository} +func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository) *ListService { + return &ListService{ + installedModulesRepository: installedModulesRepository, + installationStateRepository: installationStateRepository, + } } func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { @@ -23,6 +27,11 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { + installationState, err := s.installationStateRepository.GetInstallationState(ctx, module.Status, module.Spec) + if err != nil { + return nil, err + } + results = append(results, dtos.ListResult{ Name: module.Status.Name, Version: module.Status.Version, @@ -30,6 +39,7 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { State: module.Status.State, Managed: module.Spec.Managed == nil || *module.Spec.Managed, CustomResourcePolicy: module.Spec.CustomResourcePolicy, + InstallationState: installationState, }) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index fc2437840..cec391a57 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -13,7 +13,7 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []kyma.KymaModuleInfo{}, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -28,7 +28,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { {Status: kyma.ModuleStatus{Name: "istio"}}, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -44,7 +44,7 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { {Status: kyma.ModuleStatus{Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}}, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -67,7 +67,7 @@ func TestListService_Run_ReturnsManaged(t *testing.T) { }, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -87,7 +87,7 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { }, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -106,7 +106,7 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { }, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -124,7 +124,7 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { }, }, } - svc := NewListService(installedModulesRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -132,3 +132,24 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { module := result[0] require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) } + +func TestListService_Run_ReturnsInstallationState(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []kyma.KymaModuleInfo{ + { + Spec: kyma.Module{Name: "api-gateway"}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + }, + }, + } + installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ + GetInstallationStateResult: "Ready", + } + svc := NewListService(installedModulesRepo, installationStateRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Ready", module.InstallationState) +} diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go new file mode 100644 index 000000000..8162da8d6 --- /dev/null +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -0,0 +1,121 @@ +package repository + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube" + "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/pkg/errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type ModuleInstallationStateRepository interface { + GetInstallationState(ctx context.Context, status kyma.ModuleStatus, spec kyma.Module) (string, error) +} + +type moduleInstallationStateRepository struct { + kubeClient kube.Client +} + +func NewModuleInstallationStateRepository(kubeClient kube.Client) ModuleInstallationStateRepository { + return &moduleInstallationStateRepository{kubeClient: kubeClient} +} + +func (r *moduleInstallationStateRepository) GetInstallationState(ctx context.Context, status kyma.ModuleStatus, spec kyma.Module) (string, error) { + if spec.CustomResourcePolicy == "CreateAndDelete" { + return status.State, nil + } + + if spec.Managed != nil && !*spec.Managed { + return status.State, nil + } + + moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, status.Template.GetNamespace(), status.Template.GetName()) + if err != nil { + if apierrors.IsNotFound(err) { + return "", nil + } + return "", errors.Wrapf(err, "failed to get ModuleTemplate %s/%s", status.Template.GetNamespace(), status.Template.GetName()) + } + + return getResourceState(ctx, r.kubeClient, moduleTemplate.Spec.Manager) +} + +func getResourceState(ctx context.Context, client kube.Client, manager *kyma.Manager) (string, error) { + if manager == nil { + return "", nil + } + namespace := "kyma-system" + if manager.Namespace != "" { + namespace = manager.Namespace + } + + apiVersion := manager.Group + "/" + manager.Version + unstruct := unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": apiVersion, + "kind": manager.Kind, + "metadata": map[string]interface{}{ + "name": manager.Name, + "namespace": namespace, + }, + }, + } + + result, err := client.RootlessDynamic().Get(ctx, &unstruct) + if err != nil { + if apierrors.IsNotFound(err) { + return "", nil + } + return "", err + } + + statusRaw, ok := result.Object["status"] + if !ok || statusRaw == nil { + return "", nil + } + status := statusRaw.(map[string]any) + if state, ok := status["state"]; ok { + return state.(string), nil + } + + if conditions, ok := status["conditions"]; ok { + return getStateFromConditions(conditions.([]any)), nil + } + + if readyReplicas, ok := status["readyReplicas"]; ok { + spec := result.Object["spec"].(map[string]any) + if wantedReplicas, ok := spec["replicas"]; ok { + return resolveStateFromReplicas(readyReplicas.(int64), wantedReplicas.(int64)), nil + } + } + + return "", nil +} + +func getStateFromConditions(conditions []interface{}) string { + for _, condition := range conditions { + c := condition.(map[string]interface{}) + if c["status"] != "True" { + continue + } + switch c["type"].(string) { + case "Available": + return "Ready" + case "Processing", "Error", "Warning": + return c["type"].(string) + } + } + return "" +} + +func resolveStateFromReplicas(ready, wanted int64) string { + if ready == wanted { + return "Ready" + } + if ready < wanted { + return "Processing" + } + return "Deleting" +} From f82ff858105653b5e91216448630038cccf4ce9b Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 13:01:40 +0200 Subject: [PATCH 24/29] render installationState in all output formats --- internal/modulesv2/render.go | 5 +++-- internal/modulesv2/render_test.go | 13 +++++++------ 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 25af8bb50..99c67784a 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -54,6 +54,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface "state": r.State, "managed": r.Managed, "customResourcePolicy": r.CustomResourcePolicy, + "installationState": r.InstallationState, } } return output @@ -61,7 +62,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface func renderListTable(results []dtos.ListResult, printer *out.Printer) error { sortListResults(results) - headers := []interface{}{"MODULE", "VERSION", "CR POLICY", "MANAGED", "MODULE STATUS"} + headers := []interface{}{"MODULE", "VERSION", "CR POLICY", "MANAGED", "MODULE STATUS", "INSTALLATION STATUS"} rows := convertListToRows(results) render.Table(printer, headers, rows) return nil @@ -76,7 +77,7 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State} + rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State, r.InstallationState} } return rows } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index dfbad6721..1d5dd46fe 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -13,27 +13,27 @@ import ( func TestRenderList_Table(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `MODULE.*VERSION.*CR POLICY.*MANAGED.*MODULE STATUS`, buf.String()) - require.Regexp(t, `api-gateway.*3\.5\.1\(regular\).*CreateAndDelete.*true.*Ready`, buf.String()) + require.Regexp(t, `MODULE.*VERSION.*CR POLICY.*MANAGED.*MODULE STATUS.*INSTALLATION STATUS`, buf.String()) + require.Regexp(t, `api-gateway.*3\.5\.1\(regular\).*CreateAndDelete.*true.*Ready.*Ready`, buf.String()) } func TestRenderList_JSON(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true,"customResourcePolicy":"CreateAndDelete"}]`, buf.String()) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true,"customResourcePolicy":"CreateAndDelete","installationState":"Ready"}]`, buf.String()) } func TestRenderList_Table_SortedByName(t *testing.T) { @@ -51,7 +51,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer @@ -68,4 +68,5 @@ func TestRenderList_YAML(t *testing.T) { require.Equal(t, "Ready", module["state"]) require.Equal(t, true, module["managed"]) require.Equal(t, "CreateAndDelete", module["customResourcePolicy"]) + require.Equal(t, "Ready", module["installationState"]) } From 210ab90133b2be343994a28dc0fb461b02c78c21 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 13:18:43 +0200 Subject: [PATCH 25/29] combine module and installation status when they differ --- internal/modulesv2/render.go | 9 ++++++++- internal/modulesv2/render_test.go | 12 ++++++++++++ 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 99c67784a..2db4136a7 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -77,11 +77,18 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State, r.InstallationState} + rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State, installationStatus(r)} } return rows } +func installationStatus(r dtos.ListResult) string { + if r.InstallationState != "" && r.State != r.InstallationState { + return fmt.Sprintf("%s(%s)", r.State, r.InstallationState) + } + return r.InstallationState +} + func versionWithChannel(r dtos.ListResult) string { if r.Channel == "" { return r.Version diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index 1d5dd46fe..796ca4ac0 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -49,6 +49,18 @@ func TestRenderList_Table_SortedByName(t *testing.T) { require.Regexp(t, `(?s)api-gateway.*istio`, buf.String()) } +func TestRenderList_Table_CombinedInstallationStatus(t *testing.T) { + results := []dtos.ListResult{ + {Name: "nats", State: "Warning", InstallationState: "Deleting"}, + } + + var buf bytes.Buffer + err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) + + require.NoError(t, err) + require.Regexp(t, `nats.*Warning\(Deleting\)`, buf.String()) +} + func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, From 3fd52e5466653108ec945af93befaf2fc4e2e2f5 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 21:09:29 +0200 Subject: [PATCH 26/29] update list command description --- docs/user/gen-docs/kyma_alpha_module_list.md | 4 ++-- internal/cmd/alpha/module/list.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/user/gen-docs/kyma_alpha_module_list.md b/docs/user/gen-docs/kyma_alpha_module_list.md index d3f03fc17..cfa4216ab 100644 --- a/docs/user/gen-docs/kyma_alpha_module_list.md +++ b/docs/user/gen-docs/kyma_alpha_module_list.md @@ -4,10 +4,10 @@ Lists installed modules. ## Synopsis -Use this command to list all installed Kyma modules. +Use this command to list the installed Kyma modules. NOTE: functionality under construction - - listing installed core modules: partial (name, version, channel) + - community modules not yet supported ```bash kyma alpha module list [flags] diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index d49e2def2..da1f8290d 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -22,10 +22,10 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd := &cobra.Command{ Use: "list [flags]", Short: "Lists installed modules", - Long: `Use this command to list all installed Kyma modules. + Long: `Use this command to list the installed Kyma modules. NOTE: functionality under construction - - listing installed core modules: partial (name, version, channel)`, + - community modules not yet supported`, Run: func(_ *cobra.Command, _ []string) { clierror.Check(listModulesV2(&cfg)) }, From 18e0bbabd2bfe29299354035b6e8503fa12742f4 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 21:26:47 +0200 Subject: [PATCH 27/29] align JSON/YAML keys with column names --- internal/modulesv2/render.go | 14 +++++++------- internal/modulesv2/render_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 2db4136a7..639d867c0 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -48,13 +48,13 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface output := make([]map[string]interface{}, len(results)) for i, r := range results { output[i] = map[string]interface{}{ - "name": r.Name, - "version": r.Version, - "channel": r.Channel, - "state": r.State, - "managed": r.Managed, - "customResourcePolicy": r.CustomResourcePolicy, - "installationState": r.InstallationState, + "name": r.Name, + "version": r.Version, + "channel": r.Channel, + "moduleStatus": r.State, + "managed": r.Managed, + "crPolicy": r.CustomResourcePolicy, + "installationStatus": r.InstallationState, } } return output diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index 796ca4ac0..d3dd31722 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -33,7 +33,7 @@ func TestRenderList_JSON(t *testing.T) { err := RenderList(results, types.JSONFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","state":"Ready","managed":true,"customResourcePolicy":"CreateAndDelete","installationState":"Ready"}]`, buf.String()) + require.JSONEq(t, `[{"name":"api-gateway","version":"3.5.1","channel":"regular","moduleStatus":"Ready","managed":true,"crPolicy":"CreateAndDelete","installationStatus":"Ready"}]`, buf.String()) } func TestRenderList_Table_SortedByName(t *testing.T) { @@ -77,8 +77,8 @@ func TestRenderList_YAML(t *testing.T) { require.Equal(t, "api-gateway", module["name"]) require.Equal(t, "3.5.1", module["version"]) require.Equal(t, "regular", module["channel"]) - require.Equal(t, "Ready", module["state"]) require.Equal(t, true, module["managed"]) - require.Equal(t, "CreateAndDelete", module["customResourcePolicy"]) - require.Equal(t, "Ready", module["installationState"]) + require.Equal(t, "CreateAndDelete", module["crPolicy"]) + require.Equal(t, "Ready", module["moduleStatus"]) + require.Equal(t, "Ready", module["installationStatus"]) } From 65e2a4fd7d4157e04facbec5f0ff38bb5bee2627 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 22:38:06 +0200 Subject: [PATCH 28/29] rename State to ModuleState in ListResult --- internal/modulesv2/dtos/listresult.go | 2 +- internal/modulesv2/list.go | 2 +- internal/modulesv2/list_test.go | 2 +- internal/modulesv2/render.go | 8 ++++---- internal/modulesv2/render_test.go | 8 ++++---- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/internal/modulesv2/dtos/listresult.go b/internal/modulesv2/dtos/listresult.go index 5a5c4c1d7..a4630db40 100644 --- a/internal/modulesv2/dtos/listresult.go +++ b/internal/modulesv2/dtos/listresult.go @@ -4,7 +4,7 @@ type ListResult struct { Name string Version string Channel string - State string + ModuleState string Managed bool CustomResourcePolicy string InstallationState string diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 4cdc60bce..add7f4692 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -36,7 +36,7 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { Name: module.Status.Name, Version: module.Status.Version, Channel: module.Status.Channel, - State: module.Status.State, + ModuleState: module.Status.State, Managed: module.Spec.Managed == nil || *module.Spec.Managed, CustomResourcePolicy: module.Spec.CustomResourcePolicy, InstallationState: installationState, diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index cec391a57..a0eb666ac 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -54,7 +54,7 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { require.Equal(t, "api-gateway", module.Name) require.Equal(t, "3.5.1", module.Version) require.Equal(t, "regular", module.Channel) - require.Equal(t, "Ready", module.State) + require.Equal(t, "Ready", module.ModuleState) } func TestListService_Run_ReturnsManaged(t *testing.T) { diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index 639d867c0..b447322db 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -51,7 +51,7 @@ func convertListToOutputFormat(results []dtos.ListResult) []map[string]interface "name": r.Name, "version": r.Version, "channel": r.Channel, - "moduleStatus": r.State, + "moduleStatus": r.ModuleState, "managed": r.Managed, "crPolicy": r.CustomResourcePolicy, "installationStatus": r.InstallationState, @@ -77,14 +77,14 @@ func sortListResults(results []dtos.ListResult) { func convertListToRows(results []dtos.ListResult) [][]interface{} { rows := make([][]interface{}, len(results)) for i, r := range results { - rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.State, installationStatus(r)} + rows[i] = []interface{}{r.Name, versionWithChannel(r), r.CustomResourcePolicy, r.Managed, r.ModuleState, installationStatus(r)} } return rows } func installationStatus(r dtos.ListResult) string { - if r.InstallationState != "" && r.State != r.InstallationState { - return fmt.Sprintf("%s(%s)", r.State, r.InstallationState) + if r.InstallationState != "" && r.ModuleState != r.InstallationState { + return fmt.Sprintf("%s(%s)", r.ModuleState, r.InstallationState) } return r.InstallationState } diff --git a/internal/modulesv2/render_test.go b/internal/modulesv2/render_test.go index d3dd31722..f9920663c 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -13,7 +13,7 @@ import ( func TestRenderList_Table(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer @@ -26,7 +26,7 @@ func TestRenderList_Table(t *testing.T) { func TestRenderList_JSON(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer @@ -51,7 +51,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { func TestRenderList_Table_CombinedInstallationStatus(t *testing.T) { results := []dtos.ListResult{ - {Name: "nats", State: "Warning", InstallationState: "Deleting"}, + {Name: "nats", ModuleState: "Warning", InstallationState: "Deleting"}, } var buf bytes.Buffer @@ -63,7 +63,7 @@ func TestRenderList_Table_CombinedInstallationStatus(t *testing.T) { func TestRenderList_YAML(t *testing.T) { results := []dtos.ListResult{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready", Managed: true, CustomResourcePolicy: "CreateAndDelete", InstallationState: "Ready"}, } var buf bytes.Buffer From cc073ec5238414bc4fa36bf4535c93d2e2af9e71 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 28 Apr 2026 13:53:13 +0200 Subject: [PATCH 29/29] introduce ModuleInstallation entity, move business logic to service --- .../modulesv2/entities/moduleinstallation.go | 29 ++++++++ .../entities/moduleinstallation_test.go | 71 +++++++++++++++++++ internal/modulesv2/fake/installedmodules.go | 6 +- .../modulesv2/fake/moduleinstallationstate.go | 4 +- internal/modulesv2/list.go | 29 +++++--- internal/modulesv2/list_test.go | 49 +++++-------- .../modulesv2/repository/installedmodules.go | 12 ++-- .../repository/installedmodules_test.go | 4 +- .../repository/moduleinstallationstate.go | 17 ++--- 9 files changed, 157 insertions(+), 64 deletions(-) create mode 100644 internal/modulesv2/entities/moduleinstallation.go create mode 100644 internal/modulesv2/entities/moduleinstallation_test.go diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go new file mode 100644 index 000000000..177a5a57f --- /dev/null +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -0,0 +1,29 @@ +package entities + +import "github.com/kyma-project/cli.v3/internal/kube/kyma" + +type ModuleInstallation struct { + Name string + Version string + Channel string + ModuleState string + Managed *bool + CustomResourcePolicy string + Template kyma.ModuleStatus +} + +func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { + return &ModuleInstallation{ + Name: raw.Status.Name, + Version: raw.Status.Version, + Channel: raw.Status.Channel, + ModuleState: raw.Status.State, + Managed: raw.Spec.Managed, + CustomResourcePolicy: raw.Spec.CustomResourcePolicy, + Template: raw.Status, + } +} + +func (m *ModuleInstallation) IsManaged() bool { + return m.Managed == nil || *m.Managed +} diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go new file mode 100644 index 000000000..48959d177 --- /dev/null +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -0,0 +1,71 @@ +package entities + +import ( + "testing" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/stretchr/testify/require" +) + +func TestModuleInstallation_IsManaged_TrueWhenManagedIsNil(t *testing.T) { + m := ModuleInstallation{Managed: nil} + require.True(t, m.IsManaged()) +} + +func TestModuleInstallation_IsManaged_TrueWhenManagedIsTrue(t *testing.T) { + managed := true + m := ModuleInstallation{Managed: &managed} + require.True(t, m.IsManaged()) +} + +func TestModuleInstallation_IsManaged_FalseWhenManagedIsFalse(t *testing.T) { + managed := false + m := ModuleInstallation{Managed: &managed} + require.False(t, m.IsManaged()) +} + +func TestNewModuleInstallationFromRaw_MapsNameVersionChannel(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Status: kyma.ModuleStatus{Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.Equal(t, "api-gateway", m.Name) + require.Equal(t, "3.5.1", m.Version) + require.Equal(t, "regular", m.Channel) +} + +func TestNewModuleInstallationFromRaw_MapsModuleState(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Status: kyma.ModuleStatus{Name: "api-gateway", State: "Ready"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.Equal(t, "Ready", m.ModuleState) +} + +func TestNewModuleInstallationFromRaw_MapsManaged(t *testing.T) { + managed := false + raw := kyma.KymaModuleInfo{ + Spec: kyma.Module{Managed: &managed}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.NotNil(t, m.Managed) + require.False(t, *m.Managed) +} + +func TestNewModuleInstallationFromRaw_MapsCustomResourcePolicy(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Spec: kyma.Module{CustomResourcePolicy: "CreateAndDelete"}, + Status: kyma.ModuleStatus{Name: "api-gateway"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.Equal(t, "CreateAndDelete", m.CustomResourcePolicy) +} diff --git a/internal/modulesv2/fake/installedmodules.go b/internal/modulesv2/fake/installedmodules.go index 3cc69cf18..01f0ced1f 100644 --- a/internal/modulesv2/fake/installedmodules.go +++ b/internal/modulesv2/fake/installedmodules.go @@ -3,14 +3,14 @@ package fake import ( "context" - "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" ) type InstalledModulesRepository struct { - ListInstalledModulesResult []kyma.KymaModuleInfo + ListInstalledModulesResult []entities.ModuleInstallation ListInstalledModulesError error } -func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]kyma.KymaModuleInfo, error) { +func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]entities.ModuleInstallation, error) { return f.ListInstalledModulesResult, f.ListInstalledModulesError } diff --git a/internal/modulesv2/fake/moduleinstallationstate.go b/internal/modulesv2/fake/moduleinstallationstate.go index 19cf2a9cd..0d2ddce27 100644 --- a/internal/modulesv2/fake/moduleinstallationstate.go +++ b/internal/modulesv2/fake/moduleinstallationstate.go @@ -3,7 +3,7 @@ package fake import ( "context" - "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" ) type ModuleInstallationStateRepository struct { @@ -11,6 +11,6 @@ type ModuleInstallationStateRepository struct { GetInstallationStateError error } -func (f *ModuleInstallationStateRepository) GetInstallationState(_ context.Context, _ kyma.ModuleStatus, _ kyma.Module) (string, error) { +func (f *ModuleInstallationStateRepository) GetInstallationState(_ context.Context, _ entities.ModuleInstallation) (string, error) { return f.GetInstallationStateResult, f.GetInstallationStateError } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index add7f4692..6ebae68bc 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -4,11 +4,12 @@ import ( "context" "github.com/kyma-project/cli.v3/internal/modulesv2/dtos" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" "github.com/kyma-project/cli.v3/internal/modulesv2/repository" ) type ListService struct { - installedModulesRepository repository.InstalledModulesRepository + installedModulesRepository repository.InstalledModulesRepository installationStateRepository repository.ModuleInstallationStateRepository } @@ -27,21 +28,33 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { - installationState, err := s.installationStateRepository.GetInstallationState(ctx, module.Status, module.Spec) + installationState, err := s.resolveInstallationState(ctx, module) if err != nil { return nil, err } results = append(results, dtos.ListResult{ - Name: module.Status.Name, - Version: module.Status.Version, - Channel: module.Status.Channel, - ModuleState: module.Status.State, - Managed: module.Spec.Managed == nil || *module.Spec.Managed, - CustomResourcePolicy: module.Spec.CustomResourcePolicy, + Name: module.Name, + Version: module.Version, + Channel: module.Channel, + ModuleState: module.ModuleState, + Managed: module.IsManaged(), + CustomResourcePolicy: module.CustomResourcePolicy, InstallationState: installationState, }) } return results, nil } + +func (s *ListService) resolveInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { + if module.CustomResourcePolicy == "CreateAndDelete" { + return module.ModuleState, nil + } + + if !module.IsManaged() { + return module.ModuleState, nil + } + + return s.installationStateRepository.GetInstallationState(ctx, module) +} diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index a0eb666ac..7d4e4e999 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -4,14 +4,14 @@ import ( "context" "testing" - "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" modulesfake "github.com/kyma-project/cli.v3/internal/modulesv2/fake" "github.com/stretchr/testify/require" ) func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{}, + ListInstalledModulesResult: []entities.ModuleInstallation{}, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -23,9 +23,9 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { func TestListService_Run_ReturnsCoreModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - {Status: kyma.ModuleStatus{Name: "api-gateway"}}, - {Status: kyma.ModuleStatus{Name: "istio"}}, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway"}, + {Name: "istio"}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -40,8 +40,8 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - {Status: kyma.ModuleStatus{Name: "api-gateway", Version: "3.5.1", Channel: "regular", State: "Ready"}}, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready"}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -60,11 +60,8 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { func TestListService_Run_ReturnsManaged(t *testing.T) { managed := true installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - { - Spec: kyma.Module{Name: "api-gateway", Managed: &managed}, - Status: kyma.ModuleStatus{Name: "api-gateway"}, - }, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", Managed: &managed}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -80,11 +77,8 @@ func TestListService_Run_ReturnsManaged(t *testing.T) { func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - { - Spec: kyma.Module{Name: "api-gateway", Managed: nil}, - Status: kyma.ModuleStatus{Name: "api-gateway"}, - }, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", Managed: nil}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -99,11 +93,8 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { managed := false installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - { - Spec: kyma.Module{Name: "api-gateway", Managed: &managed}, - Status: kyma.ModuleStatus{Name: "api-gateway"}, - }, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", Managed: &managed}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -117,11 +108,8 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - { - Spec: kyma.Module{Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, - Status: kyma.ModuleStatus{Name: "api-gateway"}, - }, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, } svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) @@ -135,11 +123,8 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { func TestListService_Run_ReturnsInstallationState(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []kyma.KymaModuleInfo{ - { - Spec: kyma.Module{Name: "api-gateway"}, - Status: kyma.ModuleStatus{Name: "api-gateway"}, - }, + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway"}, }, } installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index a66cb5bd3..cab79e2cd 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -4,10 +4,11 @@ import ( "context" "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" ) type InstalledModulesRepository interface { - ListInstalledModules(ctx context.Context) ([]kyma.KymaModuleInfo, error) + ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) } type installedModulesRepository struct { @@ -18,21 +19,22 @@ func NewInstalledModulesRepository(kymaClient kyma.Interface) InstalledModulesRe return &installedModulesRepository{kymaClient: kymaClient} } -func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]kyma.KymaModuleInfo, error) { +func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) { kymaCR, err := r.kymaClient.GetDefaultKyma(ctx) if err != nil { return nil, err } - modules := make([]kyma.KymaModuleInfo, len(kymaCR.Status.Modules)) + modules := make([]entities.ModuleInstallation, len(kymaCR.Status.Modules)) for i, status := range kymaCR.Status.Modules { - modules[i] = kyma.KymaModuleInfo{Status: status} + raw := kyma.KymaModuleInfo{Status: status} for _, spec := range kymaCR.Spec.Modules { if spec.Name == status.Name { - modules[i].Spec = spec + raw.Spec = spec break } } + modules[i] = *entities.NewModuleInstallationFromRaw(raw) } return modules, nil } diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index b2c25fd82..d56e437dd 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -27,6 +27,6 @@ func TestInstalledModulesRepository_ListInstalledModules(t *testing.T) { require.NoError(t, err) require.Len(t, result, 2) - require.Equal(t, "api-gateway", result[0].Status.Name) - require.Equal(t, "istio", result[1].Status.Name) + require.Equal(t, "api-gateway", result[0].Name) + require.Equal(t, "istio", result[1].Name) } diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go index 8162da8d6..353504349 100644 --- a/internal/modulesv2/repository/moduleinstallationstate.go +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -5,13 +5,14 @@ import ( "github.com/kyma-project/cli.v3/internal/kube" "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" "github.com/pkg/errors" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) type ModuleInstallationStateRepository interface { - GetInstallationState(ctx context.Context, status kyma.ModuleStatus, spec kyma.Module) (string, error) + GetInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) } type moduleInstallationStateRepository struct { @@ -22,21 +23,13 @@ func NewModuleInstallationStateRepository(kubeClient kube.Client) ModuleInstalla return &moduleInstallationStateRepository{kubeClient: kubeClient} } -func (r *moduleInstallationStateRepository) GetInstallationState(ctx context.Context, status kyma.ModuleStatus, spec kyma.Module) (string, error) { - if spec.CustomResourcePolicy == "CreateAndDelete" { - return status.State, nil - } - - if spec.Managed != nil && !*spec.Managed { - return status.State, nil - } - - moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, status.Template.GetNamespace(), status.Template.GetName()) +func (r *moduleInstallationStateRepository) GetInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { + moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.Template.Template.GetNamespace(), module.Template.Template.GetName()) if err != nil { if apierrors.IsNotFound(err) { return "", nil } - return "", errors.Wrapf(err, "failed to get ModuleTemplate %s/%s", status.Template.GetNamespace(), status.Template.GetName()) + return "", errors.Wrapf(err, "failed to get ModuleTemplate %s/%s", module.Template.Template.GetNamespace(), module.Template.Template.GetName()) } return getResourceState(ctx, r.kubeClient, moduleTemplate.Spec.Manager)