From 012dab328cbdc860f4df340c474af599f114f5de Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 19:25:32 +0200 Subject: [PATCH 01/45] 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 a5d433f7381ef97323adb4aeb7cc670275af7013 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 19:38:16 +0200 Subject: [PATCH 02/45] 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 033079b085081f68a7599f4e59e2f47633027985 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:12:24 +0200 Subject: [PATCH 03/45] 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 4544dccf54882ea7df2972c9aafae1c9e536a84b Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:25:58 +0200 Subject: [PATCH 04/45] 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 35d387ba79d27ce0fe37d5f6e2182e80a613fedf Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:45:16 +0200 Subject: [PATCH 05/45] 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 a6de9d78316589f0febe7bf58f5ba42f6087be2a Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 20:56:23 +0200 Subject: [PATCH 06/45] 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 8a884d677ba27378937edb8045abb199828479f6 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:04:05 +0200 Subject: [PATCH 07/45] 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 d76f5b6f4551a35bc59cde0fddddf304c5b39fd0 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:05:58 +0200 Subject: [PATCH 08/45] 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 6341e66a62bcf4013fc241cb58f1746112847c0f Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:25:08 +0200 Subject: [PATCH 09/45] 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 96670399d92d10348ef569f095ca7e4389031188 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:31:07 +0200 Subject: [PATCH 10/45] 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 f22c323ea676a0915b3705fd2df4c265bbcab625 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:36:58 +0200 Subject: [PATCH 11/45] 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 240dadd2d6958f26e1e07468f2d731b4c46261f5 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:48:07 +0200 Subject: [PATCH 12/45] 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 14505c878f187d39fc42149979c5afde757f6c53 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:53:32 +0200 Subject: [PATCH 13/45] 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 8059e2f9ef364fb0f3c8c75c95c71a7e1a62c839 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 15 Apr 2026 21:59:26 +0200 Subject: [PATCH 14/45] 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 68c214ebf0cc2fa37dc163cd7a101194d4afca5c Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 09:15:28 +0200 Subject: [PATCH 15/45] 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 273becdf78f806f581eda87ee1dd4cebd6dbbd03 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:28:41 +0200 Subject: [PATCH 16/45] 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 19ed8425131cbd7a669de41a3616f0bfd5bc005a Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:40:17 +0200 Subject: [PATCH 17/45] 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 d6a3b2376be63c3ec1be0aa69bc250489baff3e3 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:45:16 +0200 Subject: [PATCH 18/45] 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 7d98767142aa051f2b4c7283935f640e08875713 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 11:49:30 +0200 Subject: [PATCH 19/45] 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 155c63dbbd466c10bb75411aee177938c4e88786 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:22:36 +0200 Subject: [PATCH 20/45] 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 e50e9c7c17102c1d45452e4fe427429e43a3f094 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:28:22 +0200 Subject: [PATCH 21/45] 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 2be1cc1a2975d7dd596efd2ef87beb6398545e54 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:39:43 +0200 Subject: [PATCH 22/45] 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 6eba781e487b7aab72a6851d9f384f0fe6af2dc2 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 12:55:03 +0200 Subject: [PATCH 23/45] 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 872ff1c6f087f41f7ae00b63777482eb9b7d9056 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 13:01:40 +0200 Subject: [PATCH 24/45] 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 c48165cece26e39a7f8805c09be388c4bf682329 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 13:18:43 +0200 Subject: [PATCH 25/45] 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 31b823cbf600ca33e125a9c6bc43b4455f93e12c Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 21:09:29 +0200 Subject: [PATCH 26/45] 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 a9b7f384495c96269206928f454f8ffd06d345d0 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 21:26:47 +0200 Subject: [PATCH 27/45] 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 7af17ad882fcb58767064a7cfbfd3da01cd0d7fe Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Fri, 17 Apr 2026 22:38:06 +0200 Subject: [PATCH 28/45] 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 ddd7143d4cd39afb152739a9a5a74d82f938662c Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 28 Apr 2026 13:53:13 +0200 Subject: [PATCH 29/45] 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) From 0fa1e4ff4be122a8f1c09fdda1e08f28f25f5ef1 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Thu, 14 May 2026 21:34:32 +0200 Subject: [PATCH 30/45] replace Template field with TemplateName and TemplateNamespace in ModuleInstallation --- internal/modulesv2/entities/moduleinstallation.go | 6 ++++-- .../modulesv2/entities/moduleinstallation_test.go | 14 ++++++++++++++ .../repository/moduleinstallationstate.go | 4 ++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go index 177a5a57f..4555ca287 100644 --- a/internal/modulesv2/entities/moduleinstallation.go +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -9,7 +9,8 @@ type ModuleInstallation struct { ModuleState string Managed *bool CustomResourcePolicy string - Template kyma.ModuleStatus + TemplateName string + TemplateNamespace string } func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { @@ -20,7 +21,8 @@ func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { ModuleState: raw.Status.State, Managed: raw.Spec.Managed, CustomResourcePolicy: raw.Spec.CustomResourcePolicy, - Template: raw.Status, + TemplateName: raw.Status.Template.GetName(), + TemplateNamespace: raw.Status.Template.GetNamespace(), } } diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go index 48959d177..10b4fb83f 100644 --- a/internal/modulesv2/entities/moduleinstallation_test.go +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -69,3 +69,17 @@ func TestNewModuleInstallationFromRaw_MapsCustomResourcePolicy(t *testing.T) { require.Equal(t, "CreateAndDelete", m.CustomResourcePolicy) } + +func TestNewModuleInstallationFromRaw_MapsTemplateNameAndNamespace(t *testing.T) { + template := kyma.ModuleStatus{ + Name: "api-gateway", + } + template.Template.SetName("api-gateway-template") + template.Template.SetNamespace("kyma-system") + raw := kyma.KymaModuleInfo{Status: template} + + m := NewModuleInstallationFromRaw(raw) + + require.Equal(t, "api-gateway-template", m.TemplateName) + require.Equal(t, "kyma-system", m.TemplateNamespace) +} diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go index 353504349..5f9dafb4f 100644 --- a/internal/modulesv2/repository/moduleinstallationstate.go +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -24,12 +24,12 @@ func NewModuleInstallationStateRepository(kubeClient kube.Client) ModuleInstalla } 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()) + moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.TemplateNamespace, module.TemplateName) if err != nil { if apierrors.IsNotFound(err) { return "", nil } - return "", errors.Wrapf(err, "failed to get ModuleTemplate %s/%s", module.Template.Template.GetNamespace(), module.Template.Template.GetName()) + return "", errors.Wrapf(err, "failed to get ModuleTemplate %s/%s", module.TemplateNamespace, module.TemplateName) } return getResourceState(ctx, r.kubeClient, moduleTemplate.Spec.Manager) From 36da7dacd2e2f7c99992a3b87e263662146b7ea2 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 12:18:44 +0200 Subject: [PATCH 31/45] merge spec and status when listing installed modules --- .../modulesv2/entities/moduleinstallation.go | 6 +- .../entities/moduleinstallation_test.go | 10 +++ .../modulesv2/repository/installedmodules.go | 33 +++++++--- .../repository/installedmodules_test.go | 63 +++++++++++++++++-- 4 files changed, 97 insertions(+), 15 deletions(-) diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go index 4555ca287..384b343c0 100644 --- a/internal/modulesv2/entities/moduleinstallation.go +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -14,8 +14,12 @@ type ModuleInstallation struct { } func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { + name := raw.Status.Name + if name == "" { + name = raw.Spec.Name + } return &ModuleInstallation{ - Name: raw.Status.Name, + Name: name, Version: raw.Status.Version, Channel: raw.Status.Channel, ModuleState: raw.Status.State, diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go index 10b4fb83f..2cd144395 100644 --- a/internal/modulesv2/entities/moduleinstallation_test.go +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -83,3 +83,13 @@ func TestNewModuleInstallationFromRaw_MapsTemplateNameAndNamespace(t *testing.T) require.Equal(t, "api-gateway-template", m.TemplateName) require.Equal(t, "kyma-system", m.TemplateNamespace) } + +func TestNewModuleInstallationFromRaw_UsesSpecNameWhenStatusNameIsEmpty(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Spec: kyma.Module{Name: "api-gateway"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.Equal(t, "api-gateway", m.Name) +} diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index cab79e2cd..786f19bb3 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -25,16 +25,33 @@ func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ( return nil, err } - modules := make([]entities.ModuleInstallation, len(kymaCR.Status.Modules)) - for i, status := range kymaCR.Status.Modules { + statusByName := make(map[string]kyma.ModuleStatus, len(kymaCR.Status.Modules)) + for _, status := range kymaCR.Status.Modules { + statusByName[status.Name] = status + } + + specByName := make(map[string]kyma.Module, len(kymaCR.Spec.Modules)) + for _, spec := range kymaCR.Spec.Modules { + specByName[spec.Name] = spec + } + + var modules []entities.ModuleInstallation + + for _, status := range kymaCR.Status.Modules { raw := kyma.KymaModuleInfo{Status: status} - for _, spec := range kymaCR.Spec.Modules { - if spec.Name == status.Name { - raw.Spec = spec - break - } + if spec, ok := specByName[status.Name]; ok { + raw.Spec = spec } - modules[i] = *entities.NewModuleInstallationFromRaw(raw) + modules = append(modules, *entities.NewModuleInstallationFromRaw(raw)) } + + for _, spec := range kymaCR.Spec.Modules { + if _, inStatus := statusByName[spec.Name]; inStatus { + continue + } + raw := kyma.KymaModuleInfo{Spec: spec} + modules = append(modules, *entities.NewModuleInstallationFromRaw(raw)) + } + return modules, nil } diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index d56e437dd..155fc25ca 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -10,13 +10,63 @@ import ( "github.com/stretchr/testify/require" ) -func TestInstalledModulesRepository_ListInstalledModules(t *testing.T) { +func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, + }, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{ + {Name: "api-gateway", State: "Ready"}, + }, + }, + }, + } + repo := repository.NewInstalledModulesRepository(kymaClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 1) + module := result[0] + require.Equal(t, "api-gateway", module.Name) + require.Equal(t, "Ready", module.ModuleState) + require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) +} + +func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *testing.T) { + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, + }, + }, + Status: kyma.KymaStatus{}, + }, + } + repo := repository.NewInstalledModulesRepository(kymaClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + require.Len(t, result, 1) + module := result[0] + require.Equal(t, "api-gateway", module.Name) + require.Equal(t, "", module.ModuleState) + require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) +} + +func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *testing.T) { + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: kyma.Kyma{ + Spec: kyma.KymaSpec{}, Status: kyma.KymaStatus{ Modules: []kyma.ModuleStatus{ - {Name: "api-gateway"}, - {Name: "istio"}, + {Name: "api-gateway", State: "Deleting"}, }, }, }, @@ -26,7 +76,8 @@ func TestInstalledModulesRepository_ListInstalledModules(t *testing.T) { 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) + require.Len(t, result, 1) + module := result[0] + require.Equal(t, "api-gateway", module.Name) + require.Equal(t, "Deleting", module.ModuleState) } From 37f85965c5ec6cf90a7e640d1f8e738640cf1ac6 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 21:24:24 +0200 Subject: [PATCH 32/45] add IsBeingDeleted to ModuleInstallation, handle deleting state in resolveInstallationState --- .../modulesv2/entities/moduleinstallation.go | 8 +++++++ .../entities/moduleinstallation_test.go | 21 +++++++++++++++++++ internal/modulesv2/list.go | 4 ++++ internal/modulesv2/list_test.go | 20 ++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go index 384b343c0..1d58b371b 100644 --- a/internal/modulesv2/entities/moduleinstallation.go +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -11,6 +11,8 @@ type ModuleInstallation struct { CustomResourcePolicy string TemplateName string TemplateNamespace string + specModuleName string + statusModuleName string } func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { @@ -27,9 +29,15 @@ func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { CustomResourcePolicy: raw.Spec.CustomResourcePolicy, TemplateName: raw.Status.Template.GetName(), TemplateNamespace: raw.Status.Template.GetNamespace(), + specModuleName: raw.Spec.Name, + statusModuleName: raw.Status.Name, } } func (m *ModuleInstallation) IsManaged() bool { return m.Managed == nil || *m.Managed } + +func (m *ModuleInstallation) IsBeingDeleted() bool { + return m.statusModuleName != "" && m.specModuleName == "" +} diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go index 2cd144395..a66efcc76 100644 --- a/internal/modulesv2/entities/moduleinstallation_test.go +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -93,3 +93,24 @@ func TestNewModuleInstallationFromRaw_UsesSpecNameWhenStatusNameIsEmpty(t *testi require.Equal(t, "api-gateway", m.Name) } + +func TestModuleInstallation_IsBeingDeleted_TrueWhenOnlyInStatus(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Status: kyma.ModuleStatus{Name: "api-gateway", State: "Deleting"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.True(t, m.IsBeingDeleted()) +} + +func TestModuleInstallation_IsBeingDeleted_FalseWhenSpecPresent(t *testing.T) { + raw := kyma.KymaModuleInfo{ + Spec: kyma.Module{Name: "api-gateway"}, + Status: kyma.ModuleStatus{Name: "api-gateway", State: "Ready"}, + } + + m := NewModuleInstallationFromRaw(raw) + + require.False(t, m.IsBeingDeleted()) +} diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 6ebae68bc..21dd3eafe 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -48,6 +48,10 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { } func (s *ListService) resolveInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { + if module.IsBeingDeleted() { + return module.ModuleState, nil + } + if module.CustomResourcePolicy == "CreateAndDelete" { return module.ModuleState, nil } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 7d4e4e999..1fb131be9 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -4,6 +4,7 @@ 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" @@ -138,3 +139,22 @@ func TestListService_Run_ReturnsInstallationState(t *testing.T) { module := result[0] require.Equal(t, "Ready", module.InstallationState) } + +func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingDeleted(t *testing.T) { + deletingModule := entities.NewModuleInstallationFromRaw(kyma.KymaModuleInfo{ + Status: kyma.ModuleStatus{Name: "api-gateway", State: "Deleting"}, + }) + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []entities.ModuleInstallation{*deletingModule}, + } + 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, "Deleting", module.InstallationState) +} From 72d0cd83ce918e332c64c18e7e2807725a66d4ef Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 21:53:15 +0200 Subject: [PATCH 33/45] add ModuleCRStateRepository to fetch module state from CR --- internal/modulesv2/dependencies.go | 16 +++- internal/modulesv2/fake/modulecrstate.go | 16 ++++ internal/modulesv2/list.go | 11 ++- internal/modulesv2/list_test.go | 39 ++++++--- .../modulesv2/repository/modulecrstate.go | 86 +++++++++++++++++++ .../repository/modulecrstate_test.go | 55 ++++++++++++ 6 files changed, 210 insertions(+), 13 deletions(-) create mode 100644 internal/modulesv2/fake/modulecrstate.go create mode 100644 internal/modulesv2/repository/modulecrstate.go create mode 100644 internal/modulesv2/repository/modulecrstate_test.go diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index a90fcce06..2cc066111 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -108,6 +108,15 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return repository.NewModuleInstallationStateRepository(kubeClient), nil }) + di.RegisterTyped(container, func(c *di.Container) (repository.ModuleCRStateRepository, error) { + kubeClient, err := di.GetTyped[kube.Client](c) + if err != nil { + return nil, err + } + + return repository.NewModuleCRStateRepository(kubeClient), nil + }) + // Services: di.RegisterTyped(container, func(c *di.Container) (*CatalogService, error) { @@ -144,7 +153,12 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - return NewListService(installedModulesRepo, installationStateRepo), nil + moduleCRStateRepo, err := di.GetTyped[repository.ModuleCRStateRepository](c) + if err != nil { + return nil, err + } + + return NewListService(installedModulesRepo, installationStateRepo, moduleCRStateRepo), nil }) return container diff --git a/internal/modulesv2/fake/modulecrstate.go b/internal/modulesv2/fake/modulecrstate.go new file mode 100644 index 000000000..8a32402c3 --- /dev/null +++ b/internal/modulesv2/fake/modulecrstate.go @@ -0,0 +1,16 @@ +package fake + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" +) + +type ModuleCRStateRepository struct { + GetModuleCRStateResult string + GetModuleCRStateError error +} + +func (f *ModuleCRStateRepository) GetModuleCRState(_ context.Context, _ entities.ModuleInstallation) (string, error) { + return f.GetModuleCRStateResult, f.GetModuleCRStateError +} diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 21dd3eafe..36ce86beb 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -11,12 +11,14 @@ import ( type ListService struct { installedModulesRepository repository.InstalledModulesRepository installationStateRepository repository.ModuleInstallationStateRepository + moduleCRStateRepository repository.ModuleCRStateRepository } -func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository) *ListService { +func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository, moduleCRStateRepository repository.ModuleCRStateRepository) *ListService { return &ListService{ installedModulesRepository: installedModulesRepository, installationStateRepository: installationStateRepository, + moduleCRStateRepository: moduleCRStateRepository, } } @@ -33,11 +35,16 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { return nil, err } + moduleState, err := s.moduleCRStateRepository.GetModuleCRState(ctx, module) + if err != nil { + return nil, err + } + results = append(results, dtos.ListResult{ Name: module.Name, Version: module.Version, Channel: module.Channel, - ModuleState: module.ModuleState, + ModuleState: moduleState, Managed: module.IsManaged(), CustomResourcePolicy: module.CustomResourcePolicy, InstallationState: installationState, diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 1fb131be9..1a22407bc 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -14,7 +14,7 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{}, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -29,7 +29,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { {Name: "istio"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -42,10 +42,11 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + moduleCRStateRepo := &modulesfake.ModuleCRStateRepository{GetModuleCRStateResult: "Ready"} + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, moduleCRStateRepo) result, err := svc.Run(context.Background()) @@ -65,7 +66,7 @@ func TestListService_Run_ReturnsManaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -82,7 +83,7 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { {Name: "api-gateway", Managed: nil}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -98,7 +99,7 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -113,7 +114,7 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -131,7 +132,7 @@ func TestListService_Run_ReturnsInstallationState(t *testing.T) { installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ GetInstallationStateResult: "Ready", } - svc := NewListService(installedModulesRepo, installationStateRepo) + svc := NewListService(installedModulesRepo, installationStateRepo, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -150,7 +151,7 @@ func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingD installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ GetInstallationStateResult: "Ready", } - svc := NewListService(installedModulesRepo, installationStateRepo) + svc := NewListService(installedModulesRepo, installationStateRepo, &modulesfake.ModuleCRStateRepository{}) result, err := svc.Run(context.Background()) @@ -158,3 +159,21 @@ func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingD module := result[0] require.Equal(t, "Deleting", module.InstallationState) } + +func TestListService_Run_ReturnsModuleStateFromCR(t *testing.T) { + installedModulesRepo := &modulesfake.InstalledModulesRepository{ + ListInstalledModulesResult: []entities.ModuleInstallation{ + {Name: "api-gateway", ModuleState: "Ready"}, + }, + } + moduleCRStateRepo := &modulesfake.ModuleCRStateRepository{ + GetModuleCRStateResult: "Warning", + } + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, moduleCRStateRepo) + + result, err := svc.Run(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Warning", module.ModuleState) +} diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go new file mode 100644 index 000000000..9c86b7822 --- /dev/null +++ b/internal/modulesv2/repository/modulecrstate.go @@ -0,0 +1,86 @@ +package repository + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube" + "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" + "github.com/kyma-project/cli.v3/internal/modulesv2/entities" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type ModuleCRStateRepository interface { + GetModuleCRState(ctx context.Context, module entities.ModuleInstallation) (string, error) +} + +type moduleCRStateRepository struct { + kubeClient kube.Client +} + +func NewModuleCRStateRepository(kubeClient kube.Client) ModuleCRStateRepository { + return &moduleCRStateRepository{kubeClient: kubeClient} +} + +func (r *moduleCRStateRepository) GetModuleCRState(ctx context.Context, module entities.ModuleInstallation) (string, error) { + moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.TemplateNamespace, module.TemplateName) + if err != nil { + if apierrors.IsNotFound(err) { + return "", nil + } + return "", err + } + + data := moduleTemplate.Spec.Data + if len(data.Object) == 0 { + return "", nil + } + + crList, err := r.kubeClient.RootlessDynamic().List(ctx, &unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": data.GetAPIVersion(), + "kind": data.GetKind(), + }, + }, &rootlessdynamic.ListOptions{AllNamespaces: true}) + if err != nil { + if apierrors.IsNotFound(err) { + return "", nil + } + return "", err + } + + return highestStateFromList(crList.Items), nil +} + +func highestStateFromList(items []unstructured.Unstructured) string { + state := "" + for _, item := range items { + statusRaw, ok := item.Object["status"] + if !ok || statusRaw == nil { + continue + } + status := statusRaw.(map[string]any) + if crState, ok := status["state"].(string); ok { + state = highestState(state, crState) + } + } + return state +} + +var statesPrecedence = []string{"Ready", "Processing", "Deleting", "Error", "Warning"} + +func highestState(a, b string) string { + for _, s := range statesPrecedence { + if a == s { + return a + } + if b == s { + return b + } + } + if a == "" { + return b + } + return a +} + diff --git a/internal/modulesv2/repository/modulecrstate_test.go b/internal/modulesv2/repository/modulecrstate_test.go new file mode 100644 index 000000000..64d945520 --- /dev/null +++ b/internal/modulesv2/repository/modulecrstate_test.go @@ -0,0 +1,55 @@ +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/entities" + "github.com/kyma-project/cli.v3/internal/modulesv2/repository" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestModuleCRStateRepository_GetModuleCRState_ReturnsStateFromCR(t *testing.T) { + moduleTemplate := kyma.ModuleTemplate{} + moduleTemplate.Spec.Data = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "APIGateway", + }, + } + + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "state": "Warning", + }, + }, + }, + }, + } + + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: &kubefake.KymaClient{ + ReturnModuleTemplate: moduleTemplate, + }, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnListObjs: crList, + }, + } + + module := entities.ModuleInstallation{ + TemplateName: "api-gateway-template", + TemplateNamespace: "kyma-system", + } + repo := repository.NewModuleCRStateRepository(kubeClient) + + state, err := repo.GetModuleCRState(context.Background(), module) + + require.NoError(t, err) + require.Equal(t, "Warning", state) +} From 8c01c1173575167ea936194ec2fc50413a17e1d5 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 22:08:33 +0200 Subject: [PATCH 34/45] handle unmanaged modules in ModuleCRStateRepository --- .../modulesv2/repository/modulecrstate.go | 33 +++++++++++-- .../repository/modulecrstate_test.go | 48 +++++++++++++++++++ 2 files changed, 77 insertions(+), 4 deletions(-) diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go index 9c86b7822..fa557d62c 100644 --- a/internal/modulesv2/repository/modulecrstate.go +++ b/internal/modulesv2/repository/modulecrstate.go @@ -4,6 +4,7 @@ import ( "context" "github.com/kyma-project/cli.v3/internal/kube" + "github.com/kyma-project/cli.v3/internal/kube/kyma" "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" "github.com/kyma-project/cli.v3/internal/modulesv2/entities" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -23,13 +24,13 @@ func NewModuleCRStateRepository(kubeClient kube.Client) ModuleCRStateRepository } func (r *moduleCRStateRepository) GetModuleCRState(ctx context.Context, module entities.ModuleInstallation) (string, error) { - moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.TemplateNamespace, module.TemplateName) + moduleTemplate, err := r.findModuleTemplate(ctx, module) if err != nil { - if apierrors.IsNotFound(err) { - return "", nil - } return "", err } + if moduleTemplate == nil { + return "", nil + } data := moduleTemplate.Spec.Data if len(data.Object) == 0 { @@ -52,6 +53,30 @@ func (r *moduleCRStateRepository) GetModuleCRState(ctx context.Context, module e return highestStateFromList(crList.Items), nil } +func (r *moduleCRStateRepository) findModuleTemplate(ctx context.Context, module entities.ModuleInstallation) (*kyma.ModuleTemplate, error) { + if module.TemplateName != "" { + mt, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.TemplateNamespace, module.TemplateName) + if err != nil { + if apierrors.IsNotFound(err) { + return nil, nil + } + return nil, err + } + return mt, nil + } + + templates, err := r.kubeClient.Kyma().ListModuleTemplate(ctx) + if err != nil { + return nil, err + } + for i, mt := range templates.Items { + if mt.Spec.ModuleName == module.Name && mt.Spec.Version == module.Version { + return &templates.Items[i], nil + } + } + return nil, nil +} + func highestStateFromList(items []unstructured.Unstructured) string { state := "" for _, item := range items { diff --git a/internal/modulesv2/repository/modulecrstate_test.go b/internal/modulesv2/repository/modulecrstate_test.go index 64d945520..33b6a829f 100644 --- a/internal/modulesv2/repository/modulecrstate_test.go +++ b/internal/modulesv2/repository/modulecrstate_test.go @@ -53,3 +53,51 @@ func TestModuleCRStateRepository_GetModuleCRState_ReturnsStateFromCR(t *testing. require.NoError(t, err) require.Equal(t, "Warning", state) } + +func TestModuleCRStateRepository_GetModuleCRState_UnmanagedModule_FindsTemplateByNameAndVersion(t *testing.T) { + matchingTemplate := kyma.ModuleTemplate{} + matchingTemplate.Spec.ModuleName = "api-gateway" + matchingTemplate.Spec.Version = "3.5.1" + matchingTemplate.Spec.Data = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "APIGateway", + }, + } + + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + { + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "state": "Ready", + }, + }, + }, + }, + } + + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: &kubefake.KymaClient{ + ReturnModuleTemplateList: kyma.ModuleTemplateList{ + Items: []kyma.ModuleTemplate{matchingTemplate}, + }, + }, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnListObjs: crList, + }, + } + + managed := false + module := entities.ModuleInstallation{ + Name: "api-gateway", + Version: "3.5.1", + Managed: &managed, + } + repo := repository.NewModuleCRStateRepository(kubeClient) + + state, err := repo.GetModuleCRState(context.Background(), module) + + require.NoError(t, err) + require.Equal(t, "Ready", state) +} From 3e0b3fb601395faa009e5c1bd285c62dac8edef0 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 22:15:56 +0200 Subject: [PATCH 35/45] log discovery errors instead of propagating them in ModuleCRStateRepository --- .../modulesv2/repository/modulecrstate.go | 9 +++++- .../repository/modulecrstate_test.go | 31 +++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go index fa557d62c..fc808e25a 100644 --- a/internal/modulesv2/repository/modulecrstate.go +++ b/internal/modulesv2/repository/modulecrstate.go @@ -2,11 +2,13 @@ package repository import ( "context" + "strings" "github.com/kyma-project/cli.v3/internal/kube" "github.com/kyma-project/cli.v3/internal/kube/kyma" "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" "github.com/kyma-project/cli.v3/internal/modulesv2/entities" + "github.com/kyma-project/cli.v3/internal/out" apierrors "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) @@ -44,7 +46,8 @@ func (r *moduleCRStateRepository) GetModuleCRState(ctx context.Context, module e }, }, &rootlessdynamic.ListOptions{AllNamespaces: true}) if err != nil { - if apierrors.IsNotFound(err) { + if apierrors.IsNotFound(err) || isDiscoveryError(err) { + out.Debugfln("failed to get CR state for module %s: %v", module.Name, err) return "", nil } return "", err @@ -94,6 +97,10 @@ func highestStateFromList(items []unstructured.Unstructured) string { var statesPrecedence = []string{"Ready", "Processing", "Deleting", "Error", "Warning"} +func isDiscoveryError(err error) bool { + return strings.Contains(err.Error(), "failed to discover API resource using discovery client") +} + func highestState(a, b string) string { for _, s := range statesPrecedence { if a == s { diff --git a/internal/modulesv2/repository/modulecrstate_test.go b/internal/modulesv2/repository/modulecrstate_test.go index 33b6a829f..776a628d0 100644 --- a/internal/modulesv2/repository/modulecrstate_test.go +++ b/internal/modulesv2/repository/modulecrstate_test.go @@ -2,6 +2,7 @@ package repository_test import ( "context" + "fmt" "testing" kubefake "github.com/kyma-project/cli.v3/internal/kube/fake" @@ -101,3 +102,33 @@ func TestModuleCRStateRepository_GetModuleCRState_UnmanagedModule_FindsTemplateB require.NoError(t, err) require.Equal(t, "Ready", state) } + +func TestModuleCRStateRepository_GetModuleCRState_ReturnsEmptyOnDiscoveryError(t *testing.T) { + moduleTemplate := kyma.ModuleTemplate{} + moduleTemplate.Spec.Data = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "Eventing", + }, + } + + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: &kubefake.KymaClient{ + ReturnModuleTemplate: moduleTemplate, + }, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnErr: fmt.Errorf("failed to discover API resource using discovery client: resource 'Eventing' in group 'operator.kyma-project.io', and version 'v1alpha1' not registered on cluster"), + }, + } + + module := entities.ModuleInstallation{ + TemplateName: "eventing-template", + TemplateNamespace: "kyma-system", + } + repo := repository.NewModuleCRStateRepository(kubeClient) + + state, err := repo.GetModuleCRState(context.Background(), module) + + require.NoError(t, err) + require.Equal(t, "", state) +} From 640e4c51321832cb82d5405669e3573057362a59 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 19 May 2026 22:25:03 +0200 Subject: [PATCH 36/45] simplify installationStatus rendering - show InstallationState directly --- internal/modulesv2/render.go | 3 --- internal/modulesv2/render_test.go | 5 +++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/internal/modulesv2/render.go b/internal/modulesv2/render.go index b447322db..9cf0f6d24 100644 --- a/internal/modulesv2/render.go +++ b/internal/modulesv2/render.go @@ -83,9 +83,6 @@ func convertListToRows(results []dtos.ListResult) [][]interface{} { } func installationStatus(r dtos.ListResult) string { - 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 f9920663c..8569f4f52 100644 --- a/internal/modulesv2/render_test.go +++ b/internal/modulesv2/render_test.go @@ -49,7 +49,7 @@ func TestRenderList_Table_SortedByName(t *testing.T) { require.Regexp(t, `(?s)api-gateway.*istio`, buf.String()) } -func TestRenderList_Table_CombinedInstallationStatus(t *testing.T) { +func TestRenderList_Table_ShowsInstallationStateDirectly(t *testing.T) { results := []dtos.ListResult{ {Name: "nats", ModuleState: "Warning", InstallationState: "Deleting"}, } @@ -58,7 +58,8 @@ func TestRenderList_Table_CombinedInstallationStatus(t *testing.T) { err := RenderList(results, types.DefaultFormat, out.NewToWriter(&buf)) require.NoError(t, err) - require.Regexp(t, `nats.*Warning\(Deleting\)`, buf.String()) + require.NotRegexp(t, `Warning\(Deleting\)`, buf.String()) + require.Regexp(t, `nats.*Warning.*Deleting`, buf.String()) } func TestRenderList_YAML(t *testing.T) { From 5a3b538dcfbf51720beae0efc02ca7a9539c0bbc Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 27 May 2026 12:29:00 +0200 Subject: [PATCH 37/45] extract extractStateFromObject, reuse in highestStateFromList --- .../modulesv2/repository/modulecrstate.go | 12 +--- .../repository/moduleinstallationstate.go | 18 +++-- .../moduleinstallationstate_test.go | 68 +++++++++++++++++++ 3 files changed, 82 insertions(+), 16 deletions(-) create mode 100644 internal/modulesv2/repository/moduleinstallationstate_test.go diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go index fc808e25a..e78fe1d6d 100644 --- a/internal/modulesv2/repository/modulecrstate.go +++ b/internal/modulesv2/repository/modulecrstate.go @@ -82,15 +82,9 @@ func (r *moduleCRStateRepository) findModuleTemplate(ctx context.Context, module func highestStateFromList(items []unstructured.Unstructured) string { state := "" - for _, item := range items { - statusRaw, ok := item.Object["status"] - if !ok || statusRaw == nil { - continue - } - status := statusRaw.(map[string]any) - if crState, ok := status["state"].(string); ok { - state = highestState(state, crState) - } + for i := range items { + crState := extractStateFromObject(&items[i]) + state = highestState(state, crState) } return state } diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go index 5f9dafb4f..dedd10bd8 100644 --- a/internal/modulesv2/repository/moduleinstallationstate.go +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -64,27 +64,31 @@ func getResourceState(ctx context.Context, client kube.Client, manager *kyma.Man return "", err } - statusRaw, ok := result.Object["status"] + return extractStateFromObject(result), nil +} + +func extractStateFromObject(obj *unstructured.Unstructured) string { + statusRaw, ok := obj.Object["status"] if !ok || statusRaw == nil { - return "", nil + return "" } status := statusRaw.(map[string]any) if state, ok := status["state"]; ok { - return state.(string), nil + return state.(string) } if conditions, ok := status["conditions"]; ok { - return getStateFromConditions(conditions.([]any)), nil + return getStateFromConditions(conditions.([]any)) } if readyReplicas, ok := status["readyReplicas"]; ok { - spec := result.Object["spec"].(map[string]any) + spec := obj.Object["spec"].(map[string]any) if wantedReplicas, ok := spec["replicas"]; ok { - return resolveStateFromReplicas(readyReplicas.(int64), wantedReplicas.(int64)), nil + return resolveStateFromReplicas(readyReplicas.(int64), wantedReplicas.(int64)) } } - return "", nil + return "" } func getStateFromConditions(conditions []interface{}) string { diff --git a/internal/modulesv2/repository/moduleinstallationstate_test.go b/internal/modulesv2/repository/moduleinstallationstate_test.go new file mode 100644 index 000000000..01f0320a5 --- /dev/null +++ b/internal/modulesv2/repository/moduleinstallationstate_test.go @@ -0,0 +1,68 @@ +package repository + +import ( + "testing" + + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +func TestExtractStateFromObject_ReturnsStateField(t *testing.T) { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "state": "Ready", + }, + }, + } + + state := extractStateFromObject(obj) + + require.Equal(t, "Ready", state) +} + +func TestExtractStateFromObject_ReturnsStateFromConditions(t *testing.T) { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "conditions": []interface{}{ + map[string]interface{}{ + "type": "Available", + "status": "True", + }, + }, + }, + }, + } + + state := extractStateFromObject(obj) + + require.Equal(t, "Ready", state) +} + +func TestExtractStateFromObject_ReturnsStateFromReplicas(t *testing.T) { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{ + "spec": map[string]interface{}{ + "replicas": int64(3), + }, + "status": map[string]interface{}{ + "readyReplicas": int64(3), + }, + }, + } + + state := extractStateFromObject(obj) + + require.Equal(t, "Ready", state) +} + +func TestExtractStateFromObject_ReturnsEmptyWhenNoStatus(t *testing.T) { + obj := &unstructured.Unstructured{ + Object: map[string]interface{}{}, + } + + state := extractStateFromObject(obj) + + require.Equal(t, "", state) +} From 9cfd6cfce621f5ca6cbcb9c55d446c1a8155f988 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 27 May 2026 13:03:59 +0200 Subject: [PATCH 38/45] move state extraction helpers to stateextractor.go --- .../repository/moduleinstallationstate.go | 50 ----------------- .../modulesv2/repository/stateextractor.go | 53 +++++++++++++++++++ 2 files changed, 53 insertions(+), 50 deletions(-) create mode 100644 internal/modulesv2/repository/stateextractor.go diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go index dedd10bd8..ec62adeaf 100644 --- a/internal/modulesv2/repository/moduleinstallationstate.go +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -66,53 +66,3 @@ func getResourceState(ctx context.Context, client kube.Client, manager *kyma.Man return extractStateFromObject(result), nil } - -func extractStateFromObject(obj *unstructured.Unstructured) string { - statusRaw, ok := obj.Object["status"] - if !ok || statusRaw == nil { - return "" - } - status := statusRaw.(map[string]any) - if state, ok := status["state"]; ok { - return state.(string) - } - - if conditions, ok := status["conditions"]; ok { - return getStateFromConditions(conditions.([]any)) - } - - if readyReplicas, ok := status["readyReplicas"]; ok { - spec := obj.Object["spec"].(map[string]any) - if wantedReplicas, ok := spec["replicas"]; ok { - return resolveStateFromReplicas(readyReplicas.(int64), wantedReplicas.(int64)) - } - } - - return "" -} - -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" -} diff --git a/internal/modulesv2/repository/stateextractor.go b/internal/modulesv2/repository/stateextractor.go new file mode 100644 index 000000000..9966af3cd --- /dev/null +++ b/internal/modulesv2/repository/stateextractor.go @@ -0,0 +1,53 @@ +package repository + +import "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + +func extractStateFromObject(obj *unstructured.Unstructured) string { + statusRaw, ok := obj.Object["status"] + if !ok || statusRaw == nil { + return "" + } + status := statusRaw.(map[string]any) + if state, ok := status["state"]; ok { + return state.(string) + } + + if conditions, ok := status["conditions"]; ok { + return getStateFromConditions(conditions.([]any)) + } + + if readyReplicas, ok := status["readyReplicas"]; ok { + spec := obj.Object["spec"].(map[string]any) + if wantedReplicas, ok := spec["replicas"]; ok { + return resolveStateFromReplicas(readyReplicas.(int64), wantedReplicas.(int64)) + } + } + + return "" +} + +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 7961b609dde8e738410993f1e77e297dc1b28a5d Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Thu, 28 May 2026 16:48:35 +0200 Subject: [PATCH 39/45] move ModuleCRState fetching to InstalledModulesRepository, add KymaModuleState field --- internal/modulesv2/dependencies.go | 14 ++++----- .../modulesv2/entities/moduleinstallation.go | 3 +- .../entities/moduleinstallation_test.go | 2 +- internal/modulesv2/fake/modulecrstate.go | 16 ---------- internal/modulesv2/list.go | 17 ++++------- internal/modulesv2/list_test.go | 28 ++++++++--------- .../modulesv2/repository/installedmodules.go | 30 +++++++++++++++---- .../repository/installedmodules_test.go | 15 ++++++++-- 8 files changed, 63 insertions(+), 62 deletions(-) delete mode 100644 internal/modulesv2/fake/modulecrstate.go diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index 2cc066111..e9f7bd5de 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -96,7 +96,12 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - return repository.NewInstalledModulesRepository(kubeClient.Kyma()), nil + moduleCRStateRepo, err := di.GetTyped[repository.ModuleCRStateRepository](c) + if err != nil { + return nil, err + } + + return repository.NewInstalledModulesRepository(kubeClient.Kyma(), moduleCRStateRepo), nil }) di.RegisterTyped(container, func(c *di.Container) (repository.ModuleInstallationStateRepository, error) { @@ -153,12 +158,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - moduleCRStateRepo, err := di.GetTyped[repository.ModuleCRStateRepository](c) - if err != nil { - return nil, err - } - - return NewListService(installedModulesRepo, installationStateRepo, moduleCRStateRepo), nil + return NewListService(installedModulesRepo, installationStateRepo), nil }) return container diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go index 1d58b371b..a257acc3b 100644 --- a/internal/modulesv2/entities/moduleinstallation.go +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -6,6 +6,7 @@ type ModuleInstallation struct { Name string Version string Channel string + KymaModuleState string ModuleState string Managed *bool CustomResourcePolicy string @@ -24,7 +25,7 @@ func NewModuleInstallationFromRaw(raw kyma.KymaModuleInfo) *ModuleInstallation { Name: name, Version: raw.Status.Version, Channel: raw.Status.Channel, - ModuleState: raw.Status.State, + KymaModuleState: raw.Status.State, Managed: raw.Spec.Managed, CustomResourcePolicy: raw.Spec.CustomResourcePolicy, TemplateName: raw.Status.Template.GetName(), diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go index a66efcc76..f0d81f5fd 100644 --- a/internal/modulesv2/entities/moduleinstallation_test.go +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -43,7 +43,7 @@ func TestNewModuleInstallationFromRaw_MapsModuleState(t *testing.T) { m := NewModuleInstallationFromRaw(raw) - require.Equal(t, "Ready", m.ModuleState) + require.Equal(t, "Ready", m.KymaModuleState) } func TestNewModuleInstallationFromRaw_MapsManaged(t *testing.T) { diff --git a/internal/modulesv2/fake/modulecrstate.go b/internal/modulesv2/fake/modulecrstate.go deleted file mode 100644 index 8a32402c3..000000000 --- a/internal/modulesv2/fake/modulecrstate.go +++ /dev/null @@ -1,16 +0,0 @@ -package fake - -import ( - "context" - - "github.com/kyma-project/cli.v3/internal/modulesv2/entities" -) - -type ModuleCRStateRepository struct { - GetModuleCRStateResult string - GetModuleCRStateError error -} - -func (f *ModuleCRStateRepository) GetModuleCRState(_ context.Context, _ entities.ModuleInstallation) (string, error) { - return f.GetModuleCRStateResult, f.GetModuleCRStateError -} diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 36ce86beb..85f01d85a 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -11,14 +11,12 @@ import ( type ListService struct { installedModulesRepository repository.InstalledModulesRepository installationStateRepository repository.ModuleInstallationStateRepository - moduleCRStateRepository repository.ModuleCRStateRepository } -func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository, moduleCRStateRepository repository.ModuleCRStateRepository) *ListService { +func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository) *ListService { return &ListService{ installedModulesRepository: installedModulesRepository, installationStateRepository: installationStateRepository, - moduleCRStateRepository: moduleCRStateRepository, } } @@ -35,16 +33,11 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { return nil, err } - moduleState, err := s.moduleCRStateRepository.GetModuleCRState(ctx, module) - if err != nil { - return nil, err - } - results = append(results, dtos.ListResult{ Name: module.Name, Version: module.Version, Channel: module.Channel, - ModuleState: moduleState, + ModuleState: module.ModuleState, Managed: module.IsManaged(), CustomResourcePolicy: module.CustomResourcePolicy, InstallationState: installationState, @@ -56,15 +49,15 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { func (s *ListService) resolveInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { if module.IsBeingDeleted() { - return module.ModuleState, nil + return module.KymaModuleState, nil } if module.CustomResourcePolicy == "CreateAndDelete" { - return module.ModuleState, nil + return module.KymaModuleState, nil } if !module.IsManaged() { - return module.ModuleState, nil + return module.KymaModuleState, nil } return s.installationStateRepository.GetInstallationState(ctx, module) diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 1a22407bc..3b08b19d1 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -14,7 +14,7 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{}, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -29,7 +29,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { {Name: "istio"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -42,11 +42,10 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready"}, }, } - moduleCRStateRepo := &modulesfake.ModuleCRStateRepository{GetModuleCRStateResult: "Ready"} - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, moduleCRStateRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -66,7 +65,7 @@ func TestListService_Run_ReturnsManaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -83,7 +82,7 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { {Name: "api-gateway", Managed: nil}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -99,7 +98,7 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -114,7 +113,7 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) @@ -132,7 +131,7 @@ func TestListService_Run_ReturnsInstallationState(t *testing.T) { installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ GetInstallationStateResult: "Ready", } - svc := NewListService(installedModulesRepo, installationStateRepo, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, installationStateRepo) result, err := svc.Run(context.Background()) @@ -151,7 +150,7 @@ func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingD installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ GetInstallationStateResult: "Ready", } - svc := NewListService(installedModulesRepo, installationStateRepo, &modulesfake.ModuleCRStateRepository{}) + svc := NewListService(installedModulesRepo, installationStateRepo) result, err := svc.Run(context.Background()) @@ -163,13 +162,10 @@ func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingD func TestListService_Run_ReturnsModuleStateFromCR(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", ModuleState: "Ready"}, + {Name: "api-gateway", ModuleState: "Warning"}, }, } - moduleCRStateRepo := &modulesfake.ModuleCRStateRepository{ - GetModuleCRStateResult: "Warning", - } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}, moduleCRStateRepo) + svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) result, err := svc.Run(context.Background()) diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index 786f19bb3..486c346fc 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -12,11 +12,12 @@ type InstalledModulesRepository interface { } type installedModulesRepository struct { - kymaClient kyma.Interface + kymaClient kyma.Interface + moduleCRStateRepo ModuleCRStateRepository } -func NewInstalledModulesRepository(kymaClient kyma.Interface) InstalledModulesRepository { - return &installedModulesRepository{kymaClient: kymaClient} +func NewInstalledModulesRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository) InstalledModulesRepository { + return &installedModulesRepository{kymaClient: kymaClient, moduleCRStateRepo: moduleCRStateRepo} } func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) { @@ -42,16 +43,33 @@ func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ( if spec, ok := specByName[status.Name]; ok { raw.Spec = spec } - modules = append(modules, *entities.NewModuleInstallationFromRaw(raw)) + module, err := r.buildModule(ctx, raw) + if err != nil { + return nil, err + } + modules = append(modules, module) } for _, spec := range kymaCR.Spec.Modules { if _, inStatus := statusByName[spec.Name]; inStatus { continue } - raw := kyma.KymaModuleInfo{Spec: spec} - modules = append(modules, *entities.NewModuleInstallationFromRaw(raw)) + module, err := r.buildModule(ctx, kyma.KymaModuleInfo{Spec: spec}) + if err != nil { + return nil, err + } + modules = append(modules, module) } return modules, nil } + +func (r *installedModulesRepository) buildModule(ctx context.Context, raw kyma.KymaModuleInfo) (entities.ModuleInstallation, error) { + module := entities.NewModuleInstallationFromRaw(raw) + moduleState, err := r.moduleCRStateRepo.GetModuleCRState(ctx, *module) + if err != nil { + return entities.ModuleInstallation{}, err + } + module.ModuleState = moduleState + return *module, nil +} diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index 155fc25ca..6276b0090 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -6,10 +6,19 @@ import ( 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/entities" "github.com/kyma-project/cli.v3/internal/modulesv2/repository" "github.com/stretchr/testify/require" ) +type fixedCRStateRepo struct { + state string +} + +func (f *fixedCRStateRepo) GetModuleCRState(_ context.Context, _ entities.ModuleInstallation) (string, error) { + return f.state, nil +} + func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ @@ -25,7 +34,7 @@ func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}) result, err := repo.ListInstalledModules(context.Background()) @@ -48,7 +57,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *tes Status: kyma.KymaStatus{}, }, } - repo := repository.NewInstalledModulesRepository(kymaClient) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: ""}) result, err := repo.ListInstalledModules(context.Background()) @@ -71,7 +80,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *t }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}) result, err := repo.ListInstalledModules(context.Background()) From f5c9d21721710d5aa2d63971d3e6a9382538831c Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Thu, 28 May 2026 17:23:43 +0200 Subject: [PATCH 40/45] move installationState resolution to InstalledModulesRepository, simplify ListService --- internal/modulesv2/dependencies.go | 14 ++-- .../modulesv2/entities/moduleinstallation.go | 1 + .../modulesv2/fake/moduleinstallationstate.go | 16 ---- internal/modulesv2/list.go | 32 +------- internal/modulesv2/list_test.go | 43 +++-------- .../modulesv2/repository/installedmodules.go | 73 ++++++++++++++----- .../repository/installedmodules_test.go | 40 +++++++++- 7 files changed, 113 insertions(+), 106 deletions(-) delete mode 100644 internal/modulesv2/fake/moduleinstallationstate.go diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index e9f7bd5de..8c4091319 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -101,7 +101,12 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - return repository.NewInstalledModulesRepository(kubeClient.Kyma(), moduleCRStateRepo), nil + installationStateRepo, err := di.GetTyped[repository.ModuleInstallationStateRepository](c) + if err != nil { + return nil, err + } + + return repository.NewInstalledModulesRepository(kubeClient.Kyma(), moduleCRStateRepo, installationStateRepo), nil }) di.RegisterTyped(container, func(c *di.Container) (repository.ModuleInstallationStateRepository, error) { @@ -153,12 +158,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - installationStateRepo, err := di.GetTyped[repository.ModuleInstallationStateRepository](c) - if err != nil { - return nil, err - } - - return NewListService(installedModulesRepo, installationStateRepo), nil + return NewListService(installedModulesRepo), nil }) return container diff --git a/internal/modulesv2/entities/moduleinstallation.go b/internal/modulesv2/entities/moduleinstallation.go index a257acc3b..b7b0813c7 100644 --- a/internal/modulesv2/entities/moduleinstallation.go +++ b/internal/modulesv2/entities/moduleinstallation.go @@ -8,6 +8,7 @@ type ModuleInstallation struct { Channel string KymaModuleState string ModuleState string + InstallationState string Managed *bool CustomResourcePolicy string TemplateName string diff --git a/internal/modulesv2/fake/moduleinstallationstate.go b/internal/modulesv2/fake/moduleinstallationstate.go deleted file mode 100644 index 0d2ddce27..000000000 --- a/internal/modulesv2/fake/moduleinstallationstate.go +++ /dev/null @@ -1,16 +0,0 @@ -package fake - -import ( - "context" - - "github.com/kyma-project/cli.v3/internal/modulesv2/entities" -) - -type ModuleInstallationStateRepository struct { - GetInstallationStateResult string - GetInstallationStateError 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 85f01d85a..38bf9e3c4 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -4,19 +4,16 @@ 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 - installationStateRepository repository.ModuleInstallationStateRepository + installedModulesRepository repository.InstalledModulesRepository } -func NewListService(installedModulesRepository repository.InstalledModulesRepository, installationStateRepository repository.ModuleInstallationStateRepository) *ListService { +func NewListService(installedModulesRepository repository.InstalledModulesRepository) *ListService { return &ListService{ - installedModulesRepository: installedModulesRepository, - installationStateRepository: installationStateRepository, + installedModulesRepository: installedModulesRepository, } } @@ -28,11 +25,6 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { results := make([]dtos.ListResult, 0, len(installedModules)) for _, module := range installedModules { - installationState, err := s.resolveInstallationState(ctx, module) - if err != nil { - return nil, err - } - results = append(results, dtos.ListResult{ Name: module.Name, Version: module.Version, @@ -40,25 +32,9 @@ func (s *ListService) Run(ctx context.Context) ([]dtos.ListResult, error) { ModuleState: module.ModuleState, Managed: module.IsManaged(), CustomResourcePolicy: module.CustomResourcePolicy, - InstallationState: installationState, + InstallationState: module.InstallationState, }) } return results, nil } - -func (s *ListService) resolveInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { - if module.IsBeingDeleted() { - return module.KymaModuleState, nil - } - - if module.CustomResourcePolicy == "CreateAndDelete" { - return module.KymaModuleState, nil - } - - if !module.IsManaged() { - return module.KymaModuleState, nil - } - - return s.installationStateRepository.GetInstallationState(ctx, module) -} diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 3b08b19d1..45e2cd228 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -4,7 +4,6 @@ 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" @@ -14,7 +13,7 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{}, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -29,7 +28,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { {Name: "istio"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -45,7 +44,7 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -65,7 +64,7 @@ func TestListService_Run_ReturnsManaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -82,7 +81,7 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { {Name: "api-gateway", Managed: nil}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -98,7 +97,7 @@ func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { {Name: "api-gateway", Managed: &managed}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -113,7 +112,7 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -125,13 +124,10 @@ func TestListService_Run_ReturnsCustomResourcePolicy(t *testing.T) { func TestListService_Run_ReturnsInstallationState(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway"}, + {Name: "api-gateway", InstallationState: "Ready"}, }, } - installationStateRepo := &modulesfake.ModuleInstallationStateRepository{ - GetInstallationStateResult: "Ready", - } - svc := NewListService(installedModulesRepo, installationStateRepo) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) @@ -140,32 +136,13 @@ func TestListService_Run_ReturnsInstallationState(t *testing.T) { require.Equal(t, "Ready", module.InstallationState) } -func TestListService_Run_ReturnsModuleStateAsInstallationStateWhenModuleIsBeingDeleted(t *testing.T) { - deletingModule := entities.NewModuleInstallationFromRaw(kyma.KymaModuleInfo{ - Status: kyma.ModuleStatus{Name: "api-gateway", State: "Deleting"}, - }) - installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []entities.ModuleInstallation{*deletingModule}, - } - 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, "Deleting", module.InstallationState) -} - func TestListService_Run_ReturnsModuleStateFromCR(t *testing.T) { installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ {Name: "api-gateway", ModuleState: "Warning"}, }, } - svc := NewListService(installedModulesRepo, &modulesfake.ModuleInstallationStateRepository{}) + svc := NewListService(installedModulesRepo) result, err := svc.Run(context.Background()) diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index 486c346fc..8057212a4 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -12,12 +12,13 @@ type InstalledModulesRepository interface { } type installedModulesRepository struct { - kymaClient kyma.Interface - moduleCRStateRepo ModuleCRStateRepository + kymaClient kyma.Interface + moduleCRStateRepo ModuleCRStateRepository + installationStateRepo ModuleInstallationStateRepository } -func NewInstalledModulesRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository) InstalledModulesRepository { - return &installedModulesRepository{kymaClient: kymaClient, moduleCRStateRepo: moduleCRStateRepo} +func NewInstalledModulesRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository, installationStateRepo ModuleInstallationStateRepository) InstalledModulesRepository { + return &installedModulesRepository{kymaClient: kymaClient, moduleCRStateRepo: moduleCRStateRepo, installationStateRepo: installationStateRepo} } func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) { @@ -25,32 +26,48 @@ func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ( if err != nil { return nil, err } + return r.resolveInstalledModules(ctx, kymaCR.Spec.Modules, kymaCR.Status.Modules) +} - statusByName := make(map[string]kyma.ModuleStatus, len(kymaCR.Status.Modules)) - for _, status := range kymaCR.Status.Modules { - statusByName[status.Name] = status +func (r *installedModulesRepository) resolveInstalledModules(ctx context.Context, specs []kyma.Module, statuses []kyma.ModuleStatus) ([]entities.ModuleInstallation, error) { + modules, err := r.buildModulesFromStatuses(ctx, statuses, specs) + if err != nil { + return nil, err + } + + specOnly, err := r.buildModulesFromSpecsOnly(ctx, specs, statuses) + if err != nil { + return nil, err } - specByName := make(map[string]kyma.Module, len(kymaCR.Spec.Modules)) - for _, spec := range kymaCR.Spec.Modules { + return append(modules, specOnly...), nil +} + +func (r *installedModulesRepository) buildModulesFromStatuses(ctx context.Context, statuses []kyma.ModuleStatus, specs []kyma.Module) ([]entities.ModuleInstallation, error) { + specByName := make(map[string]kyma.Module, len(specs)) + for _, spec := range specs { specByName[spec.Name] = spec } var modules []entities.ModuleInstallation - - for _, status := range kymaCR.Status.Modules { - raw := kyma.KymaModuleInfo{Status: status} - if spec, ok := specByName[status.Name]; ok { - raw.Spec = spec - } - module, err := r.buildModule(ctx, raw) + for _, status := range statuses { + module, err := r.buildModule(ctx, kyma.KymaModuleInfo{Status: status, Spec: specByName[status.Name]}) if err != nil { return nil, err } modules = append(modules, module) } + return modules, nil +} + +func (r *installedModulesRepository) buildModulesFromSpecsOnly(ctx context.Context, specs []kyma.Module, statuses []kyma.ModuleStatus) ([]entities.ModuleInstallation, error) { + statusByName := make(map[string]kyma.ModuleStatus, len(statuses)) + for _, status := range statuses { + statusByName[status.Name] = status + } - for _, spec := range kymaCR.Spec.Modules { + var modules []entities.ModuleInstallation + for _, spec := range specs { if _, inStatus := statusByName[spec.Name]; inStatus { continue } @@ -60,7 +77,6 @@ func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ( } modules = append(modules, module) } - return modules, nil } @@ -71,5 +87,26 @@ func (r *installedModulesRepository) buildModule(ctx context.Context, raw kyma.K return entities.ModuleInstallation{}, err } module.ModuleState = moduleState + installationState, err := r.resolveInstallationState(ctx, *module) + if err != nil { + return entities.ModuleInstallation{}, err + } + module.InstallationState = installationState return *module, nil } + +func (r *installedModulesRepository) resolveInstallationState(ctx context.Context, module entities.ModuleInstallation) (string, error) { + if module.IsBeingDeleted() { + return module.KymaModuleState, nil + } + + if module.CustomResourcePolicy == "CreateAndDelete" { + return module.KymaModuleState, nil + } + + if !module.IsManaged() { + return module.KymaModuleState, nil + } + + return r.installationStateRepo.GetInstallationState(ctx, module) +} diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index 6276b0090..0af6acb84 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -19,6 +19,14 @@ func (f *fixedCRStateRepo) GetModuleCRState(_ context.Context, _ entities.Module return f.state, nil } +type fixedInstallationStateRepo struct { + state string +} + +func (f *fixedInstallationStateRepo) GetInstallationState(_ context.Context, _ entities.ModuleInstallation) (string, error) { + return f.state, nil +} + func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ @@ -34,7 +42,7 @@ func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -57,7 +65,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *tes Status: kyma.KymaStatus{}, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: ""}) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: ""}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -80,7 +88,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *t }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}) + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -88,5 +96,29 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *t require.Len(t, result, 1) module := result[0] require.Equal(t, "api-gateway", module.Name) - require.Equal(t, "Deleting", module.ModuleState) + require.Equal(t, "Deleting", module.KymaModuleState) +} + +func TestInstalledModulesRepository_ListInstalledModules_SetsInstallationStateForCreateAndDelete(t *testing.T) { + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, + }, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{ + {Name: "api-gateway", State: "Warning"}, + }, + }, + }, + } + repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Warning"}, &fixedInstallationStateRepo{state: "Ready"}) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Warning", module.InstallationState) } From 71bdc36156222f5658f4355f83bece5b37141847 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Thu, 28 May 2026 20:35:09 +0200 Subject: [PATCH 41/45] simplify tests: remove trivial field-mapping tests, consolidate MapsAllFields --- .../entities/moduleinstallation_test.go | 60 ++++-------- internal/modulesv2/list_test.go | 92 +++---------------- 2 files changed, 28 insertions(+), 124 deletions(-) diff --git a/internal/modulesv2/entities/moduleinstallation_test.go b/internal/modulesv2/entities/moduleinstallation_test.go index f0d81f5fd..fd776de4e 100644 --- a/internal/modulesv2/entities/moduleinstallation_test.go +++ b/internal/modulesv2/entities/moduleinstallation_test.go @@ -24,9 +24,23 @@ func TestModuleInstallation_IsManaged_FalseWhenManagedIsFalse(t *testing.T) { require.False(t, m.IsManaged()) } -func TestNewModuleInstallationFromRaw_MapsNameVersionChannel(t *testing.T) { +func TestNewModuleInstallationFromRaw_MapsAllFields(t *testing.T) { + managed := false + status := kyma.ModuleStatus{ + Name: "api-gateway", + Version: "3.5.1", + Channel: "regular", + State: "Ready", + } + status.Template.SetName("api-gateway-template") + status.Template.SetNamespace("kyma-system") raw := kyma.KymaModuleInfo{ - Status: kyma.ModuleStatus{Name: "api-gateway", Version: "3.5.1", Channel: "regular"}, + Spec: kyma.Module{ + Name: "api-gateway", + Managed: &managed, + CustomResourcePolicy: "CreateAndDelete", + }, + Status: status, } m := NewModuleInstallationFromRaw(raw) @@ -34,52 +48,10 @@ func TestNewModuleInstallationFromRaw_MapsNameVersionChannel(t *testing.T) { 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.KymaModuleState) -} - -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) -} - -func TestNewModuleInstallationFromRaw_MapsTemplateNameAndNamespace(t *testing.T) { - template := kyma.ModuleStatus{ - Name: "api-gateway", - } - template.Template.SetName("api-gateway-template") - template.Template.SetNamespace("kyma-system") - raw := kyma.KymaModuleInfo{Status: template} - - m := NewModuleInstallationFromRaw(raw) - require.Equal(t, "api-gateway-template", m.TemplateName) require.Equal(t, "kyma-system", m.TemplateNamespace) } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 45e2cd228..1303f0840 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -22,26 +22,18 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { } func TestListService_Run_ReturnsCoreModules(t *testing.T) { + managed := true installedModulesRepo := &modulesfake.InstalledModulesRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ - {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) -} - -func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { - installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", Version: "3.5.1", Channel: "regular", ModuleState: "Ready"}, + { + Name: "api-gateway", + Version: "3.5.1", + Channel: "regular", + ModuleState: "Ready", + InstallationState: "Ready", + Managed: &managed, + CustomResourcePolicy: "CreateAndDelete", + }, }, } svc := NewListService(installedModulesRepo) @@ -55,24 +47,9 @@ func TestListService_Run_ReturnsCoreModulesWithVersionAndChannel(t *testing.T) { require.Equal(t, "3.5.1", module.Version) require.Equal(t, "regular", module.Channel) require.Equal(t, "Ready", module.ModuleState) -} - -func TestListService_Run_ReturnsManaged(t *testing.T) { - managed := true - installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", Managed: &managed}, - }, - } - 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, "Ready", module.InstallationState) require.True(t, module.Managed) + require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) } func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { @@ -105,48 +82,3 @@ 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: []entities.ModuleInstallation{ - {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, - }, - } - svc := NewListService(installedModulesRepo) - - result, err := svc.Run(context.Background()) - - require.NoError(t, err) - module := result[0] - require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) -} - -func TestListService_Run_ReturnsInstallationState(t *testing.T) { - installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", InstallationState: "Ready"}, - }, - } - svc := NewListService(installedModulesRepo) - - result, err := svc.Run(context.Background()) - - require.NoError(t, err) - module := result[0] - require.Equal(t, "Ready", module.InstallationState) -} - -func TestListService_Run_ReturnsModuleStateFromCR(t *testing.T) { - installedModulesRepo := &modulesfake.InstalledModulesRepository{ - ListInstalledModulesResult: []entities.ModuleInstallation{ - {Name: "api-gateway", ModuleState: "Warning"}, - }, - } - svc := NewListService(installedModulesRepo) - - result, err := svc.Run(context.Background()) - - require.NoError(t, err) - module := result[0] - require.Equal(t, "Warning", module.ModuleState) -} From c68ba1cde115da4a2d5978f8be6373f3f5361664 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Thu, 28 May 2026 20:35:56 +0200 Subject: [PATCH 42/45] fix gofmt formatting in modulecrstate.go --- internal/modulesv2/repository/modulecrstate.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go index e78fe1d6d..9e944ee04 100644 --- a/internal/modulesv2/repository/modulecrstate.go +++ b/internal/modulesv2/repository/modulecrstate.go @@ -109,4 +109,3 @@ func highestState(a, b string) string { } return a } - From aed55a4a50d67510fc3336a218dc366e34720716 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Mon, 1 Jun 2026 14:05:30 +0200 Subject: [PATCH 43/45] rename InstalledModulesRepository to ModuleInstallationsRepository --- internal/modulesv2/dependencies.go | 6 +++--- internal/modulesv2/fake/installedmodules.go | 4 ++-- internal/modulesv2/list.go | 4 ++-- internal/modulesv2/list_test.go | 8 ++++---- .../modulesv2/repository/installedmodules.go | 4 ++-- .../repository/installedmodules_test.go | 16 ++++++++-------- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index 8c4091319..44971674d 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -90,7 +90,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return repository.NewClusterMetadataRepository(kubeClient), nil }) - di.RegisterTyped(container, func(c *di.Container) (repository.InstalledModulesRepository, error) { + di.RegisterTyped(container, func(c *di.Container) (repository.ModuleInstallationsRepository, error) { kubeClient, err := di.GetTyped[kube.Client](c) if err != nil { return nil, err @@ -106,7 +106,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - return repository.NewInstalledModulesRepository(kubeClient.Kyma(), moduleCRStateRepo, installationStateRepo), nil + return repository.NewModuleInstallationsRepository(kubeClient.Kyma(), moduleCRStateRepo, installationStateRepo), nil }) di.RegisterTyped(container, func(c *di.Container) (repository.ModuleInstallationStateRepository, error) { @@ -153,7 +153,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { }) di.RegisterTyped(container, func(c *di.Container) (*ListService, error) { - installedModulesRepo, err := di.GetTyped[repository.InstalledModulesRepository](c) + installedModulesRepo, err := di.GetTyped[repository.ModuleInstallationsRepository](c) if err != nil { return nil, err } diff --git a/internal/modulesv2/fake/installedmodules.go b/internal/modulesv2/fake/installedmodules.go index 01f0ced1f..dde9388b4 100644 --- a/internal/modulesv2/fake/installedmodules.go +++ b/internal/modulesv2/fake/installedmodules.go @@ -6,11 +6,11 @@ import ( "github.com/kyma-project/cli.v3/internal/modulesv2/entities" ) -type InstalledModulesRepository struct { +type ModuleInstallationsRepository struct { ListInstalledModulesResult []entities.ModuleInstallation ListInstalledModulesError error } -func (f *InstalledModulesRepository) ListInstalledModules(_ context.Context) ([]entities.ModuleInstallation, error) { +func (f *ModuleInstallationsRepository) ListInstalledModules(_ context.Context) ([]entities.ModuleInstallation, error) { return f.ListInstalledModulesResult, f.ListInstalledModulesError } diff --git a/internal/modulesv2/list.go b/internal/modulesv2/list.go index 38bf9e3c4..83dd5a384 100644 --- a/internal/modulesv2/list.go +++ b/internal/modulesv2/list.go @@ -8,10 +8,10 @@ import ( ) type ListService struct { - installedModulesRepository repository.InstalledModulesRepository + installedModulesRepository repository.ModuleInstallationsRepository } -func NewListService(installedModulesRepository repository.InstalledModulesRepository) *ListService { +func NewListService(installedModulesRepository repository.ModuleInstallationsRepository) *ListService { return &ListService{ installedModulesRepository: installedModulesRepository, } diff --git a/internal/modulesv2/list_test.go b/internal/modulesv2/list_test.go index 1303f0840..20cf110e3 100644 --- a/internal/modulesv2/list_test.go +++ b/internal/modulesv2/list_test.go @@ -10,7 +10,7 @@ import ( ) func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { - installedModulesRepo := &modulesfake.InstalledModulesRepository{ + installedModulesRepo := &modulesfake.ModuleInstallationsRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{}, } svc := NewListService(installedModulesRepo) @@ -23,7 +23,7 @@ func TestListService_Run_ReturnsEmptyWhenNoInstalledModules(t *testing.T) { func TestListService_Run_ReturnsCoreModules(t *testing.T) { managed := true - installedModulesRepo := &modulesfake.InstalledModulesRepository{ + installedModulesRepo := &modulesfake.ModuleInstallationsRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ { Name: "api-gateway", @@ -53,7 +53,7 @@ func TestListService_Run_ReturnsCoreModules(t *testing.T) { } func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { - installedModulesRepo := &modulesfake.InstalledModulesRepository{ + installedModulesRepo := &modulesfake.ModuleInstallationsRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ {Name: "api-gateway", Managed: nil}, }, @@ -69,7 +69,7 @@ func TestListService_Run_ReturnsManagedTrueWhenManagedIsNil(t *testing.T) { func TestListService_Run_ReturnsManagedFalseWhenUnmanaged(t *testing.T) { managed := false - installedModulesRepo := &modulesfake.InstalledModulesRepository{ + installedModulesRepo := &modulesfake.ModuleInstallationsRepository{ ListInstalledModulesResult: []entities.ModuleInstallation{ {Name: "api-gateway", Managed: &managed}, }, diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index 8057212a4..d9a2512eb 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -7,7 +7,7 @@ import ( "github.com/kyma-project/cli.v3/internal/modulesv2/entities" ) -type InstalledModulesRepository interface { +type ModuleInstallationsRepository interface { ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) } @@ -17,7 +17,7 @@ type installedModulesRepository struct { installationStateRepo ModuleInstallationStateRepository } -func NewInstalledModulesRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository, installationStateRepo ModuleInstallationStateRepository) InstalledModulesRepository { +func NewModuleInstallationsRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository, installationStateRepo ModuleInstallationStateRepository) ModuleInstallationsRepository { return &installedModulesRepository{kymaClient: kymaClient, moduleCRStateRepo: moduleCRStateRepo, installationStateRepo: installationStateRepo} } diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index 0af6acb84..ac7f2c513 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -27,7 +27,7 @@ func (f *fixedInstallationStateRepo) GetInstallationState(_ context.Context, _ e return f.state, nil } -func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T) { +func TestModuleInstallationsRepository_ListInstalledModules_NormalCase(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ Spec: kyma.KymaSpec{ @@ -42,7 +42,7 @@ func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}, &fixedInstallationStateRepo{}) + repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -54,7 +54,7 @@ func TestInstalledModulesRepository_ListInstalledModules_NormalCase(t *testing.T require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) } -func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *testing.T) { +func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingAdded(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ Spec: kyma.KymaSpec{ @@ -65,7 +65,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *tes Status: kyma.KymaStatus{}, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: ""}, &fixedInstallationStateRepo{}) + repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: ""}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -77,7 +77,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingAdded(t *tes require.Equal(t, "CreateAndDelete", module.CustomResourcePolicy) } -func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *testing.T) { +func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingDeleted(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ Spec: kyma.KymaSpec{}, @@ -88,7 +88,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *t }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}, &fixedInstallationStateRepo{}) + repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}, &fixedInstallationStateRepo{}) result, err := repo.ListInstalledModules(context.Background()) @@ -99,7 +99,7 @@ func TestInstalledModulesRepository_ListInstalledModules_ModuleBeingDeleted(t *t require.Equal(t, "Deleting", module.KymaModuleState) } -func TestInstalledModulesRepository_ListInstalledModules_SetsInstallationStateForCreateAndDelete(t *testing.T) { +func TestModuleInstallationsRepository_ListInstalledModules_SetsInstallationStateForCreateAndDelete(t *testing.T) { kymaClient := &kubefake.KymaClient{ ReturnDefaultKyma: kyma.Kyma{ Spec: kyma.KymaSpec{ @@ -114,7 +114,7 @@ func TestInstalledModulesRepository_ListInstalledModules_SetsInstallationStateFo }, }, } - repo := repository.NewInstalledModulesRepository(kymaClient, &fixedCRStateRepo{state: "Warning"}, &fixedInstallationStateRepo{state: "Ready"}) + repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Warning"}, &fixedInstallationStateRepo{state: "Ready"}) result, err := repo.ListInstalledModules(context.Background()) From 25d26e058879cb3994532e0500ee9f0ccc552b04 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Tue, 2 Jun 2026 13:19:51 +0200 Subject: [PATCH 44/45] internalize ModuleCRStateRepository and ModuleInstallationStateRepository --- internal/modulesv2/dependencies.go | 30 +- .../modulesv2/repository/installedmodules.go | 13 +- .../repository/installedmodules_test.go | 322 +++++++++++++++--- .../modulesv2/repository/modulecrstate.go | 8 - .../repository/modulecrstate_test.go | 134 -------- .../repository/moduleinstallationstate.go | 8 - 6 files changed, 282 insertions(+), 233 deletions(-) delete mode 100644 internal/modulesv2/repository/modulecrstate_test.go diff --git a/internal/modulesv2/dependencies.go b/internal/modulesv2/dependencies.go index 44971674d..964bab5a7 100644 --- a/internal/modulesv2/dependencies.go +++ b/internal/modulesv2/dependencies.go @@ -96,35 +96,7 @@ func setupDIContainer(kymaConfig *cmdcommon.KymaConfig) *di.Container { return nil, err } - moduleCRStateRepo, err := di.GetTyped[repository.ModuleCRStateRepository](c) - if err != nil { - return nil, err - } - - installationStateRepo, err := di.GetTyped[repository.ModuleInstallationStateRepository](c) - if err != nil { - return nil, err - } - - return repository.NewModuleInstallationsRepository(kubeClient.Kyma(), moduleCRStateRepo, installationStateRepo), 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 - }) - - di.RegisterTyped(container, func(c *di.Container) (repository.ModuleCRStateRepository, error) { - kubeClient, err := di.GetTyped[kube.Client](c) - if err != nil { - return nil, err - } - - return repository.NewModuleCRStateRepository(kubeClient), nil + return repository.NewModuleInstallationsRepository(kubeClient), nil }) // Services: diff --git a/internal/modulesv2/repository/installedmodules.go b/internal/modulesv2/repository/installedmodules.go index d9a2512eb..7f2a430e4 100644 --- a/internal/modulesv2/repository/installedmodules.go +++ b/internal/modulesv2/repository/installedmodules.go @@ -3,6 +3,7 @@ package repository import ( "context" + "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" ) @@ -13,12 +14,16 @@ type ModuleInstallationsRepository interface { type installedModulesRepository struct { kymaClient kyma.Interface - moduleCRStateRepo ModuleCRStateRepository - installationStateRepo ModuleInstallationStateRepository + moduleCRStateRepo *moduleCRStateRepository + installationStateRepo *moduleInstallationStateRepository } -func NewModuleInstallationsRepository(kymaClient kyma.Interface, moduleCRStateRepo ModuleCRStateRepository, installationStateRepo ModuleInstallationStateRepository) ModuleInstallationsRepository { - return &installedModulesRepository{kymaClient: kymaClient, moduleCRStateRepo: moduleCRStateRepo, installationStateRepo: installationStateRepo} +func NewModuleInstallationsRepository(kubeClient kube.Client) ModuleInstallationsRepository { + return &installedModulesRepository{ + kymaClient: kubeClient.Kyma(), + moduleCRStateRepo: &moduleCRStateRepository{kubeClient: kubeClient}, + installationStateRepo: &moduleInstallationStateRepository{kubeClient: kubeClient}, + } } func (r *installedModulesRepository) ListInstalledModules(ctx context.Context) ([]entities.ModuleInstallation, error) { diff --git a/internal/modulesv2/repository/installedmodules_test.go b/internal/modulesv2/repository/installedmodules_test.go index ac7f2c513..8462929b7 100644 --- a/internal/modulesv2/repository/installedmodules_test.go +++ b/internal/modulesv2/repository/installedmodules_test.go @@ -2,47 +2,62 @@ package repository_test import ( "context" + "fmt" "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/entities" "github.com/kyma-project/cli.v3/internal/modulesv2/repository" "github.com/stretchr/testify/require" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -type fixedCRStateRepo struct { - state string -} - -func (f *fixedCRStateRepo) GetModuleCRState(_ context.Context, _ entities.ModuleInstallation) (string, error) { - return f.state, nil -} - -type fixedInstallationStateRepo struct { - state string -} - -func (f *fixedInstallationStateRepo) GetInstallationState(_ context.Context, _ entities.ModuleInstallation) (string, error) { - return f.state, nil +func newKubeClient(defaultKyma kyma.Kyma, moduleTemplate kyma.ModuleTemplate, crList *unstructured.UnstructuredList) *kubefake.KubeClient { + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: defaultKyma, + ReturnModuleTemplate: moduleTemplate, + } + rootlessDynamic := &kubefake.RootlessDynamicClient{ + ReturnListObjs: crList, + } + return &kubefake.KubeClient{ + TestKymaInterface: kymaClient, + TestRootlessDynamicInterface: rootlessDynamic, + } } func TestModuleInstallationsRepository_ListInstalledModules_NormalCase(t *testing.T) { - kymaClient := &kubefake.KymaClient{ - ReturnDefaultKyma: kyma.Kyma{ - Spec: kyma.KymaSpec{ - Modules: []kyma.Module{ - {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, - }, + apiGatewayStatus := kyma.ModuleStatus{Name: "api-gateway", State: "Ready"} + apiGatewayStatus.Template.SetName("api-gateway-template") + apiGatewayStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, - Status: kyma.KymaStatus{ - Modules: []kyma.ModuleStatus{ - {Name: "api-gateway", State: "Ready"}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{apiGatewayStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Data: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "gateway.kyma-project.io/v1alpha1", + "kind": "APIGateway", }, }, }, } - repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Ready"}, &fixedInstallationStateRepo{}) + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]interface{}{"status": map[string]interface{}{"state": "Ready"}}}, + }, + } + kubeClient := newKubeClient(defaultKyma, moduleTemplate, crList) + repo := repository.NewModuleInstallationsRepository(kubeClient) result, err := repo.ListInstalledModules(context.Background()) @@ -55,17 +70,16 @@ func TestModuleInstallationsRepository_ListInstalledModules_NormalCase(t *testin } func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingAdded(t *testing.T) { - kymaClient := &kubefake.KymaClient{ - ReturnDefaultKyma: kyma.Kyma{ - Spec: kyma.KymaSpec{ - Modules: []kyma.Module{ - {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, - }, + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, - Status: kyma.KymaStatus{}, }, + Status: kyma.KymaStatus{}, } - repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: ""}, &fixedInstallationStateRepo{}) + kubeClient := newKubeClient(defaultKyma, kyma.ModuleTemplate{}, nil) + repo := repository.NewModuleInstallationsRepository(kubeClient) result, err := repo.ListInstalledModules(context.Background()) @@ -78,17 +92,32 @@ func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingAdded(t * } func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingDeleted(t *testing.T) { - kymaClient := &kubefake.KymaClient{ - ReturnDefaultKyma: kyma.Kyma{ - Spec: kyma.KymaSpec{}, - Status: kyma.KymaStatus{ - Modules: []kyma.ModuleStatus{ - {Name: "api-gateway", State: "Deleting"}, + deletingStatus := kyma.ModuleStatus{Name: "api-gateway", State: "Deleting"} + deletingStatus.Template.SetName("api-gateway-template") + deletingStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{}, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{deletingStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Data: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "gateway.kyma-project.io/v1alpha1", + "kind": "APIGateway", }, }, }, } - repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Deleting"}, &fixedInstallationStateRepo{}) + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]interface{}{"status": map[string]interface{}{"state": "Deleting"}}}, + }, + } + kubeClient := newKubeClient(defaultKyma, moduleTemplate, crList) + repo := repository.NewModuleInstallationsRepository(kubeClient) result, err := repo.ListInstalledModules(context.Background()) @@ -100,21 +129,36 @@ func TestModuleInstallationsRepository_ListInstalledModules_ModuleBeingDeleted(t } func TestModuleInstallationsRepository_ListInstalledModules_SetsInstallationStateForCreateAndDelete(t *testing.T) { - kymaClient := &kubefake.KymaClient{ - ReturnDefaultKyma: kyma.Kyma{ - Spec: kyma.KymaSpec{ - Modules: []kyma.Module{ - {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, - }, + warningStatus := kyma.ModuleStatus{Name: "api-gateway", State: "Warning"} + warningStatus.Template.SetName("api-gateway-template") + warningStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{ + {Name: "api-gateway", CustomResourcePolicy: "CreateAndDelete"}, }, - Status: kyma.KymaStatus{ - Modules: []kyma.ModuleStatus{ - {Name: "api-gateway", State: "Warning"}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{warningStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Data: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "gateway.kyma-project.io/v1alpha1", + "kind": "APIGateway", }, }, }, } - repo := repository.NewModuleInstallationsRepository(kymaClient, &fixedCRStateRepo{state: "Warning"}, &fixedInstallationStateRepo{state: "Ready"}) + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]interface{}{"status": map[string]interface{}{"state": "Warning"}}}, + }, + } + kubeClient := newKubeClient(defaultKyma, moduleTemplate, crList) + repo := repository.NewModuleInstallationsRepository(kubeClient) result, err := repo.ListInstalledModules(context.Background()) @@ -122,3 +166,181 @@ func TestModuleInstallationsRepository_ListInstalledModules_SetsInstallationStat module := result[0] require.Equal(t, "Warning", module.InstallationState) } + +func TestModuleInstallationsRepository_ListInstalledModules_ModuleCRState_ReturnsStateFromCR(t *testing.T) { + apiGatewayStatus := kyma.ModuleStatus{Name: "api-gateway", State: "Ready"} + apiGatewayStatus.Template.SetName("api-gateway-template") + apiGatewayStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{{Name: "api-gateway"}}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{apiGatewayStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Data: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "APIGateway", + }, + }, + }, + } + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]interface{}{"status": map[string]interface{}{"state": "Warning"}}}, + }, + } + kubeClient := newKubeClient(defaultKyma, moduleTemplate, crList) + repo := repository.NewModuleInstallationsRepository(kubeClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Warning", module.ModuleState) +} + +func TestModuleInstallationsRepository_ListInstalledModules_ModuleCRState_UnmanagedModule_FindsTemplateByNameAndVersion(t *testing.T) { + managed := false + matchingTemplate := kyma.ModuleTemplate{} + matchingTemplate.Spec.ModuleName = "api-gateway" + matchingTemplate.Spec.Version = "3.5.1" + matchingTemplate.Spec.Data = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "APIGateway", + }, + } + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{{Name: "api-gateway", Managed: &managed}}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{ + {Name: "api-gateway", Version: "3.5.1"}, + }, + }, + } + crList := &unstructured.UnstructuredList{ + Items: []unstructured.Unstructured{ + {Object: map[string]interface{}{"status": map[string]interface{}{"state": "Ready"}}}, + }, + } + kymaClient := &kubefake.KymaClient{ + ReturnDefaultKyma: defaultKyma, + ReturnModuleTemplateList: kyma.ModuleTemplateList{ + Items: []kyma.ModuleTemplate{matchingTemplate}, + }, + } + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: kymaClient, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnListObjs: crList, + }, + } + repo := repository.NewModuleInstallationsRepository(kubeClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Ready", module.ModuleState) +} + +func TestModuleInstallationsRepository_ListInstalledModules_ModuleCRState_ReturnsEmptyOnDiscoveryError(t *testing.T) { + eventingStatus := kyma.ModuleStatus{Name: "eventing", State: "Ready"} + eventingStatus.Template.SetName("eventing-template") + eventingStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{{Name: "eventing"}}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{eventingStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Data: unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1alpha1", + "kind": "Eventing", + }, + }, + }, + } + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: &kubefake.KymaClient{ + ReturnDefaultKyma: defaultKyma, + ReturnModuleTemplate: moduleTemplate, + }, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnErr: fmt.Errorf("failed to discover API resource using discovery client: resource 'Eventing' in group 'operator.kyma-project.io', and version 'v1alpha1' not registered on cluster"), + }, + } + repo := repository.NewModuleInstallationsRepository(kubeClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "", module.ModuleState) +} + +func TestModuleInstallationsRepository_ListInstalledModules_InstallationState_ManagedModuleUsesManagerState(t *testing.T) { + managed := true + apiGatewayStatus := kyma.ModuleStatus{Name: "api-gateway", State: "Warning"} + apiGatewayStatus.Template.SetName("api-gateway-template") + apiGatewayStatus.Template.SetNamespace("kyma-system") + defaultKyma := kyma.Kyma{ + Spec: kyma.KymaSpec{ + Modules: []kyma.Module{{Name: "api-gateway", Managed: &managed}}, + }, + Status: kyma.KymaStatus{ + Modules: []kyma.ModuleStatus{apiGatewayStatus}, + }, + } + moduleTemplate := kyma.ModuleTemplate{ + Spec: kyma.ModuleTemplateSpec{ + Manager: &kyma.Manager{ + GroupVersionKind: managerGVK("apps", "v1", "Deployment"), + Name: "api-gateway-manager", + Namespace: "kyma-system", + }, + }, + } + managerObj := unstructured.Unstructured{ + Object: map[string]interface{}{ + "status": map[string]interface{}{ + "readyReplicas": int64(1), + }, + "spec": map[string]interface{}{ + "replicas": int64(1), + }, + }, + } + kubeClient := &kubefake.KubeClient{ + TestKymaInterface: &kubefake.KymaClient{ + ReturnDefaultKyma: defaultKyma, + ReturnModuleTemplate: moduleTemplate, + }, + TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ + ReturnGetObj: managerObj, + }, + } + repo := repository.NewModuleInstallationsRepository(kubeClient) + + result, err := repo.ListInstalledModules(context.Background()) + + require.NoError(t, err) + module := result[0] + require.Equal(t, "Ready", module.InstallationState) +} + +func managerGVK(group, version, kind string) metav1.GroupVersionKind { + return metav1.GroupVersionKind{Group: group, Version: version, Kind: kind} +} diff --git a/internal/modulesv2/repository/modulecrstate.go b/internal/modulesv2/repository/modulecrstate.go index 9e944ee04..b0e47fbc1 100644 --- a/internal/modulesv2/repository/modulecrstate.go +++ b/internal/modulesv2/repository/modulecrstate.go @@ -13,18 +13,10 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -type ModuleCRStateRepository interface { - GetModuleCRState(ctx context.Context, module entities.ModuleInstallation) (string, error) -} - type moduleCRStateRepository struct { kubeClient kube.Client } -func NewModuleCRStateRepository(kubeClient kube.Client) ModuleCRStateRepository { - return &moduleCRStateRepository{kubeClient: kubeClient} -} - func (r *moduleCRStateRepository) GetModuleCRState(ctx context.Context, module entities.ModuleInstallation) (string, error) { moduleTemplate, err := r.findModuleTemplate(ctx, module) if err != nil { diff --git a/internal/modulesv2/repository/modulecrstate_test.go b/internal/modulesv2/repository/modulecrstate_test.go deleted file mode 100644 index 776a628d0..000000000 --- a/internal/modulesv2/repository/modulecrstate_test.go +++ /dev/null @@ -1,134 +0,0 @@ -package repository_test - -import ( - "context" - "fmt" - "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/entities" - "github.com/kyma-project/cli.v3/internal/modulesv2/repository" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -func TestModuleCRStateRepository_GetModuleCRState_ReturnsStateFromCR(t *testing.T) { - moduleTemplate := kyma.ModuleTemplate{} - moduleTemplate.Spec.Data = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "operator.kyma-project.io/v1alpha1", - "kind": "APIGateway", - }, - } - - crList := &unstructured.UnstructuredList{ - Items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "state": "Warning", - }, - }, - }, - }, - } - - kubeClient := &kubefake.KubeClient{ - TestKymaInterface: &kubefake.KymaClient{ - ReturnModuleTemplate: moduleTemplate, - }, - TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ - ReturnListObjs: crList, - }, - } - - module := entities.ModuleInstallation{ - TemplateName: "api-gateway-template", - TemplateNamespace: "kyma-system", - } - repo := repository.NewModuleCRStateRepository(kubeClient) - - state, err := repo.GetModuleCRState(context.Background(), module) - - require.NoError(t, err) - require.Equal(t, "Warning", state) -} - -func TestModuleCRStateRepository_GetModuleCRState_UnmanagedModule_FindsTemplateByNameAndVersion(t *testing.T) { - matchingTemplate := kyma.ModuleTemplate{} - matchingTemplate.Spec.ModuleName = "api-gateway" - matchingTemplate.Spec.Version = "3.5.1" - matchingTemplate.Spec.Data = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "operator.kyma-project.io/v1alpha1", - "kind": "APIGateway", - }, - } - - crList := &unstructured.UnstructuredList{ - Items: []unstructured.Unstructured{ - { - Object: map[string]interface{}{ - "status": map[string]interface{}{ - "state": "Ready", - }, - }, - }, - }, - } - - kubeClient := &kubefake.KubeClient{ - TestKymaInterface: &kubefake.KymaClient{ - ReturnModuleTemplateList: kyma.ModuleTemplateList{ - Items: []kyma.ModuleTemplate{matchingTemplate}, - }, - }, - TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ - ReturnListObjs: crList, - }, - } - - managed := false - module := entities.ModuleInstallation{ - Name: "api-gateway", - Version: "3.5.1", - Managed: &managed, - } - repo := repository.NewModuleCRStateRepository(kubeClient) - - state, err := repo.GetModuleCRState(context.Background(), module) - - require.NoError(t, err) - require.Equal(t, "Ready", state) -} - -func TestModuleCRStateRepository_GetModuleCRState_ReturnsEmptyOnDiscoveryError(t *testing.T) { - moduleTemplate := kyma.ModuleTemplate{} - moduleTemplate.Spec.Data = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "operator.kyma-project.io/v1alpha1", - "kind": "Eventing", - }, - } - - kubeClient := &kubefake.KubeClient{ - TestKymaInterface: &kubefake.KymaClient{ - ReturnModuleTemplate: moduleTemplate, - }, - TestRootlessDynamicInterface: &kubefake.RootlessDynamicClient{ - ReturnErr: fmt.Errorf("failed to discover API resource using discovery client: resource 'Eventing' in group 'operator.kyma-project.io', and version 'v1alpha1' not registered on cluster"), - }, - } - - module := entities.ModuleInstallation{ - TemplateName: "eventing-template", - TemplateNamespace: "kyma-system", - } - repo := repository.NewModuleCRStateRepository(kubeClient) - - state, err := repo.GetModuleCRState(context.Background(), module) - - require.NoError(t, err) - require.Equal(t, "", state) -} diff --git a/internal/modulesv2/repository/moduleinstallationstate.go b/internal/modulesv2/repository/moduleinstallationstate.go index ec62adeaf..fb3866b46 100644 --- a/internal/modulesv2/repository/moduleinstallationstate.go +++ b/internal/modulesv2/repository/moduleinstallationstate.go @@ -11,18 +11,10 @@ import ( "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -type ModuleInstallationStateRepository interface { - GetInstallationState(ctx context.Context, module entities.ModuleInstallation) (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, module entities.ModuleInstallation) (string, error) { moduleTemplate, err := r.kubeClient.Kyma().GetModuleTemplate(ctx, module.TemplateNamespace, module.TemplateName) if err != nil { From 5a92ec04b298016d7e1ef12993326a373279f559 Mon Sep 17 00:00:00 2001 From: Marcin Dobrochowski Date: Wed, 3 Jun 2026 15:52:49 +0200 Subject: [PATCH 45/45] update list command warning message --- docs/user/gen-docs/kyma_alpha_module_list.md | 3 +-- internal/cmd/alpha/module/list.go | 3 +-- 2 files changed, 2 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 cfa4216ab..70a55397b 100644 --- a/docs/user/gen-docs/kyma_alpha_module_list.md +++ b/docs/user/gen-docs/kyma_alpha_module_list.md @@ -6,8 +6,7 @@ Lists installed modules. Use this command to list the installed Kyma modules. -NOTE: functionality under construction - - community modules not yet supported +WARNING: This functionality is still under construction. Community modules are 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 da1f8290d..ebf9154bd 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -24,8 +24,7 @@ func NewListV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { Short: "Lists installed modules", Long: `Use this command to list the installed Kyma modules. -NOTE: functionality under construction - - community modules not yet supported`, +WARNING: This functionality is still under construction. Community modules are not yet supported.`, Run: func(_ *cobra.Command, _ []string) { clierror.Check(listModulesV2(&cfg)) },