diff --git a/go.mod b/go.mod index 88429dca2..f98668a4a 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.23.2 toolchain go1.23.3 require ( - github.com/avast/retry-go v3.0.0+incompatible github.com/docker/cli v27.3.1+incompatible github.com/docker/docker v27.3.1+incompatible github.com/gboddin/go-www-authenticate-parser v0.0.0-20230926203616-ec0b649bb077 @@ -17,7 +16,6 @@ require ( github.com/pkg/errors v0.9.1 github.com/spf13/cobra v1.8.1 github.com/stretchr/testify v1.10.0 - golang.org/x/mod v0.22.0 gopkg.in/yaml.v3 v3.0.1 istio.io/client-go v1.24.0 k8s.io/api v0.31.3 diff --git a/go.sum b/go.sum index f8aeb2910..647068051 100644 --- a/go.sum +++ b/go.sum @@ -7,8 +7,6 @@ github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERo github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/avast/retry-go v3.0.0+incompatible h1:4SOWQ7Qs+oroOTQOYnAHqelpCO0biHSxpiH9JdtuBj0= -github.com/avast/retry-go v3.0.0+incompatible/go.mod h1:XtSnn+n/sHqQIpZ10K1qAevBhOOCWBLXXy3hyiqqBrY= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= @@ -233,8 +231,6 @@ golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWB golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= -golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= diff --git a/internal/cmd/alpha/add/add.go b/internal/cmd/alpha/add/add.go deleted file mode 100644 index 40d0da075..000000000 --- a/internal/cmd/alpha/add/add.go +++ /dev/null @@ -1,55 +0,0 @@ -package add - -import ( - "github.com/kyma-project/cli.v3/internal/clierror" - "github.com/kyma-project/cli.v3/internal/cmd/alpha/remove/managed" - "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/kyma-project/cli.v3/internal/communitymodules/cluster" - "github.com/kyma-project/cli.v3/internal/kube/resources" - "github.com/spf13/cobra" -) - -type addConfig struct { - *cmdcommon.KymaConfig - - modules []string - crs []string -} - -func NewAddCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { - cfg := addConfig{ - KymaConfig: kymaConfig, - } - - cmd := &cobra.Command{ - Use: "add", - Short: "Adds Kyma modules.", - Long: `Use this command to add Kyma modules`, - Run: func(_ *cobra.Command, _ []string) { - clierror.Check(runAdd(&cfg)) - }, - } - - cmd.AddCommand(managed.NewManagedCMD(kymaConfig)) - - cmd.Flags().StringSliceVar(&cfg.modules, "module", []string{}, "Name and version of the modules to add. Example: --module serverless,keda:1.1.1,etc...") - cmd.Flags().StringSliceVar(&cfg.crs, "cr", []string{}, "Path to the custom CR file") - - return cmd -} - -func runAdd(cfg *addConfig) clierror.Error { - cliErr := cluster.AssureNamespace(cfg.Ctx, cfg.KubeClient.Static(), "kyma-system") - if cliErr != nil { - return cliErr - } - - crs, err := resources.ReadFromFiles(cfg.crs...) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to read CRs from input paths")) - } - - modules := cluster.ParseModules(cfg.modules) - - return cluster.ApplySpecifiedModules(cfg.Ctx, cfg.KubeClient.RootlessDynamic(), modules, crs) -} diff --git a/internal/cmd/alpha/add/managed/managed.go b/internal/cmd/alpha/add/managed/managed.go deleted file mode 100644 index 8e113d880..000000000 --- a/internal/cmd/alpha/add/managed/managed.go +++ /dev/null @@ -1,38 +0,0 @@ -package managed - -import ( - "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/spf13/cobra" -) - -type managedConfig struct { - *cmdcommon.KymaConfig - - module string - channel string -} - -func NewManagedCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { - config := &managedConfig{ - KymaConfig: kymaConfig, - } - - cmd := &cobra.Command{ - Use: "managed", - Short: "Add managed Kyma module in a managed Kyma instance", - RunE: func(_ *cobra.Command, _ []string) error { - return runAddManaged(config) - }, - } - - cmd.Flags().StringVar(&config.module, "module", "", "Name of the module to add") - cmd.Flags().StringVar(&config.channel, "channel", "", "Name of the Kyma channel to use for the module") - - _ = cmd.MarkFlagRequired("module") - - return cmd -} - -func runAddManaged(config *managedConfig) error { - return config.KubeClient.Kyma().EnableModule(config.Ctx, config.module, config.channel) -} diff --git a/internal/cmd/alpha/alpha.go b/internal/cmd/alpha/alpha.go index 81d997af0..2636e2291 100644 --- a/internal/cmd/alpha/alpha.go +++ b/internal/cmd/alpha/alpha.go @@ -3,7 +3,6 @@ package alpha import ( "github.com/kyma-project/cli.v3/internal/clierror" "github.com/kyma-project/cli.v3/internal/cmd/alpha/access" - "github.com/kyma-project/cli.v3/internal/cmd/alpha/add" "github.com/kyma-project/cli.v3/internal/cmd/alpha/app" "github.com/kyma-project/cli.v3/internal/cmd/alpha/hana" "github.com/kyma-project/cli.v3/internal/cmd/alpha/modules" @@ -12,7 +11,6 @@ import ( "github.com/kyma-project/cli.v3/internal/cmd/alpha/referenceinstance" "github.com/kyma-project/cli.v3/internal/cmd/alpha/registry/config" "github.com/kyma-project/cli.v3/internal/cmd/alpha/registry/imageimport" - "github.com/kyma-project/cli.v3/internal/cmd/alpha/remove" "github.com/kyma-project/cli.v3/internal/cmd/alpha/templates" "github.com/kyma-project/cli.v3/internal/cmdcommon" "github.com/spf13/cobra" @@ -32,14 +30,12 @@ func NewAlphaCMD() (*cobra.Command, clierror.Error) { } cmd.AddCommand(access.NewAccessCMD(kymaConfig)) - cmd.AddCommand(add.NewAddCMD(kymaConfig)) cmd.AddCommand(app.NewAppCMD(kymaConfig)) cmd.AddCommand(hana.NewHanaCMD(kymaConfig)) cmd.AddCommand(modules.NewModulesCMD(kymaConfig)) cmd.AddCommand(oidc.NewOIDCCMD(kymaConfig)) cmd.AddCommand(provision.NewProvisionCMD()) cmd.AddCommand(referenceinstance.NewReferenceInstanceCMD(kymaConfig)) - cmd.AddCommand(remove.NewRemoveCMD(kymaConfig)) cmds := kymaConfig.BuildExtensions(&cmdcommon.TemplateCommandsList{ // list of template commands deffinitions diff --git a/internal/cmd/alpha/remove/managed/managed.go b/internal/cmd/alpha/remove/managed/managed.go deleted file mode 100644 index 7cbc0a083..000000000 --- a/internal/cmd/alpha/remove/managed/managed.go +++ /dev/null @@ -1,41 +0,0 @@ -package managed - -import ( - "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/spf13/cobra" -) - -type managedConfig struct { - *cmdcommon.KymaConfig - - module string -} - -func NewManagedCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { - config := &managedConfig{ - KymaConfig: kymaConfig, - } - - cmd := &cobra.Command{ - Use: "managed", - Short: "Remove Kyma module on a managed Kyma instance", - RunE: func(_ *cobra.Command, _ []string) error { - return runRemoveManaged(config) - }, - } - - cmd.Flags().StringVar(&config.module, "module", "", "Name of the module to remove") - - _ = cmd.MarkFlagRequired("module") - - return cmd -} - -func runRemoveManaged(config *managedConfig) error { - client, err := config.GetKubeClient() - if err != nil { - return err - } - - return client.Kyma().DisableModule(config.Ctx, config.module) -} diff --git a/internal/cmd/alpha/remove/remove.go b/internal/cmd/alpha/remove/remove.go deleted file mode 100644 index 61d96d64c..000000000 --- a/internal/cmd/alpha/remove/remove.go +++ /dev/null @@ -1,47 +0,0 @@ -package remove - -import ( - "github.com/kyma-project/cli.v3/internal/clierror" - "github.com/kyma-project/cli.v3/internal/cmd/alpha/remove/managed" - "github.com/kyma-project/cli.v3/internal/cmdcommon" - "github.com/kyma-project/cli.v3/internal/communitymodules/cluster" - "github.com/spf13/cobra" -) - -type removeConfig struct { - *cmdcommon.KymaConfig - - modules []string -} - -func NewRemoveCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { - cfg := removeConfig{ - KymaConfig: kymaConfig, - } - - cmd := &cobra.Command{ - Use: "remove", - Short: "Remove Kyma modules.", - Long: `Use this command to remove Kyma modules`, - Run: func(_ *cobra.Command, _ []string) { - clierror.Check(runRemove(&cfg)) - }, - DisableFlagsInUseLine: true, - } - - cmd.AddCommand(managed.NewManagedCMD(kymaConfig)) - cmd.Flags().StringSliceVar(&cfg.modules, "module", []string{}, "Name and version of the modules to remove. Example: --module serverless,keda:1.1.1,etc...") - _ = cmd.MarkFlagRequired("module") - - return cmd -} - -func runRemove(cfg *removeConfig) clierror.Error { - modules := cluster.ParseModules(cfg.modules) - client, err := cfg.GetKubeClientWithClierr() - if err != nil { - return err - } - - return cluster.RemoveSpecifiedModules(cfg.Ctx, client.RootlessDynamic(), modules) -} diff --git a/internal/communitymodules/cluster/modules.go b/internal/communitymodules/cluster/modules.go deleted file mode 100644 index 3b11e92aa..000000000 --- a/internal/communitymodules/cluster/modules.go +++ /dev/null @@ -1,239 +0,0 @@ -package cluster - -import ( - "context" - "fmt" - "net/http" - "slices" - "strings" - "time" - - "github.com/avast/retry-go" - "github.com/kyma-project/cli.v3/internal/clierror" - "github.com/kyma-project/cli.v3/internal/communitymodules" - "github.com/kyma-project/cli.v3/internal/kube/resources" - "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" - "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -type ModuleInfo struct { - Name string - Version string -} - -// ParseModules returns ModuleInfo struct based on the string input. -// Can convert 'name' or 'name:version' into struct -func ParseModules(modules []string) []ModuleInfo { - // TODO: I can't find better place for this function (move it) - var moduleInfo []ModuleInfo - for _, module := range modules { - if module == "" { - // skip empty strings - continue - } - - elems := strings.Split(module, ":") - info := ModuleInfo{ - Name: elems[0], - } - - if len(elems) > 1 { - info.Version = elems[1] - } - - moduleInfo = append(moduleInfo, info) - } - - return moduleInfo -} - -// ApplySpecifiedModules applies modules to the cluster based on the resources from the community module json (Github) -// if module cr is in the given crs list then it will be applied instead of one from the community module json -func ApplySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, desiredModules []ModuleInfo, crs []unstructured.Unstructured) clierror.Error { - available, err := communitymodules.GetAvailableModules() - if err != nil { - return err - } - - modules, err := downloadSpecifiedModules(desiredModules, available) - if err != nil { - return err - } - - return applySpecifiedModules(ctx, client, modules, crs) -} - -func RemoveSpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, desiredModules []ModuleInfo) clierror.Error { - available, err := communitymodules.GetAvailableModules() - if err != nil { - return err - } - - modules, err := downloadSpecifiedModules(desiredModules, available) - if err != nil { - return err - } - - return removeSpecifiedModules(ctx, client, modules) -} - -type moduleDetails struct { - name string - version string - cr unstructured.Unstructured - resources []unstructured.Unstructured -} - -func downloadSpecifiedModules(desiredModules []ModuleInfo, availableModules communitymodules.Modules) ([]moduleDetails, clierror.Error) { - modules := []moduleDetails{} - for _, module := range availableModules { - moduleInfo := containsModule(module.Name, desiredModules) - if moduleInfo == nil { - // module is not specified - continue - } - - desiredVersion := getDesiredVersion(*moduleInfo, module.Versions) - - crURL := desiredVersion.CrYaml - cr, err := downloadUnstructuredList(crURL) - if err != nil { - return nil, clierror.Wrap(err, clierror.New(fmt.Sprintf("failed to download cr from '%s' url", crURL))) - } - - manifestURL := desiredVersion.DeploymentYaml - resources, err := downloadUnstructuredList(manifestURL) - if err != nil { - return nil, clierror.Wrap(err, clierror.New(fmt.Sprintf("failed to download manifest from '%s' url", manifestURL))) - } - - modules = append(modules, moduleDetails{ - name: module.Name, - version: desiredVersion.Version, - cr: cr[0], // its expected that cr will always have len==1 - resources: resources, - }) - } - - return modules, nil -} - -func downloadUnstructuredList(url string) ([]unstructured.Unstructured, error) { - resp, err := http.Get(url) - if err != nil { - return nil, err - } - defer resp.Body.Close() - - if resp.StatusCode != 200 { - return nil, fmt.Errorf("unexpected status code '%d'", resp.StatusCode) - } - - return resources.DecodeYaml(resp.Body) -} - -func applySpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, desiredModules []moduleDetails, customConfig []unstructured.Unstructured) clierror.Error { - for _, module := range desiredModules { - fmt.Printf("Applying %s module\n", module.name) - err := client.ApplyMany(ctx, module.resources) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to apply module resources")) - } - - fmt.Println("Applying CR") - cr := chooseModuleCR(module, customConfig) - - err = client.Apply(ctx, &cr) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to apply module cr")) - } - } - return nil -} - -func chooseModuleCR(module moduleDetails, customConfig []unstructured.Unstructured) unstructured.Unstructured { - customCRIndex := slices.IndexFunc(customConfig, func(u unstructured.Unstructured) bool { - return u.GetKind() == module.cr.GetKind() && u.GetAPIVersion() == module.cr.GetAPIVersion() - }) - - if customCRIndex >= 0 { - return customConfig[customCRIndex] - } - - return module.cr -} - -func containsModule(have string, want []ModuleInfo) *ModuleInfo { - for _, rec := range want { - if have == rec.Name { - return &rec - } - } - return nil -} - -func getDesiredVersion(moduleInfo ModuleInfo, versions []communitymodules.Version) communitymodules.Version { - if moduleInfo.Version != "" { - for _, version := range versions { - if version.Version == moduleInfo.Version { - // TODO: what if the user passes a version that does not exist? - // shall we for sure install the latest version? - fmt.Printf("Version %s found for %s\n", version.Version, moduleInfo.Name) - return version - } - } - } - - fmt.Printf("Using latest version for %s\n", moduleInfo.Name) - return communitymodules.GetLatestVersion(versions) -} - -func removeSpecifiedModules(ctx context.Context, client rootlessdynamic.Interface, desiredModules []moduleDetails) clierror.Error { - for _, module := range desiredModules { - fmt.Printf("Removing CR\n") - err := client.Remove(ctx, &module.cr) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to remove module cr")) - } - - cliErr := RetryUntilRemoved(ctx, client, module) - if cliErr != nil { - return cliErr - } - - fmt.Printf("Removing %s module\n", module.name) - err = client.RemoveMany(ctx, module.resources) - if err != nil { - return clierror.Wrap(err, clierror.New("failed to remove module resources")) - } - } - return nil -} - -func RetryUntilRemoved(ctx context.Context, client rootlessdynamic.Interface, module moduleDetails) clierror.Error { - return retryUntilRemoved(ctx, client, module, 50) -} - -func retryUntilRemoved(ctx context.Context, client rootlessdynamic.Interface, module moduleDetails, attempts uint) clierror.Error { - retryErr := retry.Do(func() error { - object, err := client.Get(ctx, &module.cr) - if object != nil { - return fmt.Errorf("object still exists") - } - if errors.IsNotFound(err) { - return nil - } - return err - }, - retry.Delay(1*time.Second), - retry.DelayType(retry.FixedDelay), - retry.Attempts(attempts), - retry.LastErrorOnly(true), - retry.Context(ctx), - ) - if retryErr != nil { - return clierror.Wrap(retryErr, clierror.New("failed to remove module cr")) - } - return nil -} diff --git a/internal/communitymodules/cluster/modules_test.go b/internal/communitymodules/cluster/modules_test.go deleted file mode 100644 index 004260cc3..000000000 --- a/internal/communitymodules/cluster/modules_test.go +++ /dev/null @@ -1,385 +0,0 @@ -package cluster - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - "github.com/kyma-project/cli.v3/internal/clierror" - "github.com/kyma-project/cli.v3/internal/communitymodules" - "github.com/stretchr/testify/require" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" -) - -var ( - fakeIstioCR = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "fakegroup/v1", - "kind": "istio", - "metadata": map[string]interface{}{ - "name": "fake-istio", - "namespace": "kyma-system", - }, - }, - } - - fakeServerlessCR = unstructured.Unstructured{ - Object: map[string]interface{}{ - "apiVersion": "fakegroup/v1", - "kind": "serverless", - "metadata": map[string]interface{}{ - "name": "fake-serverless", - "namespace": "kyma-system", - }, - }, - } - - fakeModuleDetails = []moduleDetails{ - { - name: "serverless", - version: "1.0.0", - cr: fakeServerlessCR, - resources: []unstructured.Unstructured{ - { - // empty - }, - { - // empty - }, - { - // empty - }, - }, - }, - { - name: "istio", - version: "0.1.0", - cr: fakeIstioCR, - resources: []unstructured.Unstructured{ - { - // empty - }, - { - // empty - }, - }, - }, - } -) - -func Test_downloadSpecifiedModules(t *testing.T) { - t.Run("Download module details", func(t *testing.T) { - mux := &http.ServeMux{} - mux.HandleFunc("/istio/cr", func(w http.ResponseWriter, _ *http.Request) { - bytes, err := json.Marshal(fakeIstioCR.Object) - require.NoError(t, err) - - _, err = w.Write(bytes) - require.NoError(t, err) - }) - mux.HandleFunc("/istio/resources", func(w http.ResponseWriter, _ *http.Request) { - data := "\n---\n---\n---\n" // three resources - - _, err := w.Write([]byte(data)) - require.NoError(t, err) - }) - - server := httptest.NewServer(mux) - defer server.Close() - - availableModules := fixFakeAvailableDetails( - fmt.Sprintf("%s/istio/cr", server.URL), - fmt.Sprintf("%s/istio/resources", server.URL), - ) - - modules, err := downloadSpecifiedModules([]ModuleInfo{{"istio", "0.0.1"}}, availableModules) - require.Nil(t, err) - require.Len(t, modules, 1) - require.Equal(t, fakeIstioCR, modules[0].cr) - require.Len(t, modules[0].resources, 3) - }) - - t.Run("broken cr url", func(t *testing.T) { - availableModules := fixFakeAvailableDetails( - "does-not-exist", - "does-not-exist", - ) - - module, err := downloadSpecifiedModules([]ModuleInfo{{"istio", "0.0.1"}}, availableModules) - require.Equal(t, clierror.Wrap( - errors.New("Get \"does-not-exist\": unsupported protocol scheme \"\""), - clierror.New("failed to download cr from 'does-not-exist' url"), - ), err) - require.Empty(t, module) - }) - - t.Run("bad response from resources url", func(t *testing.T) { - mux := &http.ServeMux{} - mux.HandleFunc("/istio/cr", func(w http.ResponseWriter, _ *http.Request) { - bytes, err := json.Marshal(fakeIstioCR.Object) - require.NoError(t, err) - - _, err = w.Write(bytes) - require.NoError(t, err) - }) - mux.HandleFunc("/istio/resources", func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(404) - }) - - server := httptest.NewServer(mux) - defer server.Close() - - availableModules := fixFakeAvailableDetails( - fmt.Sprintf("%s/istio/cr", server.URL), - fmt.Sprintf("%s/istio/resources", server.URL), - ) - - modules, err := downloadSpecifiedModules([]ModuleInfo{{"istio", "0.0.1"}}, availableModules) - require.Equal(t, clierror.Wrap( - errors.New("unexpected status code '404'"), - clierror.New(fmt.Sprintf("failed to download manifest from '%s/istio/resources' url", server.URL)), - ), err) - require.Empty(t, modules) - }) -} - -func Test_applySpecifiedModules(t *testing.T) { - t.Run("Apply fake serverless and istio modules", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{} - - err := applySpecifiedModules(context.Background(), - fakerootlessdynamic, fakeModuleDetails, []unstructured.Unstructured{}) - require.Nil(t, err) - - require.Len(t, fakerootlessdynamic.appliedObjects, 7) - require.Contains(t, fakerootlessdynamic.appliedObjects, fakeServerlessCR) - require.Contains(t, fakerootlessdynamic.appliedObjects, fakeIstioCR) - }) - - t.Run("Apply fake serverless and istio with CRs", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{} - - istioCR := fakeIstioCR - istioCR.Object["spec"] = "test" - - err := applySpecifiedModules(context.Background(), - fakerootlessdynamic, fakeModuleDetails, []unstructured.Unstructured{ - istioCR, - }) - require.Nil(t, err) - - require.Len(t, fakerootlessdynamic.appliedObjects, 7) - require.Contains(t, fakerootlessdynamic.appliedObjects, istioCR) - require.Contains(t, fakerootlessdynamic.appliedObjects, fakeServerlessCR) - }) - - t.Run("Apply client error", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{ - returnErr: errors.New("test error"), - } - - err := applySpecifiedModules(context.Background(), - fakerootlessdynamic, fakeModuleDetails, []unstructured.Unstructured{}) - require.Equal(t, clierror.Wrap( - errors.New("test error"), - clierror.New("failed to apply module resources"), - ), err) - }) -} - -func TestParseModules(t *testing.T) { - t.Run("parse input", func(t *testing.T) { - input := []string{"test", "", "test2:1.2.3"} - - moduleInfoList := ParseModules(input) - require.Len(t, moduleInfoList, 2) - require.Contains(t, moduleInfoList, ModuleInfo{"test", ""}) - require.Contains(t, moduleInfoList, ModuleInfo{"test2", "1.2.3"}) - }) -} - -func Test_verifyVersion(t *testing.T) { - t.Run("Version found", func(t *testing.T) { - versions := []communitymodules.Version{ - { - Version: "1.0.0", - }, - { - Version: "1.0.1", - }, - } - moduleInfo := ModuleInfo{ - Name: "test", - Version: "1.0.0", - } - - got := getDesiredVersion(moduleInfo, versions) - require.Equal(t, got, versions[0]) - }) - t.Run("Version not found", func(t *testing.T) { - versions := []communitymodules.Version{ - { - Version: "1.0.0", - }, - { - Version: "1.0.1", - }, - } - moduleInfo := ModuleInfo{ - Name: "test", - Version: "1.0.2", - } - - got := getDesiredVersion(moduleInfo, versions) - require.Equal(t, got, versions[1]) - }) -} - -func Test_containsModule(t *testing.T) { - t.Run("Module found", func(t *testing.T) { - have := "serverless" - want := []ModuleInfo{ - {"serverless", "1.0.0"}, - {"keda", "1.0.1"}, - } - - got := containsModule(have, want) - if got.Name != "serverless" { - t.Errorf("containsModule() got = %v, want %v", got, "test:1.0.0") - } - }) - t.Run("Module not found", func(t *testing.T) { - have := "test" - want := []ModuleInfo{ - {"Serverless", "1.0.0"}, - {"Keda", "1.0.1"}, - } - - got := containsModule(have, want) - if got != nil { - t.Errorf("containsModule() got = %v, want %v", got, nil) - } - }) -} - -func Test_removeSpecifiedModules(t *testing.T) { - t.Run("Remove module", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{} - - err := removeSpecifiedModules(context.Background(), fakerootlessdynamic, fakeModuleDetails) - require.Nil(t, err) - require.Len(t, fakerootlessdynamic.removedObjects, 7) - require.Contains(t, fakerootlessdynamic.removedObjects, fakeServerlessCR) - require.Contains(t, fakerootlessdynamic.removedObjects, fakeIstioCR) - }) - - t.Run("Remove module cr error", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{ - returnErr: errors.New("test error"), - } - - err := removeSpecifiedModules(context.Background(), fakerootlessdynamic, fakeModuleDetails) - require.Equal(t, clierror.Wrap( - errors.New("test error"), - clierror.New("failed to remove module cr"), - ), err) - }) -} - -func Test_retryUntilRemoved(t *testing.T) { - t.Run("Object removed", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{} - - err := retryUntilRemoved(context.Background(), fakerootlessdynamic, fakeModuleDetails[0], 1) - require.Nil(t, err) - }) - - t.Run("Failed to remove object", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{ - returnErr: errors.New("test error"), - } - - err := retryUntilRemoved(context.Background(), fakerootlessdynamic, fakeModuleDetails[0], 1) - require.Contains(t, err.String(), "failed to remove module cr") - }) - - t.Run("Object still exists", func(t *testing.T) { - fakerootlessdynamic := &rootlessdynamicMock{ - appliedObjects: []unstructured.Unstructured{fakeServerlessCR}, - } - - err := retryUntilRemoved(context.Background(), fakerootlessdynamic, fakeModuleDetails[0], 1) - require.Contains(t, err.String(), "object still exists") - }) -} - -func fixFakeAvailableDetails(istioCRURL, istioResourcesURL string) communitymodules.Modules { - return communitymodules.Modules{ - { - Name: "serverless", - Versions: []communitymodules.Version{ - { - Version: "0.0.1", - }, - }, - }, - { - Name: "istio", - Versions: []communitymodules.Version{ - { - Version: "0.0.2", - }, - { - Version: "0.0.1", - DeploymentYaml: istioResourcesURL, - CrYaml: istioCRURL, - }, - }, - }, - { - Name: "eventing", - Versions: []communitymodules.Version{ - { - Version: "1.0.0", - }, - }, - }, - } -} - -type rootlessdynamicMock struct { - returnErr error - appliedObjects []unstructured.Unstructured - removedObjects []unstructured.Unstructured -} - -func (m *rootlessdynamicMock) Apply(_ context.Context, obj *unstructured.Unstructured) error { - m.appliedObjects = append(m.appliedObjects, *obj) - return m.returnErr -} - -func (m *rootlessdynamicMock) ApplyMany(_ context.Context, objs []unstructured.Unstructured) error { - m.appliedObjects = append(m.appliedObjects, objs...) - return m.returnErr -} - -func (m *rootlessdynamicMock) Get(_ context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { - if len(m.appliedObjects) == 0 { - return nil, m.returnErr - } - return obj, m.returnErr -} - -func (m *rootlessdynamicMock) Remove(_ context.Context, obj *unstructured.Unstructured) error { - m.removedObjects = append(m.removedObjects, *obj) - return m.returnErr -} - -func (m *rootlessdynamicMock) RemoveMany(_ context.Context, objs []unstructured.Unstructured) error { - m.removedObjects = append(m.removedObjects, objs...) - return m.returnErr -} diff --git a/internal/communitymodules/cluster/namespace.go b/internal/communitymodules/cluster/namespace.go deleted file mode 100644 index 7be2fbf99..000000000 --- a/internal/communitymodules/cluster/namespace.go +++ /dev/null @@ -1,26 +0,0 @@ -package cluster - -import ( - "context" - "github.com/kyma-project/cli.v3/internal/clierror" - v1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes" -) - -func AssureNamespace(ctx context.Context, client kubernetes.Interface, namespace string) clierror.Error { - _, err := client.CoreV1().Namespaces().Get(ctx, namespace, metav1.GetOptions{}) - if !errors.IsNotFound(err) { - return nil - } - _, err = client.CoreV1().Namespaces().Create(ctx, &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: namespace, - }, - }, metav1.CreateOptions{}) - if err != nil { - return clierror.New("failed to create namespace") - } - return nil -} diff --git a/internal/communitymodules/cluster/namespace_test.go b/internal/communitymodules/cluster/namespace_test.go deleted file mode 100644 index 06b9a52be..000000000 --- a/internal/communitymodules/cluster/namespace_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package cluster - -import ( - "context" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - k8s_fake "k8s.io/client-go/kubernetes/fake" - "testing" -) - -func Test_AssureNamespace(t *testing.T) { - t.Run("Should do nothing when namespace exists", func(t *testing.T) { - staticClient := k8s_fake.NewSimpleClientset( - &v1.Namespace{ - ObjectMeta: metav1.ObjectMeta{ - Name: "kyma-system", - }, - }, - ) - - cliErr := AssureNamespace(context.Background(), staticClient, "kyma-system") - require.Nil(t, cliErr) - - ns, err := staticClient.CoreV1().Namespaces().Get(context.Background(), "kyma-system", metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, ns) - }) - t.Run("Should create namespace when it does not exist", func(t *testing.T) { - staticClient := k8s_fake.NewSimpleClientset() - - cliErr := AssureNamespace(context.Background(), staticClient, "kyma-system") - require.Nil(t, cliErr) - - ns, err := staticClient.CoreV1().Namespaces().Get(context.Background(), "kyma-system", metav1.GetOptions{}) - require.NoError(t, err) - require.NotNil(t, ns) - }) -} diff --git a/internal/communitymodules/merge.go b/internal/communitymodules/merge.go deleted file mode 100644 index fb5bb00ad..000000000 --- a/internal/communitymodules/merge.go +++ /dev/null @@ -1,35 +0,0 @@ -package communitymodules - -func MergeRowMaps(moduleMaps ...moduleMap) moduleMap { - result := make(moduleMap) - for _, moduleMap := range moduleMaps { - for name, value := range moduleMap { - if resultValue, ok := result[name]; ok { - result[name] = mergeTwoRows(resultValue, value) - } else { - result[name] = value - } - } - } - return result -} - -func mergeTwoRows(a row, b row) row { - result := a - if result.Name == "" { - result.Name = b.Name - } - if result.Repository == "" { - result.Repository = b.Repository - } - if result.LatestVersion == "" { - result.LatestVersion = b.LatestVersion - } - if result.Version == "" { - result.Version = b.Version - } - if result.Channel == "" { - result.Channel = b.Channel - } - return result -} diff --git a/internal/communitymodules/merge_test.go b/internal/communitymodules/merge_test.go deleted file mode 100644 index 65304d89e..000000000 --- a/internal/communitymodules/merge_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package communitymodules - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestMergeRowMaps(t *testing.T) { - moduleMapCatalog := moduleMap{ - "serverless": { - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - }, - } - moduleMapCatalogWith := moduleMap{ - "serverless": { - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - }, - "istio": { - Name: "istio", - Repository: "github.com/kyma-project/istio", - }, - } - moduleMapManaged := moduleMap{ - "serverless": { - Name: "serverless", - Channel: "Managed", - }, - } - moduleMapInstalled := moduleMap{ - "serverless": { - Name: "serverless", - Version: "1.0.0", - }, - } - moduleMapCollective := moduleMap{ - "serverless": { - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - Channel: "Managed", - Version: "1.0.0", - }, - } - - onlyIstio := moduleMap{ - "istio": { - Name: "istio", - Repository: "github.com/kyma-project/istio", - }, - } - - moduleMapCollectiveWith := moduleMap{ - "serverless": { - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - Channel: "Managed", - Version: "1.0.0", - }, - "istio": { - Name: "istio", - Repository: "github.com/kyma-project/istio", - Channel: "", - Version: "", - }, - } - - t.Run("Create collective view", func(t *testing.T) { - result := MergeRowMaps(moduleMapCatalog, moduleMapManaged, moduleMapInstalled) - require.Equal(t, moduleMapCollective, result) - }) - t.Run("Create collective view with additional catalog entry", func(t *testing.T) { - result := MergeRowMaps(moduleMapCatalogWith, moduleMapManaged, moduleMapInstalled) - require.Equal(t, moduleMapCollectiveWith, result) - }) - t.Run("Create collective view with a map that has a single entry with diffrent key", func(t *testing.T) { - result := MergeRowMaps(moduleMapCatalog, moduleMapManaged, moduleMapInstalled, onlyIstio) - require.Equal(t, moduleMapCollectiveWith, result) - }) -} - -func TestMergeTwoRows(t *testing.T) { - var rowA = row{ - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - } - var rowB = row{ - Name: "serverless", - Channel: "Managed", - } - var rowResult = row{ - Name: "serverless", - Repository: "github.com/kyma-project/serverless", - Channel: "Managed", - } - var rowC = row{ - Name: "serverless", - Repository: "github.com/kyma-project/test", - } - t.Run("Merge two rows", func(t *testing.T) { - result := mergeTwoRows(rowA, rowB) - require.Equal(t, rowResult, result) - }) - t.Run("Merge two rows with different repository", func(t *testing.T) { - result := mergeTwoRows(rowA, rowC) - require.Equal(t, rowA, result) - }) -} diff --git a/internal/communitymodules/modules.go b/internal/communitymodules/modules.go deleted file mode 100644 index e685babc3..000000000 --- a/internal/communitymodules/modules.go +++ /dev/null @@ -1,222 +0,0 @@ -package communitymodules - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "slices" - "strings" - - "github.com/kyma-project/cli.v3/internal/clierror" - "github.com/kyma-project/cli.v3/internal/kube" - "github.com/kyma-project/cli.v3/internal/kube/kyma" - "golang.org/x/mod/semver" - v1 "k8s.io/api/apps/v1" - "k8s.io/apimachinery/pkg/api/errors" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -const URL = "https://raw.githubusercontent.com/kyma-project/community-modules/main/model.json" - -type row struct { - Name string - Repository string - LatestVersion string - Version string - Channel 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() (moduleMap, clierror.Error) { - return modulesCatalog(URL) -} - -func modulesCatalog(url string) (moduleMap, clierror.Error) { - modules, err := getCommunityModules(url) - if err != nil { - return nil, err - } - - catalog := make(moduleMap) - for _, rec := range modules { - latestVersion := GetLatestVersion(rec.Versions) - catalog[rec.Name] = row{ - Name: rec.Name, - Repository: chooseRepository(rec, latestVersion), - LatestVersion: latestVersion.Version, - } - } - return catalog, nil -} - -// chooseRepository returns the repository of the module for specific version if it is available, otherwise it returns the repository of the module. -// Sometimes one of those values don't exist so this function makes sure that we provide the user with the most information possible. -func chooseRepository(module Module, version Version) string { - if version.Repository != "" { - return version.Repository - } - if module.Repository != "" { - return module.Repository - } - return "Unknown" -} -func GetLatestVersion(versions []Version) Version { - return slices.MaxFunc(versions, func(a, b Version) int { - cmpA := a.Version - if !semver.IsValid(cmpA) { - cmpA = fmt.Sprintf("v%s", cmpA) - } - cmpB := b.Version - if !semver.IsValid(cmpB) { - cmpB = fmt.Sprintf("v%s", cmpB) - } - return semver.Compare(cmpA, cmpB) - }) -} - -// getCommunityModules returns a list of all available modules from the community-modules repository -func getCommunityModules(url string) (Modules, clierror.Error) { - resp, err := http.Get(url) - if err != nil { - return nil, clierror.Wrap(err, clierror.New("while getting modules list from github")) - } - defer resp.Body.Close() - - var modules Modules - modules, respErr := decodeCommunityModulesResponse(resp, modules) - if respErr != nil { - return nil, clierror.WrapE(respErr, clierror.New("while handling response")) - } - return modules, nil -} - -// decodeCommunityModulesResponse reads the response body and unmarshals it into the template -func decodeCommunityModulesResponse(resp *http.Response, modules Modules) (Modules, clierror.Error) { - if resp.StatusCode != 200 { - errMsg := fmt.Sprintf("error response: %s", resp.Status) - return nil, clierror.New(errMsg) - } - - bodyText, err := io.ReadAll(resp.Body) - if err != nil { - return nil, clierror.Wrap(err, clierror.New("while reading http response")) - } - - err = json.Unmarshal(bodyText, &modules) - if err != nil { - return nil, clierror.Wrap(err, clierror.New("while unmarshalling")) - } - return modules, nil -} - -// ManagedModules returns a map of all managed modules from the cluster -func ManagedModules(ctx context.Context, client kube.Client) (moduleMap, clierror.Error) { - modules, err := getManagedList(ctx, client) - if err != nil { - return nil, clierror.WrapE(err, clierror.New("while getting managed modules")) - } - - managed := make(moduleMap) - for _, module := range modules { - managed[module.Name] = row{ - Name: module.Name, - Channel: module.Channel, - Version: module.Version, - } - } - return managed, nil -} - -// getManagedList gets a list of all managed modules from the Kyma CR -func getManagedList(ctx context.Context, client kube.Client) ([]kyma.ModuleStatus, clierror.Error) { - kyma, err := client.Kyma().GetDefaultKyma(ctx) - if err != nil && !errors.IsNotFound(err) { - return nil, clierror.Wrap(err, clierror.New("while getting Kyma CR")) - } - if errors.IsNotFound(err) { - return nil, nil - } - - return kyma.Status.Modules, nil -} - -// InstalledModules returns a map of all installed modules from the cluster, regardless whether they are managed or not -func InstalledModules(ctx context.Context, client kube.Client) (moduleMap, clierror.Error) { - return installedModules(ctx, URL, client) -} - -func installedModules(ctx context.Context, url string, client kube.Client) (moduleMap, clierror.Error) { - modules, err := getCommunityModules(url) - if err != nil { - return nil, clierror.WrapE(err, clierror.New("while getting installed modules")) - } - - installed, err := getInstalledModules(ctx, modules, client) - if err != nil { - return nil, err - } - - return installed, nil -} - -func getInstalledModules(ctx context.Context, modules Modules, client kube.Client) (moduleMap, clierror.Error) { - installed := make(moduleMap) - for _, module := range modules { - latestVersion := GetLatestVersion(module.Versions) - managerName := getManagerName(latestVersion) - deployment, err := client.Static().AppsV1().Deployments("kyma-system"). - Get(ctx, managerName, metav1.GetOptions{}) - if err != nil && !errors.IsNotFound(err) { - msg := "while getting the " + managerName + " deployment" - return nil, clierror.Wrap(err, clierror.New(msg)) - } - if errors.IsNotFound(err) { - continue - } - - installedVersion := getInstalledVersion(deployment) - moduleVersion := latestVersion.Version - installed[module.Name] = row{ - Name: module.Name, - Version: calculateVersion(moduleVersion, installedVersion), - } - } - return installed, nil -} - -func getInstalledVersion(deployment *v1.Deployment) string { - deploymentImage := strings.Split(deployment.Spec.Template.Spec.Containers[0].Image, "/") - nameAndTag := strings.Split(deploymentImage[len(deploymentImage)-1], ":") - return nameAndTag[len(nameAndTag)-1] -} - -func getManagerName(version Version) string { - managerPath := strings.Split(version.ManagerPath, "/") - return managerPath[len(managerPath)-1] -} - -func calculateVersion(moduleVersion string, installedVersion string) string { - if moduleVersion == installedVersion { - return installedVersion - } - return "outdated moduleVersion, latest is " + moduleVersion -} - -func GetAvailableModules() (Modules, clierror.Error) { - return getAvailableModules(URL) -} - -func getAvailableModules(url string) (Modules, clierror.Error) { - resp, err := http.Get(url) - if err != nil { - return nil, clierror.Wrap(err, clierror.New("failed to get available modules")) - } - defer resp.Body.Close() - - var modules Modules - return decodeCommunityModulesResponse(resp, modules) -} diff --git a/internal/communitymodules/modules_test.go b/internal/communitymodules/modules_test.go deleted file mode 100644 index aca532e5c..000000000 --- a/internal/communitymodules/modules_test.go +++ /dev/null @@ -1,404 +0,0 @@ -package communitymodules - -import ( - "context" - "encoding/json" - "fmt" - "net/http" - "net/http/httptest" - "testing" - - kube_fake "github.com/kyma-project/cli.v3/internal/kube/fake" - "github.com/kyma-project/cli.v3/internal/kube/kyma" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - v1 "k8s.io/api/apps/v1" - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "k8s.io/apimachinery/pkg/runtime" - dynamic_fake "k8s.io/client-go/dynamic/fake" - k8s_fake "k8s.io/client-go/kubernetes/fake" -) - -const ( - kymaNamespace = "kyma-system" -) - -func Test_modulesCatalog(t *testing.T) { - t.Run("ok", func(t *testing.T) { - expectedResult := moduleMap{ - "module1": row{ - Name: "module1", - Repository: "https://repo2/path/module1.git", - LatestVersion: "1.7.0", - Version: "", - Channel: "", - }, - "module2": row{ - Name: "module2", - Repository: "https://repo/path/module2.git", - LatestVersion: "4.5.6", - Version: "", - Channel: "", - }, - } - - httpServer := httptest.NewServer(http.HandlerFunc( - fixHttpResponseHandler(200, fixCommunityModulesResponse()))) - defer httpServer.Close() - modules, err := modulesCatalog(httpServer.URL) - require.Nil(t, err) - require.Equal(t, expectedResult, modules) - }) - - t.Run("invalid http response", func(t *testing.T) { - httpServer := httptest.NewServer(http.HandlerFunc(fixHttpResponseHandler(500, ""))) - defer httpServer.Close() - modules, err := modulesCatalog(httpServer.URL) - require.Nil(t, modules) - require.NotNil(t, err) - require.Contains(t, err.String(), "while handling response") - require.Contains(t, err.String(), "error response: 500") - }) - - t.Run("invalid json response", func(t *testing.T) { - httpServer := httptest.NewServer(http.HandlerFunc(fixHttpResponseHandler(200, "invalid json"))) - defer httpServer.Close() - modules, err := modulesCatalog(httpServer.URL) - require.Nil(t, modules) - require.NotNil(t, err) - require.Contains(t, err.String(), "while handling response") - require.Contains(t, err.String(), "while unmarshalling") - }) - t.Run("greatest version is latest", func(t *testing.T) { - expectedResult := moduleMap{ - "module1": row{ - Name: "module1", - Repository: "https://repo2/path/module1.git", - LatestVersion: "1.7.0", - Version: "", - Channel: "", - }, - "module2": row{ - Name: "module2", - Repository: "https://repo/path/module2.git", - LatestVersion: "4.5.6", - Version: "", - Channel: "", - }, - } - - httpServer := httptest.NewServer(http.HandlerFunc(fixHttpResponseHandler(200, fixCommunityModulesResponse()))) - defer httpServer.Close() - modules, err := modulesCatalog(httpServer.URL) - require.Nil(t, err) - require.Equal(t, expectedResult, modules) - }) -} - -func Test_ManagedModules(t *testing.T) { - t.Run("ok", func(t *testing.T) { - expectedResult := moduleMap{ - "module1": row{ - Name: "module1", - Channel: "fast", - }, - "module2": row{ - Name: "module2", - Channel: "fast", - }, - "module3": row{ - Name: "module3", - Channel: "regular", - }, - } - - testKyma := fixTestKyma() - scheme := runtime.NewScheme() - scheme.AddKnownTypes(GVRKyma.GroupVersion(), testKyma) - dynamic := dynamic_fake.NewSimpleDynamicClient(scheme, testKyma) - kubeClient := &kube_fake.FakeKubeClient{ - TestKubernetesInterface: nil, - TestKymaInterface: kyma.NewClient(dynamic), - } - - modules, err := ManagedModules(context.Background(), kubeClient) - - assert.Equal(t, expectedResult, modules) - assert.Nil(t, err) - }) - t.Run("kyma cr not found", func(t *testing.T) { - expectedResult := moduleMap{} - - testKyma := fixTestKyma() - scheme := runtime.NewScheme() - scheme.AddKnownTypes(GVRKyma.GroupVersion(), testKyma) - dynamic := dynamic_fake.NewSimpleDynamicClient(scheme) - kubeClient := &kube_fake.FakeKubeClient{ - TestKubernetesInterface: nil, - TestKymaInterface: kyma.NewClient(dynamic), - } - - modules, err := ManagedModules(context.Background(), kubeClient) - - assert.Equal(t, expectedResult, modules) - assert.Nil(t, err) - }) -} - -func Test_installedModules(t *testing.T) { - t.Run("ok", func(t *testing.T) { - expectedResult := moduleMap{ - "module1": row{ - Name: "module1", - Version: "1.7.0", - }, - "module2": row{ - Name: "module2", - Version: "outdated moduleVersion, latest is 4.5.6", - }, - } - - httpServer := httptest.NewServer(http.HandlerFunc( - fixHttpResponseHandler(200, fixCommunityModulesResponse()))) - defer httpServer.Close() - - staticClient := k8s_fake.NewSimpleClientset( - fixTestDeployment("module1-controller-manager", "1.7.0"), - fixTestDeployment("module2-manager", "6.7.8"), // outdated - fixTestDeployment("other-deployment", "1.2.3")) - kubeClient := &kube_fake.FakeKubeClient{ - TestKubernetesInterface: staticClient, - TestDynamicInterface: nil, - } - - modules, err := installedModules(context.Background(), httpServer.URL, kubeClient) - - assert.Equal(t, expectedResult, modules) - assert.Nil(t, err) - }) - t.Run("only one installed", func(t *testing.T) { - expectedResult := moduleMap{ - "module2": row{ - Name: "module2", - Version: "4.5.6", - }, - } - - httpServer := httptest.NewServer(http.HandlerFunc( - fixHttpResponseHandler(200, fixCommunityModulesResponse()))) - defer httpServer.Close() - - staticClient := k8s_fake.NewSimpleClientset( - fixTestDeployment("module2-manager", "4.5.6"), - fixTestDeployment("other-deployment", "1.2.3")) - kubeClient := &kube_fake.FakeKubeClient{ - TestKubernetesInterface: staticClient, - TestDynamicInterface: nil, - } - - modules, err := installedModules(context.Background(), httpServer.URL, kubeClient) - - assert.Equal(t, expectedResult, modules) - assert.Nil(t, err) - }) -} - -func fixHttpResponseHandler(status int, response string) func(http.ResponseWriter, *http.Request) { - return func(w http.ResponseWriter, _ *http.Request) { - w.WriteHeader(status) - _, _ = w.Write([]byte(response)) - } -} - -func fixTestKyma() *unstructured.Unstructured { - b := []byte(` - { - "apiVersion": "operator.kyma-project.io/v1beta2", - "kind": "Kyma", - "metadata": { - "name": "default", - "namespace": "kyma-system" - }, - "status": { - "modules": [ - { - "name": "module1", - "channel": "fast" - }, - { - "name": "module3", - "channel": "regular" - }, - { - "name": "module2", - "channel": "fast" - } - ] - } - } - `) - f := make(map[string]interface{}) - _ = json.Unmarshal(b, &f) - u := &unstructured.Unstructured{Object: f} - return u -} - -func fixTestDeployment(name, imageTag string) *v1.Deployment { - return &v1.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: name, - Namespace: kymaNamespace, - }, - Spec: v1.DeploymentSpec{ - Template: corev1.PodTemplateSpec{ - ObjectMeta: metav1.ObjectMeta{}, - Spec: corev1.PodSpec{ - Containers: []corev1.Container{ - { - Image: fmt.Sprintf("localhost:5000/some-project/some-image-name:%s", imageTag), - }, - }, - }, - }, - }, - } -} - -func fixCommunityModulesResponse() string { - return ` - [ - { - "name": "module1", - "versions": [ - { - "version": "1.5.3", - "repository": "https://repo1/path/module1.git", - "managerPath": "/some/path1/module1-controller-manager" - }, - { - "version": "1.7.0", - "repository": "https://repo2/path/module1.git", - "managerPath": "/other/path2/module1-controller-manager" - }, - { - "version": "1.3.4", - "repository": "https://repo3/path/module1.git", - "managerPath": "/some/path3/module1-controller-manager" - } - ] - }, - { - "name": "module2", - "versions": [ - { - "version": "4.5.6", - "repository": "https://repo/path/module2.git", - "managerPath": "/some/path/module2-manager" - } - ] - } - ]` -} - -func Test_GetLatestVersion(t *testing.T) { - t.Run("simple versions", func(t *testing.T) { - result := GetLatestVersion([]Version{ - { - Version: "1.5.3", - }, - { - Version: "1.7.1", - }, - { - Version: "1.4.3", - }, - }) - assert.Equal(t, Version{ - Version: "1.7.1", - }, result) - }) - t.Run("v prefix", func(t *testing.T) { - result := GetLatestVersion([]Version{ - { - Version: "v1.5.3", - }, - { - Version: "v1.7.1", - }, - { - Version: "1.4.3", - }, - }) - assert.Equal(t, Version{ - Version: "v1.7.1", - }, result) - }) - t.Run("with suffix", func(t *testing.T) { - result := GetLatestVersion([]Version{ - { - Version: "1.5.3-experimental", - }, - { - Version: "1.7.1-dev", - }, - { - Version: "1.7.1", - }, - { - Version: "1.4.3", - }, - }) - assert.Equal(t, Version{ - Version: "1.7.1", - }, result) - }) -} - -func Test_chooseRepository(t *testing.T) { - t.Run("version has repository", func(t *testing.T) { - result := chooseRepository(Module{ - Name: "module1", - Repository: "https://repo1/path/module1.git", - }, Version{ - Version: "1.5.3", - Repository: "https://repo2/path/module1.git", - ManagerPath: "/some/path1/module1-controller-manager", - }) - assert.Equal(t, "https://repo2/path/module1.git", result) - }) - t.Run("module has repository", func(t *testing.T) { - result := chooseRepository(Module{ - Name: "module1", - Repository: "https://repo1/path/module1.git", - }, Version{ - Version: "1.5.3", - Repository: "", - ManagerPath: "/some/path1/module1-controller-manager", - }) - assert.Equal(t, "https://repo1/path/module1.git", result) - }) - t.Run("Both have repository", func(t *testing.T) { - result := chooseRepository(Module{ - Name: "module1", - Repository: "https://repo1/path/module1.git", - }, Version{ - Version: "1.5.3", - Repository: "https://repo2/path/module1.git", - ManagerPath: "/some/path1/module1-controller-manager", - }) - assert.Equal(t, "https://repo2/path/module1.git", result) - }) - t.Run("no repository", func(t *testing.T) { - result := chooseRepository(Module{ - Name: "module1", - Repository: "", - }, Version{ - Version: "1.5.3", - Repository: "", - ManagerPath: "/some/path1/module1-controller-manager", - }) - assert.Equal(t, "Unknown", result) - }) -} diff --git a/internal/communitymodules/render.go b/internal/communitymodules/render.go deleted file mode 100644 index 243beb690..000000000 --- a/internal/communitymodules/render.go +++ /dev/null @@ -1,86 +0,0 @@ -package communitymodules - -import ( - "os" - "sort" - "strings" - - "github.com/olekukonko/tablewriter" -) - -type RowConverter func(row) []string -type TableInfo struct { - Header []string - RowConverter RowConverter -} - -var ( - CollectiveTableInfo = TableInfo{ - Header: []string{"NAME", "REPOSITORY", "VERSION INSTALLED", "CHANNEL"}, - RowConverter: func(r row) []string { return []string{r.Name, r.Repository, r.Version, r.Channel} }, - } - InstalledTableInfo = TableInfo{ - Header: []string{"NAME", "VERSION"}, - RowConverter: func(r row) []string { return []string{r.Name, r.Version} }, - } - ManagedTableInfo = TableInfo{ - Header: []string{"NAME", "VERSION", "CHANNEL"}, - RowConverter: func(r row) []string { return []string{r.Name, r.Version, r.Channel} }, - } - CatalogTableInfo = TableInfo{ - Header: []string{"NAME", "REPOSITORY", "LATEST VERSION"}, - RowConverter: func(r row) []string { return []string{r.Name, r.Repository, r.LatestVersion} }, - } -) - -func RenderModules(raw bool, moduleMap moduleMap, tableInfo TableInfo) { - renderTable( - raw, - convertModuleMapToTable(moduleMap, tableInfo.RowConverter), - tableInfo.Header) -} - -func convertModuleMapToTable(moduleMap moduleMap, rowConverter RowConverter) [][]string { - var moduleNames []string - for key := range moduleMap { - moduleNames = append(moduleNames, key) - } - sort.Strings(moduleNames) - var result [][]string - for _, key := range moduleNames { - result = append(result, rowConverter(moduleMap[key])) - } - return result -} - -// renderTable renders the table with the provided headers -func renderTable(raw bool, modulesData [][]string, headers []string) { - if raw { - for _, row := range modulesData { - println(strings.Join(row, "\t")) - } - } else { - var table [][]string - table = append(table, modulesData...) - - twTable := setTable(table) - twTable.SetHeader(headers) - twTable.Render() - } -} - -// setTable sets the table settings for the tablewriter -func setTable(inTable [][]string) *tablewriter.Table { - table := tablewriter.NewWriter(os.Stdout) - table.AppendBulk(inTable) - table.SetRowLine(false) - table.SetHeaderLine(false) - table.SetColumnSeparator("") - table.SetAlignment(tablewriter.ALIGN_CENTER) - table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) - table.SetColumnAlignment([]int{tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT, tablewriter.ALIGN_LEFT}) - table.SetBorder(false) - table.SetTablePadding("\t") - table.SetNoWhiteSpace(true) - return table -} diff --git a/internal/communitymodules/render_test.go b/internal/communitymodules/render_test.go deleted file mode 100644 index 9b3280d0f..000000000 --- a/internal/communitymodules/render_test.go +++ /dev/null @@ -1,73 +0,0 @@ -package communitymodules - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestRender(t *testing.T) { - var testMap = moduleMap{ - "test": { - Name: "testName", - Repository: "testRepo", - LatestVersion: "testLatest", - Version: "testVer", - Channel: "testMan", - }, - } - var moduleMapEmpty = moduleMap{} - - var testMapLong = moduleMap{ - "test1": { - Name: "testName1", - Repository: "testRepo1", - LatestVersion: "testLatest1", - Version: "testVer1", - Channel: "testMan1", - }, - "test2": { - Name: "testName2", - Repository: "testRepo2", - LatestVersion: "testLatest2", - Version: "testVer2", - Channel: "testMan2", - }, - } - var testMapSort = moduleMap{ - "ghi": { - Name: "ghi", - Version: "3", - }, - "def": { - Name: "def", - Version: "2", - }, - "abc": { - Name: "abc", - Version: "1", - }, - "jkl": { - Name: "jkl", - Version: "4", - }, - } - - t.Run("RenderModules doesn't panic for empty Map", func(t *testing.T) { - require.NotPanics(t, func() { - RenderModules(false, moduleMapEmpty, CatalogTableInfo) - }) - }) - t.Run("convertRowToCatalog", func(t *testing.T) { - result := convertModuleMapToTable(testMap, func(r row) []string { return []string{r.Name, r.LatestVersion, r.Version} }) - require.Equal(t, [][]string{{"testName", "testLatest", "testVer"}}, result) - }) - t.Run("convertRowToCatalog for map with mutliple entries", func(t *testing.T) { - result := convertModuleMapToTable(testMapLong, func(r row) []string { return []string{r.Repository, r.LatestVersion, r.Channel} }) - require.ElementsMatch(t, [][]string{{"testRepo1", "testLatest1", "testMan1"}, {"testRepo2", "testLatest2", "testMan2"}}, result) - }) - t.Run("sort names", func(t *testing.T) { - result := convertModuleMapToTable(testMapSort, func(r row) []string { return []string{r.Name, r.Version} }) - require.Equal(t, [][]string{{"abc", "1"}, {"def", "2"}, {"ghi", "3"}, {"jkl", "4"}}, result) - }) -} diff --git a/internal/communitymodules/types.go b/internal/communitymodules/types.go deleted file mode 100644 index 69bfddb8a..000000000 --- a/internal/communitymodules/types.go +++ /dev/null @@ -1,29 +0,0 @@ -package communitymodules - -import "k8s.io/apimachinery/pkg/runtime/schema" - -// This structure contains only the fields currently in use. -type Modules []Module - -type Module struct { - Name string `json:"name,omitempty"` - Versions []Version `json:"versions,omitempty"` - Repository string `json:"repository,omitempty"` - ManagedResources []string `json:"managedResources,omitempty"` -} - -type Version struct { - Version string `json:"version,omitempty"` - ManagerPath string `json:"managerPath,omitempty"` - Repository string `json:"repository,omitempty"` - DeploymentYaml string `json:"deploymentYaml,omitempty"` - CrYaml string `json:"crYaml,omitempty"` -} - -var ( - GVRKyma = schema.GroupVersionResource{ - Group: "operator.kyma-project.io", - Version: "v1beta2", - Resource: "kymas", - } -)