From 7321179ffe5e22b8ab5087a389ac246df4bfd0fc Mon Sep 17 00:00:00 2001 From: Andrzej Pankowski Date: Fri, 21 Jun 2024 13:18:06 +0200 Subject: [PATCH] Add a collective view for the module command (#2139) Co-authored-by: MichalKalke --- go.mod | 2 + go.sum | 14 ++- internal/cmd/modules/modules.go | 71 +++++++---- internal/model/modules.go | 202 ++++++++++++++++++++++++-------- internal/model/types.go | 1 + 5 files changed, 211 insertions(+), 79 deletions(-) diff --git a/go.mod b/go.mod index 92d5cb45e..43e05637d 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.22.2 require ( github.com/gboddin/go-www-authenticate-parser v0.0.0-20230926203616-ec0b649bb077 github.com/google/go-containerregistry v0.19.2 + github.com/olekukonko/tablewriter v0.0.5 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.9.0 k8s.io/api v0.30.2 @@ -44,6 +45,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.16.5 // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mattn/go-runewidth v0.0.9 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/moby/spdystream v0.2.0 // indirect github.com/moby/term v0.5.0 // indirect diff --git a/go.sum b/go.sum index 880e5c7c0..1f0387f00 100644 --- a/go.sum +++ b/go.sum @@ -12,7 +12,7 @@ github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3 github.com/containerd/stargz-snapshotter/estargz v0.14.3 h1:OqlDCK3ZVUO6C3B/5FSkDwbkEETK84kQgEeFwDC+62k= github.com/containerd/stargz-snapshotter/estargz v0.14.3/go.mod h1:KY//uOCIkSuNAHhJogcZtrNHdKrA99/FCCRjE3HD36o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/cpuguy83/go-md2man/v2 v2.0.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -62,8 +62,8 @@ github.com/google/gnostic-models v0.6.8/go.mod h1:5n7qKqH0f5wFt+aWF8CW6pZLLNOfYu github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-containerregistry v0.19.2 h1:TannFKE1QSajsP6hPWb5oJNgKe1IKjHukIKDUmvsV6w= -github.com/google/go-containerregistry v0.19.2/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= +github.com/google/go-containerregistry v0.19.1 h1:yMQ62Al6/V0Z7CqIrrS1iYoA5/oQCm88DeNujc7C1KY= +github.com/google/go-containerregistry v0.19.1/go.mod h1:YCMFNQeeXeLF+dnhhWkqDItx/JSkH01j1Kis4PsjzFI= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= @@ -97,6 +97,8 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/spdystream v0.2.0 h1:cjW1zVyyoiM0T7b6UoySUFqzXMoqRckQtXwGPiBhOM8= @@ -114,6 +116,8 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus= github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= github.com/onsi/ginkgo/v2 v2.17.1 h1:V++EzdbhI4ZV4ev0UTIj0PzhzOcReJFyJaLjtSF55M8= github.com/onsi/ginkgo/v2 v2.17.1/go.mod h1:llBI3WDLL9Z6taip6f33H76YcWtJv+7R3HigUjbIBOs= github.com/onsi/gomega v1.32.0 h1:JRYU78fJ1LPxlckP6Txi/EYqJvjtMrDC04/MM5XRHPk= @@ -132,8 +136,8 @@ github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQD github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= -github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= diff --git a/internal/cmd/modules/modules.go b/internal/cmd/modules/modules.go index e685cbd1c..2a8e99185 100644 --- a/internal/cmd/modules/modules.go +++ b/internal/cmd/modules/modules.go @@ -1,7 +1,6 @@ package modules 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/model" @@ -15,6 +14,7 @@ type modulesConfig struct { catalog bool managed bool installed bool + raw bool } func NewModulesCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { @@ -39,20 +39,19 @@ func NewModulesCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd.Flags().BoolVar(&cfg.catalog, "catalog", false, "List of al available Kyma modules.") cmd.Flags().BoolVar(&cfg.managed, "managed", false, "List of all Kyma modules managed by central control-plane.") cmd.Flags().BoolVar(&cfg.installed, "installed", false, "List of all currently installed Kyma modules.") + cmd.Flags().BoolVar(&cfg.raw, "raw", false, "Simple output format without table rendering.") - cmd.MarkFlagsOneRequired("catalog", "managed", "installed") - cmd.MarkFlagsMutuallyExclusive("catalog", "managed") - cmd.MarkFlagsMutuallyExclusive("catalog", "installed") - cmd.MarkFlagsMutuallyExclusive("managed", "installed") + cmd.MarkFlagsMutuallyExclusive("catalog", "managed", "installed") return cmd } +// listModules collects all the methods responsible for the command and its flags func listModules(cfg *modulesConfig) clierror.Error { var err clierror.Error if cfg.catalog { - err = listAllModules() + err = listModulesCatalog(cfg) if err != nil { return clierror.WrapE(err, clierror.New("failed to list all Kyma modules")) } @@ -75,41 +74,65 @@ func listModules(cfg *modulesConfig) clierror.Error { return nil } - return clierror.WrapE(err, clierror.New("failed to get modules", "please use one of: catalog, managed or installed flags")) + err = collectiveView(cfg) + if err != nil { + return clierror.WrapE(err, clierror.New("failed to list modules")) + } + + return nil } -func listInstalledModules(cfg *modulesConfig) clierror.Error { - installed, err := model.GetInstalledModules(cfg.KubeClientConfig, *cfg.KymaConfig) +// collectiveView combines the list of all available, installed and managed modules +func collectiveView(cfg *modulesConfig) clierror.Error { + catalog, err := model.ModulesCatalog(nil) + if err != nil { + return clierror.WrapE(err, clierror.New("failed to get all Kyma catalog")) + } + installedWith, err := model.InstalledModules(catalog, cfg.KubeClientConfig, *cfg.KymaConfig) if err != nil { return clierror.WrapE(err, clierror.New("failed to get installed Kyma modules")) } - fmt.Println("Installed modules:\n") - for _, rec := range installed { - fmt.Println(rec) + managedWith, err := model.ManagedModules(installedWith, cfg.KubeClientConfig, *cfg.KymaConfig) + if err != nil { + return clierror.WrapE(err, clierror.New("failed to get managed Kyma modules")) } + + model.RenderTable(cfg.raw, managedWith, []string{"NAME", "REPOSITORY", "VERSION INSTALLED", "CONTROL-PLANE"}) + return nil } +// listInstalledModules lists all installed modules +func listInstalledModules(cfg *modulesConfig) clierror.Error { + installed, err := model.InstalledModules(nil, cfg.KubeClientConfig, *cfg.KymaConfig) + if err != nil { + return clierror.WrapE(err, clierror.New("failed to get installed Kyma modules")) + } + + model.RenderTable(cfg.raw, installed, []string{"NAME", "VERSION"}) + + return nil +} + +// listManagedModules lists all managed modules func listManagedModules(cfg *modulesConfig) clierror.Error { - managed, err := model.GetManagedModules(cfg.KubeClientConfig, *cfg.KymaConfig) + managed, err := model.ManagedModules(nil, cfg.KubeClientConfig, *cfg.KymaConfig) if err != nil { return clierror.WrapE(err, clierror.New("failed to get managed Kyma modules")) } - fmt.Println("Managed modules:\n") - for _, rec := range managed { - fmt.Println(rec) - } + + model.RenderTable(cfg.raw, managed, []string{"NAME"}) + return nil } -func listAllModules() clierror.Error { - modules, err := model.GetAllModules() +// listModulesCatalog lists all available modules +func listModulesCatalog(cfg *modulesConfig) clierror.Error { + catalog, err := model.ModulesCatalog(nil) if err != nil { - return clierror.WrapE(err, clierror.New("failed to get all Kyma modules")) - } - fmt.Println("Available modules:\n") - for _, rec := range modules { - fmt.Println(rec) + return clierror.WrapE(err, clierror.New("failed to get all Kyma catalog")) } + + model.RenderTable(cfg.raw, catalog, []string{"NAME", "REPOSITORY"}) return nil } diff --git a/internal/model/modules.go b/internal/model/modules.go index f74cd5285..e55889e43 100644 --- a/internal/model/modules.go +++ b/internal/model/modules.go @@ -4,18 +4,49 @@ import ( "encoding/json" "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/olekukonko/tablewriter" "io" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "net/http" + "os" "strings" ) const URL = "https://raw.githubusercontent.com/kyma-project/community-modules/main/model.json" -func GetAllModules() ([]string, clierror.Error) { +type row []string + +type moduleMap map[string]row + +// ModulesCatalog returns a map of all available modules and their repositories, if the map is nil it will create a new one +func ModulesCatalog(modulesMap moduleMap) (moduleMap, clierror.Error) { + + template, err := getModel() + if err != nil { + return nil, err + } + + catalog := make(moduleMap) + if modulesMap != nil { + catalog = modulesMap + } + + for _, rec := range template { + if modulesMap != nil { + modulesMap[rec.Name] = append(modulesMap[rec.Name], rec.Versions[0].Repository) + } else { + catalog[rec.Name] = append(catalog[rec.Name], rec.Name) + catalog[rec.Name] = append(catalog[rec.Name], rec.Versions[0].Repository) + } + } + return catalog, nil +} + +// getModel returns a list of all available modules from the community-modules repository +func getModel() (Module, clierror.Error) { resp, err := http.Get(URL) if err != nil { return nil, clierror.Wrap(err, clierror.New("while getting modules list from github")) @@ -23,20 +54,39 @@ func GetAllModules() ([]string, clierror.Error) { defer resp.Body.Close() var template Module - - template, respErr := handleResponse(err, resp, template) + template, respErr := handleHTTPResponse(err, resp, template) if respErr != nil { return nil, clierror.WrapE(respErr, clierror.New("while handling response")) } + return template, nil +} - var modules []string - for _, rec := range template { - modules = append(modules, rec.Name) +// ManagedModules returns a map of all managed modules from the cluster +func ManagedModules(modulesMap moduleMap, client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) (moduleMap, clierror.Error) { + + name, err := getManagedList(client, cfg) + if err != nil { + return nil, clierror.WrapE(err, clierror.New("while getting managed modules")) + } + + managed := make(moduleMap) + if modulesMap != nil { + managed = modulesMap + } + + for _, rec := range name { + if modulesMap != nil { + modulesMap[rec] = append(modulesMap[rec], "Managed") + } else { + managed[rec] = append(managed[rec], rec) + } } - return modules, nil + + return managed, nil } -func GetManagedModules(client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) ([]string, clierror.Error) { +// getManagedList gets a list of all managed modules from the Kyma CR +func getManagedList(client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) (row, clierror.Error) { GVRKyma := schema.GroupVersionResource{ Group: "operator.kyma-project.io", Version: "v1beta2", @@ -48,29 +98,71 @@ func GetManagedModules(client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConf return nil, clierror.Wrap(err, clierror.New("while getting Kyma CR")) } - managed, err := getModuleNames(unstruct) + name, err := handleClusterResponse(unstruct) if err != nil { return nil, clierror.Wrap(err, clierror.New("while getting module names from CR")) } + return name, nil +} - return managed, nil +// handleClusterResponse interprets the response and returns a list of managed modules names +func handleClusterResponse(unstruct *unstructured.Unstructured) (row, error) { + var moduleNames row + managedFields := unstruct.GetManagedFields() + for _, field := range managedFields { + var data map[string]interface{} + err := json.Unmarshal(field.FieldsV1.Raw, &data) + if err != nil { + return nil, err + } + + spec, ok := data["f:spec"].(map[string]interface{}) + if !ok { + continue + } + + modules, ok := spec["f:modules"].(map[string]interface{}) + if !ok { + continue + } + + moduleNames = manageNames(moduleNames, modules) + } + return moduleNames, nil } -func GetInstalledModules(client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) ([]string, clierror.Error) { - resp, err := http.Get(URL) +func manageNames(moduleNames row, modules map[string]interface{}) row { + for key := range modules { + if strings.Contains(key, "name") { + name := strings.TrimPrefix(key, "k:{\"name\":\"") + name = strings.Trim(name, "\"}") + moduleNames = append(moduleNames, name) + } + } + return moduleNames +} + +// InstalledModules returns a map of all installed modules from the cluster, regardless whether they are managed or not +func InstalledModules(partialMap moduleMap, client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) (moduleMap, clierror.Error) { + template, err := getModel() if err != nil { - return nil, clierror.Wrap(err, clierror.New("while getting modules list from github")) + return nil, clierror.WrapE(err, clierror.New("while getting installed modules")) } - defer resp.Body.Close() - var template Module + installed := make(moduleMap) + if partialMap != nil { + installed = partialMap + } - template, respErr := handleResponse(err, resp, template) - if respErr != nil { - return nil, clierror.WrapE(respErr, clierror.New("while handling response")) + installed, err = getInstalledModules(partialMap, installed, template, client, cfg) + if err != nil { + return nil, err } - var installed []string + return installed, nil +} + +func getInstalledModules(moduleMap, installed moduleMap, template Module, client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaConfig) (moduleMap, clierror.Error) { for _, rec := range template { managerPath := strings.Split(rec.Versions[0].ManagerPath, "/") managerName := managerPath[len(managerPath)-1] @@ -83,18 +175,29 @@ func GetInstalledModules(client cmdcommon.KubeClientConfig, cfg cmdcommon.KymaCo if !errors.IsNotFound(err) { deploymentImage := strings.Split(deployment.Spec.Template.Spec.Containers[0].Image, "/") installedVersion := strings.Split(deploymentImage[len(deploymentImage)-1], ":") - - if version == installedVersion[len(installedVersion)-1] { - installed = append(installed, rec.Name+" - "+installedVersion[len(installedVersion)-1]) - } else { - installed = append(installed, rec.Name+" - "+"outdated version, latest version is "+version) - } + manageVersion(rec.Name, version, installedVersion, moduleMap, installed) } } return installed, nil } -func handleResponse(err error, resp *http.Response, template Module) (Module, clierror.Error) { +func manageVersion(name, version string, installedVersion row, moduleMap, installed moduleMap) { + if version == installedVersion[len(installedVersion)-1] { + if moduleMap == nil { + installed[name] = append(installed[name], name) + } + installed[name] = append(installed[name], installedVersion[len(installedVersion)-1]) + + } else { + if moduleMap == nil { + installed[name] = append(installed[name], name) + } + installed[name] = append(installed[name], "outdated version, latest is "+version) + } +} + +// handleHTTPResponse reads the response body and unmarshals it into the template +func handleHTTPResponse(err error, resp *http.Response, template Module) (Module, clierror.Error) { bodyText, err := io.ReadAll(resp.Body) if err != nil { return nil, clierror.Wrap(err, clierror.New("while reading http response")) @@ -106,33 +209,32 @@ func handleResponse(err error, resp *http.Response, template Module) (Module, cl return template, nil } -func getModuleNames(unstruct *unstructured.Unstructured) ([]string, error) { - var moduleNames []string - managedFields := unstruct.GetManagedFields() - for _, field := range managedFields { - var data map[string]interface{} - err := json.Unmarshal(field.FieldsV1.Raw, &data) - if err != nil { - return nil, err - } - - spec, ok := data["f:spec"].(map[string]interface{}) - if !ok { - continue +// renderTable renders the table with the provided headers +func RenderTable(raw bool, modulesMap moduleMap, headers []string) { + if raw { + for _, row := range modulesMap { + println(strings.Join(row, "\t")) } + } else { - modules, ok := spec["f:modules"].(map[string]interface{}) - if !ok { - continue + var table [][]string + for _, row := range modulesMap { + table = append(table, row) } - for key := range modules { - if strings.Contains(key, "name") { - name := strings.TrimPrefix(key, "k:{\"name\":\"") - name = strings.Trim(name, "\"}") - moduleNames = append(moduleNames, name) - } - } + twTable := setTable(table) + twTable.SetHeader(headers) + twTable.Render() } - return moduleNames, nil +} + +// setTable sets the table settings for the tablewriter +func setTable(inTable [][]string) *tablewriter.Table { + table := tablewriter.NewWriter(os.Stdout) + table.AppendBulk(inTable) + table.SetRowLine(true) + table.SetAlignment(tablewriter.ALIGN_CENTER) + table.SetColumnAlignment([]int{tablewriter.ALIGN_CENTER, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) + table.SetBorder(false) + return table } diff --git a/internal/model/types.go b/internal/model/types.go index d2f344772..0da478c32 100644 --- a/internal/model/types.go +++ b/internal/model/types.go @@ -11,6 +11,7 @@ type Versions struct { Channels []string `json:"channels,omitempty"` ManagerPath string `json:"managerPath,omitempty"` ManagerImage string `json:"managerImage,omitempty"` + Repository string `json:"repository,omitempty"` Resources []Resources `json:"resources,omitempty"` }