From 11b0a987c7adbb0ec0bb784e939056636187c3e0 Mon Sep 17 00:00:00 2001 From: pPrecel Date: Tue, 26 Nov 2024 14:39:55 +0100 Subject: [PATCH 1/2] list modules based on ModuleTemplates --- internal/kube/kyma/kyma.go | 36 +++++---- internal/modules/modules.go | 80 +++++++++++++++++-- internal/modules/modules_test.go | 128 +++++++++++++++++++++++++------ internal/modules/render.go | 52 +++++-------- internal/modules/render_test.go | 15 +++- 5 files changed, 231 insertions(+), 80 deletions(-) diff --git a/internal/kube/kyma/kyma.go b/internal/kube/kyma/kyma.go index 1f494ab44..ddb5ac038 100644 --- a/internal/kube/kyma/kyma.go +++ b/internal/kube/kyma/kyma.go @@ -12,8 +12,8 @@ import ( ) const ( - defaultKymaName = "default" - defaultKymaNamespace = "kyma-system" + DefaultKymaName = "default" + DefaultKymaNamespace = "kyma-system" ) type Interface interface { @@ -35,31 +35,21 @@ func NewClient(dynamic dynamic.Interface) Interface { } } +// ListModuleReleaseMeta lists ModuleReleaseMeta resources from across the whole cluster func (c *client) ListModuleReleaseMeta(ctx context.Context) (*ModuleReleaseMetaList, error) { return list[ModuleReleaseMetaList](ctx, c.dynamic, GVRModuleReleaseMeta) } +// ListModuleTemplate lists ModuleTemplate resources from across the whole cluster func (c *client) ListModuleTemplate(ctx context.Context) (*ModuleTemplateList, error) { return list[ModuleTemplateList](ctx, c.dynamic, GVRModuleTemplate) } -func list[T any](ctx context.Context, client dynamic.Interface, gvr schema.GroupVersionResource) (*T, error) { - list, err := client.Resource(gvr). - List(ctx, metav1.ListOptions{}) - if err != nil { - return nil, err - } - - structuredList := new(T) - err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), structuredList) - return structuredList, err -} - // GetDefaultKyma gets the default Kyma CR from the kyma-system namespace and cast it to the Kyma structure func (c *client) GetDefaultKyma(ctx context.Context) (*Kyma, error) { u, err := c.dynamic.Resource(GVRKyma). - Namespace(defaultKymaNamespace). - Get(ctx, defaultKymaName, metav1.GetOptions{}) + Namespace(DefaultKymaNamespace). + Get(ctx, DefaultKymaName, metav1.GetOptions{}) if err != nil { return nil, err } @@ -78,7 +68,7 @@ func (c *client) UpdateDefaultKyma(ctx context.Context, obj *Kyma) error { } _, err = c.dynamic.Resource(GVRKyma). - Namespace(defaultKymaNamespace). + Namespace(DefaultKymaNamespace). Update(ctx, &unstructured.Unstructured{Object: u}, metav1.UpdateOptions{}) return err @@ -133,3 +123,15 @@ func disableModule(kymaCR *Kyma, moduleName string) *Kyma { return kymaCR } + +func list[T any](ctx context.Context, client dynamic.Interface, gvr schema.GroupVersionResource) (*T, error) { + list, err := client.Resource(gvr). + List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + + structuredList := new(T) + err = runtime.DefaultUnstructuredConverter.FromUnstructured(list.UnstructuredContent(), structuredList) + return structuredList, err +} diff --git a/internal/modules/modules.go b/internal/modules/modules.go index 9f02de905..059481115 100644 --- a/internal/modules/modules.go +++ b/internal/modules/modules.go @@ -4,11 +4,26 @@ import ( "context" "github.com/kyma-project/cli.v3/internal/kube/kyma" + apierrors "k8s.io/apimachinery/pkg/api/errors" ) type Module struct { - Name string - Versions []ModuleVersion + Name string + Versions []ModuleVersion + InstallDetails ModuleInstallDetails +} + +type Managed string + +const ( + ManagedTrue Managed = "true" + ManagedFalse Managed = "false" +) + +type ModuleInstallDetails struct { + Version string + Channel string + Managed Managed } type ModuleVersion struct { @@ -30,25 +45,38 @@ func List(ctx context.Context, client kyma.Interface) (ModulesList, error) { return nil, err } + defaultKyma, err := client.GetDefaultKyma(ctx) + if err != nil && !apierrors.IsNotFound(err) { + return nil, err + } + + // assign spec to another value to operate on nil-free value + defaultKymaSpec := kyma.KymaSpec{} + if defaultKyma != nil { + defaultKymaSpec = defaultKyma.Spec + } + modulesList := ModulesList{} for _, moduleTemplate := range moduleTemplates.Items { + moduleName := moduleTemplate.Spec.ModuleName version := ModuleVersion{ Version: moduleTemplate.Spec.Version, Repository: moduleTemplate.Spec.Info.Repository, Channel: getAssignedChannel( *modulereleasemetas, - moduleTemplate.Spec.ModuleName, + moduleName, moduleTemplate.Spec.Version, ), } - if i := getModuleIndex(modulesList, moduleTemplate.Spec.ModuleName); i != -1 { + if i := getModuleIndex(modulesList, moduleName); i != -1 { // append version if module with same name is in the list modulesList[i].Versions = append(modulesList[i].Versions, version) } else { // otherwise create anew record in the list modulesList = append(modulesList, Module{ - Name: moduleTemplate.Spec.ModuleName, + Name: moduleName, + InstallDetails: getInstallDetails(defaultKymaSpec, *modulereleasemetas, moduleName), Versions: []ModuleVersion{ version, }, @@ -59,6 +87,28 @@ func List(ctx context.Context, client kyma.Interface) (ModulesList, error) { return modulesList, nil } +func getInstallDetails(kymaSpec kyma.KymaSpec, releaseMetas kyma.ModuleReleaseMetaList, moduleName string) ModuleInstallDetails { + for _, module := range kymaSpec.Modules { + if module.Name == moduleName { + moduleChannel := kymaSpec.Channel + if module.Channel != "" { + moduleChannel = module.Channel + } + + return ModuleInstallDetails{ + Channel: moduleChannel, + Version: getAssignedVersion(releaseMetas, module.Name, moduleChannel), + Managed: ManagedTrue, + } + } + } + + // TODO: support community modules + + // return empty struct because module is not installed + return ModuleInstallDetails{} +} + // look for channel assigned to version with specified moduleName func getAssignedChannel(releaseMetas kyma.ModuleReleaseMetaList, moduleName, version string) string { for _, releaseMeta := range releaseMetas.Items { @@ -69,6 +119,16 @@ func getAssignedChannel(releaseMetas kyma.ModuleReleaseMetaList, moduleName, ver return "" } +// look for version assigned to channel with specified moduleName +func getAssignedVersion(releaseMetas kyma.ModuleReleaseMetaList, moduleName, channel string) string { + for _, releaseMeta := range releaseMetas.Items { + if releaseMeta.Spec.ModuleName == moduleName { + return getVersionFromAssignments(releaseMeta.Spec.Channels, channel) + } + } + return "" +} + func getChannelFromAssignments(assignments []kyma.ChannelVersionAssignment, version string) string { for _, assignment := range assignments { if assignment.Version == version { @@ -79,6 +139,16 @@ func getChannelFromAssignments(assignments []kyma.ChannelVersionAssignment, vers return "" } +func getVersionFromAssignments(assignments []kyma.ChannelVersionAssignment, channel string) string { + for _, assignment := range assignments { + if assignment.Channel == channel { + return assignment.Version + } + } + + return "" +} + // return index of module with given name. if not exists return -1 func getModuleIndex(list ModulesList, name string) int { for i := range list { diff --git a/internal/modules/modules_test.go b/internal/modules/modules_test.go index be281487b..23a7595aa 100644 --- a/internal/modules/modules_test.go +++ b/internal/modules/modules_test.go @@ -124,31 +124,73 @@ var ( }, }, } -) -func TestList(t *testing.T) { - t.Run("list modules from cluster", func(t *testing.T) { - scheme := runtime.NewScheme() - scheme.AddKnownTypes(kyma.GVRModuleTemplate.GroupVersion()) - scheme.AddKnownTypes(kyma.GVRModuleReleaseMeta.GroupVersion()) - dynamicClient := dynamic_fake.NewSimpleDynamicClient(scheme, - &testModuleTemplate1, - &testModuleTemplate2, - &testModuleTemplate3, - &testModuleTemplate4, - &testReleaseMeta1, - &testReleaseMeta2, - ) - - modules, err := List(context.Background(), kyma.NewClient(dynamicClient)) + testKymaCR = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "operator.kyma-project.io/v1beta2", + "kind": "Kyma", + "metadata": map[string]interface{}{ + "name": kyma.DefaultKymaName, + "namespace": kyma.DefaultKymaNamespace, + }, + "spec": map[string]interface{}{ + "channel": "fast", + "modules": []interface{}{ + map[string]interface{}{ + "name": "serverless", + "channel": "regular", + }, + map[string]interface{}{ + "name": "keda", + "channel": "fast", + }, + }, + }, + }, + } - require.NoError(t, err) - require.Equal(t, ModulesList(fixModuleList()), modules) - }) -} + testManagedModuleList = []Module{ + { + Name: "keda", + InstallDetails: ModuleInstallDetails{ + Managed: ManagedTrue, + Channel: "fast", + Version: "0.2", + }, + Versions: []ModuleVersion{ + { + Repository: "url-3", + Version: "0.1", + Channel: "regular", + }, + { + Version: "0.2", + Channel: "fast", + }, + }, + }, + { + Name: "serverless", + InstallDetails: ModuleInstallDetails{ + Managed: ManagedTrue, + Channel: "regular", + Version: "0.0.2", + }, + Versions: []ModuleVersion{ + { + Repository: "url-1", + Version: "0.0.1", + Channel: "fast", + }, + { + Repository: "url-2", + Version: "0.0.2", + }, + }, + }, + } -func fixModuleList() []Module { - return []Module{ + testModuleList = []Module{ { Name: "keda", Versions: []ModuleVersion{ @@ -178,4 +220,46 @@ func fixModuleList() []Module { }, }, } +) + +func TestList(t *testing.T) { + t.Run("list modules from cluster without Kyma CR", func(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(kyma.GVRModuleTemplate.GroupVersion()) + scheme.AddKnownTypes(kyma.GVRModuleReleaseMeta.GroupVersion()) + dynamicClient := dynamic_fake.NewSimpleDynamicClient(scheme, + &testModuleTemplate1, + &testModuleTemplate2, + &testModuleTemplate3, + &testModuleTemplate4, + &testReleaseMeta1, + &testReleaseMeta2, + ) + + modules, err := List(context.Background(), kyma.NewClient(dynamicClient)) + + require.NoError(t, err) + require.Equal(t, ModulesList(testModuleList), modules) + }) + + t.Run("list managed modules from cluster", func(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(kyma.GVRModuleTemplate.GroupVersion()) + scheme.AddKnownTypes(kyma.GVRModuleReleaseMeta.GroupVersion()) + scheme.AddKnownTypes(kyma.GVRKyma.GroupVersion()) + dynamicClient := dynamic_fake.NewSimpleDynamicClient(scheme, + &testModuleTemplate1, + &testModuleTemplate2, + &testModuleTemplate3, + &testModuleTemplate4, + &testReleaseMeta1, + &testReleaseMeta2, + &testKymaCR, + ) + + modules, err := List(context.Background(), kyma.NewClient(dynamicClient)) + + require.NoError(t, err) + require.Equal(t, ModulesList(testManagedModuleList), modules) + }) } diff --git a/internal/modules/render.go b/internal/modules/render.go index 429345580..872414264 100644 --- a/internal/modules/render.go +++ b/internal/modules/render.go @@ -20,9 +20,14 @@ type TableInfo struct { var ( ModulesTableInfo = TableInfo{ - Header: []string{"NAME", "REPOSITORY", "VERSIONS"}, + Header: []string{"NAME", "VERSIONS", "INSTALLED", "MANAGED"}, RowConverter: func(m Module) []string { - return []string{m.Name, convertRepositories(m.Versions), convertVersions(m.Versions)} + return []string{ + m.Name, + convertVersions(m.Versions), + convertInstall(m.InstallDetails), + string(m.InstallDetails.Managed), + } }, } ) @@ -51,24 +56,21 @@ func convertModuleListToTable(modulesList ModulesList, rowConverter RowConverter return result } -// renderTable renders the table with the provided headers +// renderTable renders the table with the provided headers and data func renderTable(writer io.Writer, modulesData [][]string, headers []string) { - var table [][]string - table = append(table, modulesData...) - - twTable := setTable(writer, table) + twTable := setTable(writer) + twTable.AppendBulk(modulesData) twTable.SetHeader(headers) twTable.Render() } // setTable sets the table settings for the tablewriter -func setTable(writer io.Writer, inTable [][]string) *tablewriter.Table { +func setTable(writer io.Writer) *tablewriter.Table { table := tablewriter.NewWriter(writer) - table.AppendBulk(inTable) table.SetRowLine(false) table.SetHeaderLine(false) table.SetColumnSeparator("") - table.SetAlignment(tablewriter.ALIGN_CENTER) + table.SetAlignment(tablewriter.ALIGN_LEFT) table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) table.SetBorder(false) @@ -77,31 +79,13 @@ func setTable(writer io.Writer, inTable [][]string) *tablewriter.Table { return table } -// convert versions to string containing links to repositories without duplicates separated by '\n' -func convertRepositories(versions []ModuleVersion) string { - values := []string{} - for _, version := range versions { - if version.Repository == "" { - // ignore if repository is empty - continue - } - - if !contains(values, version.Repository) { - values = append(values, version.Repository) - } - } - - return strings.Join(values, ", ") -} - -func contains(in []string, value string) bool { - for _, inValue := range in { - if inValue == value { - return true - } +// convert version and channel into field in format 'version (channel)' for core modules and 'version' for community ones +func convertInstall(details ModuleInstallDetails) string { + if details.Channel != "" { + return fmt.Sprintf("%s(%s)", details.Version, details.Channel) } - return false + return details.Version } // convert versions to string containing values separated with '\n' @@ -111,7 +95,7 @@ func convertVersions(versions []ModuleVersion) string { for i, version := range versions { value := version.Version if version.Channel != "" { - value += fmt.Sprintf(" (%s)", version.Channel) + value += fmt.Sprintf("(%s)", version.Channel) } values[i] = value diff --git a/internal/modules/render_test.go b/internal/modules/render_test.go index b9c1441c1..ca2334bec 100644 --- a/internal/modules/render_test.go +++ b/internal/modules/render_test.go @@ -9,17 +9,28 @@ import ( ) const ( - testModulesTableView = "NAME \tREPOSITORY \tVERSIONS \nkeda \turl-3 \t0.1 (regular), 0.2 (fast)\t\nserverless\turl-1, url-2\t0.0.1 (fast), 0.0.2 \t\n" + testModulesTableView = "NAME \tREPOSITORY \tVERSIONS \tINSTALLED\tMANAGED \n keda \t url-3 \t0.1 (regular), 0.2 (fast)\t false \t false \t\nserverless\turl-1, url-2\t 0.0.1 (fast), 0.0.2 \t false \t false \t\n" + testManagedModulesTableView = "NAME \tREPOSITORY \tVERSIONS \tINSTALLED\tMANAGED \n keda \t url-3 \t0.1 (regular), 0.2 (fast)\t true \t true \t\nserverless\turl-1, url-2\t 0.0.1 (fast), 0.0.2 \t true \t true \t\n" ) func TestRender(t *testing.T) { t.Run("render table from modules", func(t *testing.T) { buffer := bytes.NewBuffer([]byte{}) - render(buffer, fixModuleList(), ModulesTableInfo) + render(buffer, testModuleList, ModulesTableInfo) tableViewBytes, err := io.ReadAll(buffer) require.NoError(t, err) require.Equal(t, testModulesTableView, string(tableViewBytes)) }) + + t.Run("render table from managed modules", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + + render(buffer, testManagedModuleList, ModulesTableInfo) + + tableViewBytes, err := io.ReadAll(buffer) + require.NoError(t, err) + require.Equal(t, testManagedModulesTableView, string(tableViewBytes)) + }) } From e53effc315c4f663c417902bb120ba9980070297 Mon Sep 17 00:00:00 2001 From: pPrecel Date: Thu, 28 Nov 2024 12:49:00 +0100 Subject: [PATCH 2/2] get info from kymaCR status --- internal/kube/kyma/types.go | 2 ++ internal/modules/modules.go | 58 ++++++++++++-------------------- internal/modules/modules_test.go | 22 +++++++++--- internal/modules/render_test.go | 4 +-- 4 files changed, 42 insertions(+), 44 deletions(-) diff --git a/internal/kube/kyma/types.go b/internal/kube/kyma/types.go index d54d63968..117d628b7 100644 --- a/internal/kube/kyma/types.go +++ b/internal/kube/kyma/types.go @@ -114,6 +114,7 @@ type Module struct { ControllerName string `json:"controller,omitempty"` Channel string `json:"channel,omitempty"` CustomResourcePolicy string `json:"customResourcePolicy,omitempty"` + Managed bool `json:"managed,omitempty"` } // KymaStatus defines the observed state of Kyma @@ -125,6 +126,7 @@ type ModuleStatus struct { Name string `json:"name"` Channel string `json:"channel,omitempty"` Version string `json:"version,omitempty"` + State string `json:"state,omitempty"` } // ModuleFromInterface converts a map retrieved from the Unstructured kyma CR to a Module struct. diff --git a/internal/modules/modules.go b/internal/modules/modules.go index 059481115..085b780e7 100644 --- a/internal/modules/modules.go +++ b/internal/modules/modules.go @@ -2,6 +2,7 @@ package modules import ( "context" + "strconv" "github.com/kyma-project/cli.v3/internal/kube/kyma" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -50,12 +51,6 @@ func List(ctx context.Context, client kyma.Interface) (ModulesList, error) { return nil, err } - // assign spec to another value to operate on nil-free value - defaultKymaSpec := kyma.KymaSpec{} - if defaultKyma != nil { - defaultKymaSpec = defaultKyma.Spec - } - modulesList := ModulesList{} for _, moduleTemplate := range moduleTemplates.Items { moduleName := moduleTemplate.Spec.ModuleName @@ -76,7 +71,7 @@ func List(ctx context.Context, client kyma.Interface) (ModulesList, error) { // otherwise create anew record in the list modulesList = append(modulesList, Module{ Name: moduleName, - InstallDetails: getInstallDetails(defaultKymaSpec, *modulereleasemetas, moduleName), + InstallDetails: getInstallDetails(defaultKyma, *modulereleasemetas, moduleName), Versions: []ModuleVersion{ version, }, @@ -87,18 +82,16 @@ func List(ctx context.Context, client kyma.Interface) (ModulesList, error) { return modulesList, nil } -func getInstallDetails(kymaSpec kyma.KymaSpec, releaseMetas kyma.ModuleReleaseMetaList, moduleName string) ModuleInstallDetails { - for _, module := range kymaSpec.Modules { - if module.Name == moduleName { - moduleChannel := kymaSpec.Channel - if module.Channel != "" { - moduleChannel = module.Channel - } - - return ModuleInstallDetails{ - Channel: moduleChannel, - Version: getAssignedVersion(releaseMetas, module.Name, moduleChannel), - Managed: ManagedTrue, +func getInstallDetails(kyma *kyma.Kyma, releaseMetas kyma.ModuleReleaseMetaList, moduleName string) ModuleInstallDetails { + if kyma != nil { + for _, module := range kyma.Status.Modules { + if module.Name == moduleName { + moduleVersion := module.Version + return ModuleInstallDetails{ + Channel: getAssignedChannel(releaseMetas, module.Name, moduleVersion), + Managed: getManaged(kyma.Spec.Modules, moduleName), + Version: moduleVersion, + } } } } @@ -109,21 +102,22 @@ func getInstallDetails(kymaSpec kyma.KymaSpec, releaseMetas kyma.ModuleReleaseMe return ModuleInstallDetails{} } -// look for channel assigned to version with specified moduleName -func getAssignedChannel(releaseMetas kyma.ModuleReleaseMetaList, moduleName, version string) string { - for _, releaseMeta := range releaseMetas.Items { - if releaseMeta.Spec.ModuleName == moduleName { - return getChannelFromAssignments(releaseMeta.Spec.Channels, version) +// look for value of managed for specific moduleName +func getManaged(specModules []kyma.Module, moduleName string) Managed { + for _, module := range specModules { + if module.Name == moduleName { + return Managed(strconv.FormatBool(module.Managed)) } } + return "" } -// look for version assigned to channel with specified moduleName -func getAssignedVersion(releaseMetas kyma.ModuleReleaseMetaList, moduleName, channel string) string { +// look for channel assigned to version with specified moduleName +func getAssignedChannel(releaseMetas kyma.ModuleReleaseMetaList, moduleName, version string) string { for _, releaseMeta := range releaseMetas.Items { if releaseMeta.Spec.ModuleName == moduleName { - return getVersionFromAssignments(releaseMeta.Spec.Channels, channel) + return getChannelFromAssignments(releaseMeta.Spec.Channels, version) } } return "" @@ -139,16 +133,6 @@ func getChannelFromAssignments(assignments []kyma.ChannelVersionAssignment, vers return "" } -func getVersionFromAssignments(assignments []kyma.ChannelVersionAssignment, channel string) string { - for _, assignment := range assignments { - if assignment.Channel == channel { - return assignment.Version - } - } - - return "" -} - // return index of module with given name. if not exists return -1 func getModuleIndex(list ModulesList, name string) int { for i := range list { diff --git a/internal/modules/modules_test.go b/internal/modules/modules_test.go index 23a7595aa..97682ecfc 100644 --- a/internal/modules/modules_test.go +++ b/internal/modules/modules_test.go @@ -138,11 +138,23 @@ var ( "modules": []interface{}{ map[string]interface{}{ "name": "serverless", - "channel": "regular", + "managed": false, }, map[string]interface{}{ "name": "keda", - "channel": "fast", + "managed": true, + }, + }, + }, + "status": map[string]interface{}{ + "modules": []interface{}{ + map[string]interface{}{ + "name": "serverless", + "version": "0.0.1", + }, + map[string]interface{}{ + "name": "keda", + "version": "0.2", }, }, }, @@ -172,9 +184,9 @@ var ( { Name: "serverless", InstallDetails: ModuleInstallDetails{ - Managed: ManagedTrue, - Channel: "regular", - Version: "0.0.2", + Managed: ManagedFalse, + Channel: "fast", + Version: "0.0.1", }, Versions: []ModuleVersion{ { diff --git a/internal/modules/render_test.go b/internal/modules/render_test.go index ca2334bec..264ae1553 100644 --- a/internal/modules/render_test.go +++ b/internal/modules/render_test.go @@ -9,8 +9,8 @@ import ( ) const ( - testModulesTableView = "NAME \tREPOSITORY \tVERSIONS \tINSTALLED\tMANAGED \n keda \t url-3 \t0.1 (regular), 0.2 (fast)\t false \t false \t\nserverless\turl-1, url-2\t 0.0.1 (fast), 0.0.2 \t false \t false \t\n" - testManagedModulesTableView = "NAME \tREPOSITORY \tVERSIONS \tINSTALLED\tMANAGED \n keda \t url-3 \t0.1 (regular), 0.2 (fast)\t true \t true \t\nserverless\turl-1, url-2\t 0.0.1 (fast), 0.0.2 \t true \t true \t\n" + testModulesTableView = "NAME \tVERSIONS \tINSTALLED\tMANAGED \nkeda \t0.1(regular), 0.2(fast)\t \t \t\nserverless\t0.0.1(fast), 0.0.2 \t \t \t\n" + testManagedModulesTableView = "NAME \tVERSIONS \tINSTALLED \tMANAGED \nkeda \t0.1(regular), 0.2(fast)\t0.2(fast) \ttrue \t\nserverless\t0.0.1(fast), 0.0.2 \t0.0.1(fast)\tfalse \t\n" ) func TestRender(t *testing.T) {