Skip to content

Commit 9ef7f38

Browse files
committed
Refactor for modules package - PoC
1 parent 36b34c1 commit 9ef7f38

File tree

12 files changed

+597
-45
lines changed

12 files changed

+597
-45
lines changed

internal/cmd/module/catalogv2.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package module
2+
3+
import (
4+
"github.com/kyma-project/cli.v3/internal/clierror"
5+
"github.com/kyma-project/cli.v3/internal/cmdcommon"
6+
"github.com/kyma-project/cli.v3/internal/cmdcommon/types"
7+
"github.com/kyma-project/cli.v3/internal/di"
8+
"github.com/kyma-project/cli.v3/internal/modulesv2"
9+
"github.com/spf13/cobra"
10+
)
11+
12+
type catalogV2Config struct {
13+
*cmdcommon.KymaConfig
14+
outputFormat types.Format
15+
}
16+
17+
func newCatalogV2CMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
18+
cfg := catalogV2Config{
19+
KymaConfig: kymaConfig,
20+
}
21+
22+
cmd := &cobra.Command{
23+
Use: "catalogv2 [flags]",
24+
Short: "Lists modules catalog",
25+
Long: `Use this command to list all available Kyma modules.`,
26+
Run: func(_ *cobra.Command, _ []string) {
27+
clierror.Check(catalogV2Modules(&cfg))
28+
},
29+
}
30+
31+
cmd.Flags().VarP(&cfg.outputFormat, "output", "o", "Output format (Possible values: table, json, yaml)")
32+
33+
return cmd
34+
}
35+
36+
func catalogV2Modules(cfg *catalogV2Config) clierror.Error {
37+
c, err := modulesv2.SetupDIContainer(cfg.KymaConfig)
38+
if err != nil {
39+
return clierror.Wrap(err, clierror.New("failed to configure command dependencies"))
40+
}
41+
42+
catalogService, err := di.GetTyped[*modulesv2.CatalogService](c)
43+
if err != nil {
44+
return clierror.Wrap(err, clierror.New("failed to execute the catalog command"))
45+
}
46+
47+
catalogResult, err := catalogService.Run(cfg.Ctx, []string{"https://kyma-project.github.io/community-modules/all-modules.json"})
48+
if err != nil {
49+
return clierror.Wrap(err, clierror.New("failed to list available modules from the target Kyma environment"))
50+
}
51+
52+
err = modulesv2.RenderCatalog(catalogResult, cfg.outputFormat)
53+
if err != nil {
54+
return clierror.Wrap(err, clierror.New("failed to render catalog"))
55+
}
56+
57+
return nil
58+
}

internal/cmd/module/module.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func NewModuleCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command {
1515

1616
cmd.AddCommand(newListCMD(kymaConfig))
1717
cmd.AddCommand(newCatalogCMD(kymaConfig))
18+
cmd.AddCommand(newCatalogV2CMD(kymaConfig))
1819
cmd.AddCommand(newAddCMD(kymaConfig))
1920
cmd.AddCommand(newDeleteCMD(kymaConfig))
2021
cmd.AddCommand(newManageCMD(kymaConfig))

internal/di/container.go

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package di
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
"sync"
7+
)
8+
9+
// DIContainer is a simple dependency injection container for singleton instances
10+
type DIContainer struct {
11+
mu sync.RWMutex
12+
instances map[reflect.Type]interface{}
13+
factories map[reflect.Type]Factory
14+
}
15+
16+
type Factory func(container *DIContainer) (interface{}, error)
17+
18+
func NewDIContainer() *DIContainer {
19+
return &DIContainer{
20+
instances: make(map[reflect.Type]interface{}),
21+
factories: make(map[reflect.Type]Factory),
22+
}
23+
}
24+
25+
// Get retrieves or creates a singleton instance of the specified type
26+
func (c *DIContainer) Get(targetType reflect.Type) (interface{}, error) {
27+
c.mu.RLock()
28+
// Check if we already have a singleton instance
29+
if instance, exists := c.instances[targetType]; exists {
30+
c.mu.RUnlock()
31+
return instance, nil
32+
}
33+
c.mu.RUnlock()
34+
35+
// Check if we have a factory
36+
c.mu.RLock()
37+
factory, exists := c.factories[targetType]
38+
c.mu.RUnlock()
39+
40+
if !exists {
41+
return nil, fmt.Errorf("no registration found for type %s", targetType.String())
42+
}
43+
44+
// Create singleton instance using factory
45+
c.mu.Lock()
46+
47+
// Double-check if instance was created while we were waiting for the lock
48+
if instance, exists := c.instances[targetType]; exists {
49+
c.mu.Unlock()
50+
return instance, nil
51+
}
52+
53+
// Release lock before calling factory to avoid deadlock when factory resolves dependencies
54+
c.mu.Unlock()
55+
56+
instance, err := factory(c)
57+
if err != nil {
58+
return nil, fmt.Errorf("failed to create instance of type %s: %w", targetType.String(), err)
59+
}
60+
61+
// Re-acquire lock to cache the instance
62+
c.mu.Lock()
63+
// Check again in case another goroutine created it
64+
if existing, exists := c.instances[targetType]; exists {
65+
c.mu.Unlock()
66+
return existing, nil
67+
}
68+
c.instances[targetType] = instance
69+
c.mu.Unlock()
70+
71+
return instance, nil
72+
}
73+
74+
// GetTyped is a generic helper to get a singleton instance with type safety
75+
func GetTyped[T any](c *DIContainer) (T, error) {
76+
var zero T
77+
targetType := reflect.TypeOf((*T)(nil)).Elem()
78+
79+
instance, err := c.Get(targetType)
80+
if err != nil {
81+
return zero, err
82+
}
83+
84+
typed, ok := instance.(T)
85+
if !ok {
86+
return zero, fmt.Errorf("instance is not of expected type %T", zero)
87+
}
88+
89+
return typed, nil
90+
}
91+
92+
// RegisterTyped is a generic helper to register a factory with type safety
93+
// Usage: RegisterTyped[MyInterface](container, func(c *DIContainer) (MyInterface, error) { ... })
94+
func RegisterTyped[T any](c *DIContainer, factory func(*DIContainer) (T, error)) {
95+
c.mu.Lock()
96+
defer c.mu.Unlock()
97+
98+
targetType := reflect.TypeOf((*T)(nil)).Elem()
99+
c.factories[targetType] = func(container *DIContainer) (interface{}, error) {
100+
return factory(container)
101+
}
102+
}

internal/modulesv2/catalog.go

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,44 @@ package modulesv2
22

33
import (
44
"context"
5+
"fmt"
56

6-
"github.com/kyma-project/cli.v3/internal/modulesv2/entities"
7+
"github.com/kyma-project/cli.v3/internal/modulesv2/dtos"
78
"github.com/kyma-project/cli.v3/internal/modulesv2/repository"
89
)
910

10-
type Catalog struct {
11-
moduleTemplatesRepository *repository.ModuleTemplatesRepository
11+
type CatalogService struct {
12+
moduleTemplatesRepository repository.ModuleTemplatesRepository
1213
}
1314

14-
func NewCatalogService(moduleTemplatesRepository *repository.ModuleTemplatesRepository) *Catalog {
15-
return &Catalog{
15+
func NewCatalogService(moduleTemplatesRepository repository.ModuleTemplatesRepository) *CatalogService {
16+
return &CatalogService{
1617
moduleTemplatesRepository: moduleTemplatesRepository,
1718
}
1819
}
1920

20-
func (c *Catalog) Get(ctx context.Context) ([]entities.ModuleTemplate, error) {
21+
func (c *CatalogService) Run(ctx context.Context, urls []string) ([]dtos.CatalogResult, error) {
22+
results := []dtos.CatalogResult{}
2123

22-
return nil, nil
24+
// todo: add support for clusters without kyma cr
25+
coreModules, err := c.moduleTemplatesRepository.ListCore(ctx)
26+
if err != nil {
27+
return nil, fmt.Errorf("failed to list core modules: %v", err)
28+
}
29+
30+
localCommunityModules, err := c.moduleTemplatesRepository.ListLocalCommunity(ctx)
31+
if err != nil {
32+
return nil, fmt.Errorf("failed to list local community modules: %v", err)
33+
}
34+
35+
externalCommunityModules, err := c.moduleTemplatesRepository.ListExternalCommunity(ctx, urls)
36+
if err != nil {
37+
return nil, fmt.Errorf("failed to list external community modules: %v", err)
38+
}
39+
40+
results = append(results, dtos.CatalogResultFromCoreModuleTemplates(coreModules)...)
41+
results = append(results, dtos.CatalogResultFromCommunityModuleTemplates(localCommunityModules)...)
42+
results = append(results, dtos.CatalogResultFromCommunityModuleTemplates(externalCommunityModules)...)
43+
44+
return results, nil
2345
}

internal/modulesv2/dependencies.go

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
package modulesv2
2+
3+
import (
4+
"github.com/kyma-project/cli.v3/internal/cmdcommon"
5+
"github.com/kyma-project/cli.v3/internal/di"
6+
"github.com/kyma-project/cli.v3/internal/kube"
7+
"github.com/kyma-project/cli.v3/internal/modulesv2/repository"
8+
)
9+
10+
func SetupDIContainer(kymaConfig *cmdcommon.KymaConfig) (*di.DIContainer, error) {
11+
container := di.NewDIContainer()
12+
13+
// 1. Register kube.Client - the foundation dependency
14+
di.RegisterTyped(container, func(c *di.DIContainer) (kube.Client, error) {
15+
return kymaConfig.GetKubeClient()
16+
})
17+
18+
// 2. Register ExternalModuleTemplateRepository - has no dependencies
19+
di.RegisterTyped(container, func(c *di.DIContainer) (repository.ExternalModuleTemplateRepository, error) {
20+
return repository.NewExternalModuleTemplateRepository(), nil
21+
})
22+
23+
// 3. Register ModuleTemplatesRepository - depends on kube.Client and ExternalModuleTemplateRepository
24+
di.RegisterTyped(container, func(c *di.DIContainer) (repository.ModuleTemplatesRepository, error) {
25+
kubeClient, err := di.GetTyped[kube.Client](c)
26+
if err != nil {
27+
return nil, err
28+
}
29+
30+
externalRepo, err := di.GetTyped[repository.ExternalModuleTemplateRepository](c)
31+
if err != nil {
32+
return nil, err
33+
}
34+
35+
return repository.NewModuleTemplatesRepository(kubeClient, externalRepo), nil
36+
})
37+
38+
// 4. Register Catalog - depends on ModuleTemplatesRepository
39+
di.RegisterTyped(container, func(c *di.DIContainer) (*CatalogService, error) {
40+
moduleRepo, err := di.GetTyped[repository.ModuleTemplatesRepository](c)
41+
if err != nil {
42+
return nil, err
43+
}
44+
45+
return NewCatalogService(moduleRepo), nil
46+
})
47+
48+
return container, nil
49+
}
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package dtos
2+
3+
import "github.com/kyma-project/cli.v3/internal/modulesv2/entities"
4+
5+
const KYMA_ORIGIN = "kyma"
6+
const COMMUNITY_ORIGIN = "community"
7+
8+
type CatalogResult struct {
9+
Name string
10+
AvailableVersions []string
11+
Origin string
12+
}
13+
14+
func CatalogResultFromCoreModuleTemplates(coreModuleTemplates []entities.CoreModuleTemplate) []CatalogResult {
15+
results := []CatalogResult{}
16+
17+
// Cache to quickly get an index of module that's already present in the result set
18+
resultsCache := map[string]int{}
19+
20+
for _, coreModuleTemplate := range coreModuleTemplates {
21+
if i, exists := resultsCache[coreModuleTemplate.ModuleName]; exists {
22+
results[i].AvailableVersions = append(results[i].AvailableVersions, coreModuleTemplate.GetVersionWithChannel())
23+
} else {
24+
newResult := CatalogResult{
25+
Name: coreModuleTemplate.ModuleName,
26+
AvailableVersions: []string{coreModuleTemplate.GetVersionWithChannel()},
27+
Origin: KYMA_ORIGIN,
28+
}
29+
results = append(results, newResult)
30+
resultsCache[coreModuleTemplate.ModuleName] = len(results) - 1
31+
}
32+
}
33+
34+
return results
35+
}
36+
37+
func CatalogResultFromCommunityModuleTemplates(communityModuleTemplates []entities.CommunityModuleTemplate) []CatalogResult {
38+
results := []CatalogResult{}
39+
40+
// Cache key: moduleName + origin
41+
resultsCache := map[string]int{}
42+
43+
for _, communityModuleTemplate := range communityModuleTemplates {
44+
origin := getOriginFor(communityModuleTemplate)
45+
cacheKey := communityModuleTemplate.ModuleName + "|" + origin
46+
47+
if i, exists := resultsCache[cacheKey]; exists {
48+
results[i].AvailableVersions = append(results[i].AvailableVersions, communityModuleTemplate.Version)
49+
} else {
50+
newResult := CatalogResult{
51+
Name: communityModuleTemplate.ModuleName,
52+
AvailableVersions: []string{communityModuleTemplate.Version},
53+
Origin: origin,
54+
}
55+
results = append(results, newResult)
56+
resultsCache[cacheKey] = len(results) - 1
57+
}
58+
}
59+
60+
return results
61+
}
62+
63+
func getOriginFor(communityModuleTemplate entities.CommunityModuleTemplate) string {
64+
if communityModuleTemplate.IsExternal() {
65+
return COMMUNITY_ORIGIN
66+
}
67+
68+
return communityModuleTemplate.GetNamespacedName()
69+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
package entities
2+
3+
import "fmt"
4+
5+
type CommunityModuleTemplate struct {
6+
BaseModuleTemplate
7+
sourceURL string
8+
resources map[string]string
9+
}
10+
11+
func NewCommunityModuleTemplate(base *BaseModuleTemplate, sourceURL string, resources map[string]string) *CommunityModuleTemplate {
12+
return &CommunityModuleTemplate{
13+
*base,
14+
sourceURL,
15+
resources,
16+
}
17+
}
18+
19+
func (m *CommunityModuleTemplate) IsExternal() bool {
20+
return m.namespace == ""
21+
}
22+
23+
func (m *CommunityModuleTemplate) GetNamespacedName() string {
24+
return fmt.Sprintf("%s/%s", m.namespace, m.name)
25+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
package entities
2+
3+
import "fmt"
4+
5+
type CoreModuleTemplate struct {
6+
BaseModuleTemplate
7+
Channel string
8+
}
9+
10+
func NewCoreModuleTemplate(base *BaseModuleTemplate, channel string) *CoreModuleTemplate {
11+
return &CoreModuleTemplate{
12+
*base,
13+
channel,
14+
}
15+
}
16+
17+
func (m *CoreModuleTemplate) GetVersionWithChannel() string {
18+
return fmt.Sprintf("%s(%s)", m.Version, m.Channel)
19+
}

0 commit comments

Comments
 (0)