From beaca2f10b9ede4632fe3aae5fa0cb68ef3aad83 Mon Sep 17 00:00:00 2001 From: Filip Strozik Date: Mon, 16 Dec 2024 14:33:17 +0100 Subject: [PATCH] Add `module enable` command (#2283) --- internal/cmd/alpha/module/enable.go | 72 ++++++++++++ internal/cmd/alpha/module/list.go | 2 +- internal/cmd/alpha/module/module.go | 3 +- internal/cmdcommon/extension_test.go | 6 +- internal/kube/fake/{client.go => kube.go} | 20 ++-- internal/kube/fake/kyma.go | 80 +++++++++++++ internal/kube/fake/rootlessdynamic.go | 52 +++++++++ internal/kube/kyma/kyma.go | 98 ++++++++++++++-- internal/kube/kyma/kyma_test.go | 96 +++++++++++---- internal/kube/kyma/types.go | 5 + internal/kube/resources/resources_test.go | 12 +- internal/kube/rootlessdynamic/client.go | 8 +- internal/kubeconfig/kubeconfig_test.go | 4 +- internal/modules/enable.go | 68 +++++++++++ internal/modules/enable_test.go | 135 ++++++++++++++++++++++ internal/registry/config_test.go | 4 +- 16 files changed, 611 insertions(+), 54 deletions(-) create mode 100644 internal/cmd/alpha/module/enable.go rename internal/kube/fake/{client.go => kube.go} (69%) create mode 100644 internal/kube/fake/kyma.go create mode 100644 internal/kube/fake/rootlessdynamic.go create mode 100644 internal/modules/enable.go create mode 100644 internal/modules/enable_test.go diff --git a/internal/cmd/alpha/module/enable.go b/internal/cmd/alpha/module/enable.go new file mode 100644 index 000000000..2434b79e2 --- /dev/null +++ b/internal/cmd/alpha/module/enable.go @@ -0,0 +1,72 @@ +package module + +import ( + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/kyma-project/cli.v3/internal/kube/resources" + "github.com/kyma-project/cli.v3/internal/modules" + "github.com/spf13/cobra" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type enableConfig struct { + *cmdcommon.KymaConfig + + module string + channel string + crPath string + defaultCR bool +} + +func newEnableCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { + cfg := enableConfig{ + KymaConfig: kymaConfig, + } + + cmd := &cobra.Command{ + Use: "enable ", + Short: "Enable module.", + Long: "Use this command to enable module.", + Args: cobra.ExactArgs(1), + Run: func(cmd *cobra.Command, args []string) { + cfg.module = args[0] + clierror.Check(runEnable(&cfg)) + }, + } + + cmd.Flags().StringVar(&cfg.channel, "channel", "", "Name of the Kyma channel to use for the module") + cmd.Flags().StringVar(&cfg.crPath, "cr-path", "", "Path to the custom resource file") + cmd.Flags().BoolVar(&cfg.defaultCR, "default-cr", false, "Use this flag to deploy module with default cr") + + cmd.MarkFlagsMutuallyExclusive("cr-path", "default-cr") + + return cmd +} + +func runEnable(cfg *enableConfig) clierror.Error { + client, clierr := cfg.GetKubeClientWithClierr() + if clierr != nil { + return clierr + } + + crs, clierr := loadCustomCRs(cfg.crPath) + if clierr != nil { + return clierr + } + + return modules.Enable(cfg.Ctx, client, cfg.module, cfg.channel, cfg.defaultCR, crs...) +} + +func loadCustomCRs(crPath string) ([]unstructured.Unstructured, clierror.Error) { + if crPath == "" { + // skip if not set + return nil, nil + } + + crs, err := resources.ReadFromFiles(crPath) + if err != nil { + return nil, clierror.Wrap(err, clierror.New("failed to read object from file")) + } + + return crs, nil +} diff --git a/internal/cmd/alpha/module/list.go b/internal/cmd/alpha/module/list.go index a0bd6977d..d86fdd381 100644 --- a/internal/cmd/alpha/module/list.go +++ b/internal/cmd/alpha/module/list.go @@ -11,7 +11,7 @@ type modulesConfig struct { *cmdcommon.KymaConfig } -func NewListCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { +func newListCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cfg := modulesConfig{ KymaConfig: kymaConfig, } diff --git a/internal/cmd/alpha/module/module.go b/internal/cmd/alpha/module/module.go index a90544cbd..8c72101c1 100644 --- a/internal/cmd/alpha/module/module.go +++ b/internal/cmd/alpha/module/module.go @@ -13,7 +13,8 @@ func NewModuleCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { Long: `Use this command to manage modules on a kyma cluster.`, } - cmd.AddCommand(NewListCMD(kymaConfig)) + cmd.AddCommand(newListCMD(kymaConfig)) + cmd.AddCommand(newEnableCMD(kymaConfig)) return cmd } diff --git a/internal/cmdcommon/extension_test.go b/internal/cmdcommon/extension_test.go index c94584a99..55b69401a 100644 --- a/internal/cmdcommon/extension_test.go +++ b/internal/cmdcommon/extension_test.go @@ -16,7 +16,7 @@ import ( func TestListFromCluster(t *testing.T) { t.Run("list extensions from cluster", func(t *testing.T) { kubeClientConfig := &KubeClientConfig{ - KubeClient: &fake.FakeKubeClient{ + KubeClient: &fake.KubeClient{ TestKubernetesInterface: k8s_fake.NewSimpleClientset( fixTestExtensionConfigmap("test-1"), fixTestExtensionConfigmap("test-2"), @@ -45,7 +45,7 @@ func TestListFromCluster(t *testing.T) { t.Run("missing rootCommand error", func(t *testing.T) { kubeClientConfig := &KubeClientConfig{ - KubeClient: &fake.FakeKubeClient{ + KubeClient: &fake.KubeClient{ TestKubernetesInterface: k8s_fake.NewSimpleClientset( &corev1.ConfigMap{ ObjectMeta: v1.ObjectMeta{ @@ -71,7 +71,7 @@ func TestListFromCluster(t *testing.T) { t.Run("skip optional fields", func(t *testing.T) { kubeClientConfig := &KubeClientConfig{ - KubeClient: &fake.FakeKubeClient{ + KubeClient: &fake.KubeClient{ TestKubernetesInterface: k8s_fake.NewSimpleClientset( &corev1.ConfigMap{ ObjectMeta: v1.ObjectMeta{ diff --git a/internal/kube/fake/client.go b/internal/kube/fake/kube.go similarity index 69% rename from internal/kube/fake/client.go rename to internal/kube/fake/kube.go index 379e99af7..60d8d1f51 100644 --- a/internal/kube/fake/client.go +++ b/internal/kube/fake/kube.go @@ -13,7 +13,7 @@ import ( // Fake client for testing purposes // It implements the Client interface and returns given values only -type FakeKubeClient struct { +type KubeClient struct { TestKubernetesInterface kubernetes.Interface TestDynamicInterface dynamic.Interface TestIstioInterface istio.Interface @@ -25,38 +25,38 @@ type FakeKubeClient struct { TestRootlessDynamicInterface rootlessdynamic.Interface } -func (f *FakeKubeClient) Static() kubernetes.Interface { +func (f *KubeClient) Static() kubernetes.Interface { return f.TestKubernetesInterface } -func (f *FakeKubeClient) Dynamic() dynamic.Interface { +func (f *KubeClient) Dynamic() dynamic.Interface { return f.TestDynamicInterface } -func (f *FakeKubeClient) Istio() istio.Interface { +func (f *KubeClient) Istio() istio.Interface { return f.TestIstioInterface } -func (f *FakeKubeClient) Btp() btp.Interface { +func (f *KubeClient) Btp() btp.Interface { return f.TestBtpInterface } -func (f *FakeKubeClient) RestClient() *rest.RESTClient { +func (f *KubeClient) RestClient() *rest.RESTClient { return f.TestRestClient } -func (f *FakeKubeClient) RestConfig() *rest.Config { +func (f *KubeClient) RestConfig() *rest.Config { return f.TestRestConfig } -func (f *FakeKubeClient) APIConfig() *api.Config { +func (f *KubeClient) APIConfig() *api.Config { return f.TestAPIConfig } -func (f *FakeKubeClient) Kyma() kyma.Interface { +func (f *KubeClient) Kyma() kyma.Interface { return f.TestKymaInterface } -func (f *FakeKubeClient) RootlessDynamic() rootlessdynamic.Interface { +func (f *KubeClient) RootlessDynamic() rootlessdynamic.Interface { return f.TestRootlessDynamicInterface } diff --git a/internal/kube/fake/kyma.go b/internal/kube/fake/kyma.go new file mode 100644 index 000000000..487adb773 --- /dev/null +++ b/internal/kube/fake/kyma.go @@ -0,0 +1,80 @@ +package fake + +import ( + "context" + + "github.com/kyma-project/cli.v3/internal/kube/kyma" +) + +type FakeEnabledModule struct { + Name string + Channel string + CustomResourcePolicy string +} + +type KymaClient struct { + // outputs + ReturnErr error + ReturnGetModuleInfoErr error + ReturnDisableModuleErr error + ReturnGetModuleTemplateErr error + ReturnWaitForModuleErr error + ReturnModuleReleaseMetaList kyma.ModuleReleaseMetaList + ReturnModuleTemplateList kyma.ModuleTemplateList + ReturnModuleReleaseMeta kyma.ModuleReleaseMeta + ReturnModuleTemplate kyma.ModuleTemplate + ReturnDefaultKyma kyma.Kyma + ReturnModuleInfo kyma.KymaModuleInfo + + // input arguments + UpdateDefaultKymas []kyma.Kyma + DisabledModules []string + EnabledModules []FakeEnabledModule +} + +func (c *KymaClient) ListModuleReleaseMeta(_ context.Context) (*kyma.ModuleReleaseMetaList, error) { + return &c.ReturnModuleReleaseMetaList, c.ReturnErr +} + +func (c *KymaClient) ListModuleTemplate(_ context.Context) (*kyma.ModuleTemplateList, error) { + return &c.ReturnModuleTemplateList, c.ReturnErr +} + +func (c *KymaClient) GetModuleReleaseMetaForModule(_ context.Context, _ string) (*kyma.ModuleReleaseMeta, error) { + return &c.ReturnModuleReleaseMeta, c.ReturnErr +} + +func (c *KymaClient) GetModuleTemplateForModule(_ context.Context, _, _ string) (*kyma.ModuleTemplate, error) { + return &c.ReturnModuleTemplate, c.ReturnGetModuleTemplateErr +} + +func (c *KymaClient) GetDefaultKyma(_ context.Context) (*kyma.Kyma, error) { + return &c.ReturnDefaultKyma, c.ReturnErr +} + +func (c *KymaClient) UpdateDefaultKyma(_ context.Context, kyma *kyma.Kyma) error { + c.UpdateDefaultKymas = append(c.UpdateDefaultKymas, *kyma) + return c.ReturnErr +} + +func (c *KymaClient) GetModuleInfo(_ context.Context, _ string) (*kyma.KymaModuleInfo, error) { + return &c.ReturnModuleInfo, c.ReturnGetModuleInfoErr +} + +func (c *KymaClient) WaitForModuleState(_ context.Context, _ string, _ ...string) error { + return c.ReturnWaitForModuleErr +} + +func (c *KymaClient) EnableModule(_ context.Context, module string, channel string, customResourcePolicy string) error { + c.EnabledModules = append(c.EnabledModules, FakeEnabledModule{ + Name: module, + Channel: channel, + CustomResourcePolicy: customResourcePolicy, + }) + return c.ReturnErr +} + +func (c *KymaClient) DisableModule(_ context.Context, module string) error { + c.DisabledModules = append(c.DisabledModules, module) + return c.ReturnDisableModuleErr +} diff --git a/internal/kube/fake/rootlessdynamic.go b/internal/kube/fake/rootlessdynamic.go new file mode 100644 index 000000000..5df6ef083 --- /dev/null +++ b/internal/kube/fake/rootlessdynamic.go @@ -0,0 +1,52 @@ +package fake + +import ( + "context" + + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +type RootlessDynamicClient struct { + // outputs + ReturnErr error + ReturnGetErr error + ReturnRemoveErr error + ReturnGetObj unstructured.Unstructured + ReturnListObjs *unstructured.UnstructuredList + + // inputs summary + GetObjs []unstructured.Unstructured + ListObjs []unstructured.Unstructured + RemovedObjs []unstructured.Unstructured + ApplyObjs []unstructured.Unstructured +} + +func (m *RootlessDynamicClient) Apply(_ context.Context, obj *unstructured.Unstructured) error { + m.ApplyObjs = append(m.ApplyObjs, *obj) + return m.ReturnErr +} + +func (m *RootlessDynamicClient) ApplyMany(_ context.Context, objs []unstructured.Unstructured) error { + m.ApplyObjs = append(m.ApplyObjs, objs...) + return m.ReturnErr +} + +func (m *RootlessDynamicClient) Get(_ context.Context, obj *unstructured.Unstructured) (*unstructured.Unstructured, error) { + m.GetObjs = append(m.GetObjs, *obj) + return &m.ReturnGetObj, m.ReturnGetErr +} + +func (m *RootlessDynamicClient) List(_ context.Context, obj *unstructured.Unstructured) (*unstructured.UnstructuredList, error) { + m.ListObjs = append(m.ListObjs, *obj) + return m.ReturnListObjs, m.ReturnErr +} + +func (m *RootlessDynamicClient) Remove(_ context.Context, obj *unstructured.Unstructured) error { + m.RemovedObjs = append(m.RemovedObjs, *obj) + return m.ReturnRemoveErr +} + +func (m *RootlessDynamicClient) RemoveMany(_ context.Context, objs []unstructured.Unstructured) error { + m.RemovedObjs = append(m.RemovedObjs, objs...) + return m.ReturnRemoveErr +} diff --git a/internal/kube/kyma/kyma.go b/internal/kube/kyma/kyma.go index ddb5ac038..e3a12f02d 100644 --- a/internal/kube/kyma/kyma.go +++ b/internal/kube/kyma/kyma.go @@ -2,6 +2,7 @@ package kyma import ( "context" + "fmt" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -12,8 +13,10 @@ import ( ) const ( - DefaultKymaName = "default" - DefaultKymaNamespace = "kyma-system" + DefaultKymaName = "default" + DefaultKymaNamespace = "kyma-system" + CustomResourcePolicyIgnore = "Ignore" + CustomResourcePolicyCreateAndDelete = "CreateAndDelete" ) type Interface interface { @@ -21,7 +24,9 @@ type Interface interface { ListModuleTemplate(context.Context) (*ModuleTemplateList, error) GetDefaultKyma(context.Context) (*Kyma, error) UpdateDefaultKyma(context.Context, *Kyma) error - EnableModule(context.Context, string, string) error + WaitForModuleState(context.Context, string, ...string) error + GetModuleInfo(context.Context, string) (*KymaModuleInfo, error) + EnableModule(context.Context, string, string, string) error DisableModule(context.Context, string) error } @@ -60,6 +65,16 @@ func (c *client) GetDefaultKyma(ctx context.Context) (*Kyma, error) { return kyma, err } +// GetModuleState returns state of the specific module based on the default kyma on the cluster +func (c *client) GetModuleInfo(ctx context.Context, moduleName string) (*KymaModuleInfo, error) { + kymaCR, err := c.GetDefaultKyma(ctx) + if err != nil { + return nil, err + } + + return getmoduleInfo(kymaCR, moduleName), nil +} + // UpdateDefaultKyma updates the default Kyma CR from the kyma-system namespace based on the Kyma CR from arguments func (c *client) UpdateDefaultKyma(ctx context.Context, obj *Kyma) error { u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj) @@ -74,15 +89,45 @@ func (c *client) UpdateDefaultKyma(ctx context.Context, obj *Kyma) error { return err } +// WaitForModuleState waits until module is not in on of given expected states +func (c *client) WaitForModuleState(ctx context.Context, moduleName string, expectedStates ...string) error { + watcher, err := c.dynamic.Resource(GVRKyma). + Namespace(DefaultKymaNamespace). + Watch(ctx, metav1.ListOptions{ + FieldSelector: fmt.Sprintf("metadata.name=%s", DefaultKymaName), + }) + if err != nil { + return err + } + defer watcher.Stop() + + var lastErr error + for { + select { + case <-ctx.Done(): + return fmt.Errorf("%s with last error: %s", ctx.Err(), lastErr) + case event := <-watcher.ResultChan(): + err = checkModuleState(event.Object, moduleName, expectedStates...) + if err != nil { + // set last error and try one more time + lastErr = err + continue + } + + return nil + } + } +} + // EnableModule adds module to the default Kyma CR in the kyma-system namespace // if moduleChannel is empty it uses default channel in the Kyma CR -func (c *client) EnableModule(ctx context.Context, moduleName, moduleChannel string) error { +func (c *client) EnableModule(ctx context.Context, moduleName, moduleChannel, customResourcePolicy string) error { kymaCR, err := c.GetDefaultKyma(ctx) if err != nil { return err } - kymaCR = enableModule(kymaCR, moduleName, moduleChannel) + kymaCR = enableModule(kymaCR, moduleName, moduleChannel, customResourcePolicy) return c.UpdateDefaultKyma(ctx, kymaCR) } @@ -99,18 +144,55 @@ func (c *client) DisableModule(ctx context.Context, moduleName string) error { return c.UpdateDefaultKyma(ctx, kymaCR) } -func enableModule(kymaCR *Kyma, moduleName, moduleChannel string) *Kyma { +func checkModuleState(kymaObj runtime.Object, moduleName string, expectedStates ...string) error { + kyma := &Kyma{} + err := runtime.DefaultUnstructuredConverter.FromUnstructured(kymaObj.(*unstructured.Unstructured).Object, kyma) + if err != nil { + return err + } + + moduleInfo := getmoduleInfo(kyma, moduleName) + + for _, expectedState := range expectedStates { + if moduleInfo.Status.State == expectedState { + return nil + } + } + + return fmt.Errorf("module %s is in the %s state", moduleName, moduleInfo.Status.State) +} + +func getmoduleInfo(kymaCR *Kyma, moduleName string) *KymaModuleInfo { + info := KymaModuleInfo{} + for _, module := range kymaCR.Spec.Modules { + if module.Name == moduleName { + info.Spec = module + } + } + + for _, module := range kymaCR.Status.Modules { + if module.Name == moduleName { + info.Status = module + } + } + + return &info +} + +func enableModule(kymaCR *Kyma, moduleName, moduleChannel, customResourcePolicy string) *Kyma { for i, m := range kymaCR.Spec.Modules { if m.Name == moduleName { // module already exists, update channel kymaCR.Spec.Modules[i].Channel = moduleChannel + kymaCR.Spec.Modules[i].CustomResourcePolicy = customResourcePolicy return kymaCR } } kymaCR.Spec.Modules = append(kymaCR.Spec.Modules, Module{ - Name: moduleName, - Channel: moduleChannel, + Name: moduleName, + Channel: moduleChannel, + CustomResourcePolicy: customResourcePolicy, }) return kymaCR diff --git a/internal/kube/kyma/kyma_test.go b/internal/kube/kyma/kyma_test.go index 0a4dd06d1..fb02515fd 100644 --- a/internal/kube/kyma/kyma_test.go +++ b/internal/kube/kyma/kyma_test.go @@ -36,6 +36,15 @@ func TestGetDefaultKyma(t *testing.T) { }, }, }, + Status: KymaStatus{ + Modules: []ModuleStatus{ + { + Name: "test-module", + Version: "1.2.3", + State: "Ready", + }, + }, + }, } kyma, err := client.GetDefaultKyma(context.Background()) @@ -209,19 +218,21 @@ func Test_disableModule(t *testing.T) { } } -func Test_updateCR(t *testing.T) { +func Test_enableModule(t *testing.T) { t.Parallel() tests := []struct { - name string - kymaCR *Kyma - moduleName string - channel string - want *Kyma + name string + kymaCR *Kyma + moduleName string + customResourcePolicy string + channel string + want *Kyma }{ { - name: "unchanged modules list", - moduleName: "module", - channel: "", + name: "unchanged modules list", + moduleName: "module", + channel: "", + customResourcePolicy: "", kymaCR: &Kyma{ Spec: KymaSpec{ Modules: []Module{ @@ -268,9 +279,10 @@ func Test_updateCR(t *testing.T) { }, }, { - name: "added module with channel", - moduleName: "module", - channel: "channel", + name: "added module with channel and customResourcePolicy", + moduleName: "module", + channel: "channel", + customResourcePolicy: "customResourcePolicy", kymaCR: &Kyma{ Spec: KymaSpec{ Modules: []Module{ @@ -287,17 +299,19 @@ func Test_updateCR(t *testing.T) { Name: "istio", }, { - Name: "module", - Channel: "channel", + Name: "module", + Channel: "channel", + CustomResourcePolicy: "customResourcePolicy", }, }, }, }, }, { - name: "added channel to existing module", - moduleName: "module", - channel: "channel", + name: "added channel and customResourcePolicy to existing module", + moduleName: "module", + channel: "channel", + customResourcePolicy: "customResourcePolicy", kymaCR: &Kyma{ Spec: KymaSpec{ Modules: []Module{ @@ -311,8 +325,9 @@ func Test_updateCR(t *testing.T) { Spec: KymaSpec{ Modules: []Module{ { - Name: "module", - Channel: "channel", + Name: "module", + Channel: "channel", + CustomResourcePolicy: "customResourcePolicy", }, }, }, @@ -347,9 +362,10 @@ func Test_updateCR(t *testing.T) { kymaCR := tt.kymaCR moduleName := tt.moduleName moduleChannel := tt.channel + customResourcePolicy := tt.customResourcePolicy want := tt.want t.Run(tt.name, func(t *testing.T) { - got := enableModule(kymaCR, moduleName, moduleChannel) + got := enableModule(kymaCR, moduleName, moduleChannel, customResourcePolicy) gotBytes, err := json.Marshal(got) require.NoError(t, err) wantBytes, err := json.Marshal(want) @@ -384,6 +400,15 @@ func fixDefaultKyma() *unstructured.Unstructured { }, }, }, + "status": map[string]interface{}{ + "modules": []interface{}{ + map[string]interface{}{ + "name": "test-module", + "version": "1.2.3", + "state": "Ready", + }, + }, + }, }, } } @@ -426,6 +451,37 @@ func Test_client_ListModuleTemplate(t *testing.T) { }) } +func Test_client_GetModuleInfo(t *testing.T) { + t.Run("get ModuleInfo", func(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(GVRKyma.GroupVersion()) + client := NewClient(dynamic_fake.NewSimpleDynamicClient(scheme, fixDefaultKyma())) + + got, err := client.GetModuleInfo(context.Background(), "test-module") + require.NoError(t, err) + require.Equal(t, KymaModuleInfo{ + Spec: Module{ + Name: "test-module", + }, + Status: ModuleStatus{ + Name: "test-module", + Version: "1.2.3", + State: "Ready", + }, + }, *got) + }) + + t.Run("get error", func(t *testing.T) { + scheme := runtime.NewScheme() + scheme.AddKnownTypes(GVRKyma.GroupVersion()) + client := NewClient(dynamic_fake.NewSimpleDynamicClient(scheme)) + + got, err := client.GetModuleInfo(context.Background(), "test-module") + require.ErrorContains(t, err, "not found") + require.Nil(t, got) + }) +} + func fixModuleReleaseMetaStruct(moduleName string) ModuleReleaseMeta { return ModuleReleaseMeta{ TypeMeta: v1.TypeMeta{ diff --git a/internal/kube/kyma/types.go b/internal/kube/kyma/types.go index 117d628b7..b6bb16d9b 100644 --- a/internal/kube/kyma/types.go +++ b/internal/kube/kyma/types.go @@ -129,6 +129,11 @@ type ModuleStatus struct { State string `json:"state,omitempty"` } +type KymaModuleInfo struct { + Spec Module + Status ModuleStatus +} + // ModuleFromInterface converts a map retrieved from the Unstructured kyma CR to a Module struct. func ModuleFromInterface(i map[string]interface{}) Module { module := Module{Name: i["name"].(string)} diff --git a/internal/kube/resources/resources_test.go b/internal/kube/resources/resources_test.go index a0358aedd..b961093c1 100644 --- a/internal/kube/resources/resources_test.go +++ b/internal/kube/resources/resources_test.go @@ -6,9 +6,9 @@ import ( "testing" "github.com/kyma-project/cli.v3/internal/cmdcommon/types" + "github.com/kyma-project/cli.v3/internal/kube/fake" kube_fake "github.com/kyma-project/cli.v3/internal/kube/fake" "github.com/kyma-project/cli.v3/internal/kube/istio" - "github.com/kyma-project/cli.v3/internal/kube/rootlessdynamic" "github.com/stretchr/testify/require" appsv1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" @@ -80,7 +80,7 @@ func Test_CreateClusterRoleBinding(t *testing.T) { &ClusterRoleBinding, &existingClusterRole, ) - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: staticClient, } err := CreateClusterRoleBinding(ctx, kubeClient, username, namespace, clusterRole) @@ -146,7 +146,7 @@ func Test_CreateDeployment(t *testing.T) { staticClient := k8s_fake.NewSimpleClientset( &existingDeployment, ) - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: staticClient, } @@ -201,7 +201,7 @@ func Test_CreateService(t *testing.T) { staticClient := k8s_fake.NewSimpleClientset( &existingService, ) - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: staticClient, } @@ -219,7 +219,7 @@ func Test_CreateService(t *testing.T) { func Test_CreateAPIRule(t *testing.T) { t.Run("create apiRule", func(t *testing.T) { ctx := context.Background() - rootlessdynamic := &rootlessdynamic.Fake{} + rootlessdynamic := &fake.RootlessDynamicClient{} apiRuleName := "apiRule" namespace := "default" host := "example.com" @@ -233,7 +233,7 @@ func Test_CreateAPIRule(t *testing.T) { }) t.Run("do not allow creating existing apiRule", func(t *testing.T) { ctx := context.Background() - rootlessdynamic := &rootlessdynamic.Fake{ + rootlessdynamic := &fake.RootlessDynamicClient{ ReturnErr: fmt.Errorf("already exists"), } apiRuleName := "existing" diff --git a/internal/kube/rootlessdynamic/client.go b/internal/kube/rootlessdynamic/client.go index 8328ba20b..a8e8e5f3d 100644 --- a/internal/kube/rootlessdynamic/client.go +++ b/internal/kube/rootlessdynamic/client.go @@ -3,9 +3,10 @@ package rootlessdynamic import ( "context" "fmt" - "k8s.io/apimachinery/pkg/api/errors" "strings" + "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" @@ -76,6 +77,11 @@ func (c *client) Apply(ctx context.Context, resource *unstructured.Unstructured) } if apiResource.Namespaced { + if resource.GetNamespace() == "" { + // make resource has namespace set + resource.SetNamespace("default") + } + err = c.applyFunc(ctx, c.dynamic.Resource(*gvr).Namespace(resource.GetNamespace()), resource) if err != nil { return fmt.Errorf("failed to apply namespaced resource: %w", err) diff --git a/internal/kubeconfig/kubeconfig_test.go b/internal/kubeconfig/kubeconfig_test.go index 8b0b24a91..d3b788c48 100644 --- a/internal/kubeconfig/kubeconfig_test.go +++ b/internal/kubeconfig/kubeconfig_test.go @@ -139,7 +139,7 @@ func Test_Prepare(t *testing.T) { &serviceAccount, &secret, ) - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestAPIConfig: apiConfig, TestKubernetesInterface: staticClient, } @@ -204,7 +204,7 @@ func Test_getServiceAccountToken(t *testing.T) { staticClient := k8s_fake.NewSimpleClientset( &serviceAccount, ) - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: staticClient, } diff --git a/internal/modules/enable.go b/internal/modules/enable.go new file mode 100644 index 000000000..8db03aea4 --- /dev/null +++ b/internal/modules/enable.go @@ -0,0 +1,68 @@ +package modules + +import ( + "context" + "fmt" + "io" + "os" + "time" + + "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" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +// Enable takes care about enabling kyma module in order: +// 1. add module to the Kyma CR with CustomResourcePolicy set to CreateAndDelete if defaultCR is true and to Ingnore in any other case +// 2. if crs array is not empty wait for the module to be ready and add crs to the cluster +func Enable(ctx context.Context, client kube.Client, module, channel string, defaultCR bool, crs ...unstructured.Unstructured) clierror.Error { + return enable(os.Stdout, ctx, client, module, channel, defaultCR, crs...) +} + +func enable(writer io.Writer, ctx context.Context, client kube.Client, module, channel string, defaultCR bool, crs ...unstructured.Unstructured) clierror.Error { + crPolicy := kyma.CustomResourcePolicyIgnore + if defaultCR { + crPolicy = kyma.CustomResourcePolicyCreateAndDelete + } + + fmt.Fprintf(writer, "adding %s module to the Kyma CR\n", module) + err := client.Kyma().EnableModule(ctx, module, channel, crPolicy) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to enable module")) + } + + clierr := applyCustomCR(writer, ctx, client, module, crs...) + if clierr != nil { + return clierr + } + + fmt.Fprintf(writer, "%s module enabled\n", module) + return nil +} + +func applyCustomCR(writer io.Writer, ctx context.Context, client kube.Client, module string, crs ...unstructured.Unstructured) clierror.Error { + if len(crs) == 0 { + // skip if there is nothing to do + return nil + } + + ctx, cancel := context.WithTimeout(ctx, time.Second*100) + defer cancel() + + fmt.Fprintf(writer, "waiting for module to be ready\n") + err := client.Kyma().WaitForModuleState(ctx, module, "Ready", "Warning") + if err != nil { + return clierror.Wrap(err, clierror.New("failed to check module state")) + } + + for _, cr := range crs { + fmt.Fprintf(writer, "applying %s/%s cr\n", cr.GetNamespace(), cr.GetName()) + err = client.RootlessDynamic().Apply(ctx, &cr) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to apply custom cr from path")) + } + } + + return nil +} diff --git a/internal/modules/enable_test.go b/internal/modules/enable_test.go new file mode 100644 index 000000000..19e714007 --- /dev/null +++ b/internal/modules/enable_test.go @@ -0,0 +1,135 @@ +package modules + +import ( + "bytes" + "context" + "errors" + "testing" + + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/kyma-project/cli.v3/internal/kube/fake" + "github.com/kyma-project/cli.v3/internal/kube/kyma" + "github.com/stretchr/testify/require" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" +) + +var ( + testKedaCR = unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": "test/v1", + "kind": "Keda", + "metadata": map[string]interface{}{ + "name": "default", + "namespace": "kyma-system", + }, + }, + } +) + +func TestEnable(t *testing.T) { + t.Run("enable module", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + kymaClient := fake.KymaClient{ + ReturnErr: nil, + } + client := fake.KubeClient{ + TestKymaInterface: &kymaClient, + } + + expectedEnabledModule := fake.FakeEnabledModule{ + Name: "keda", + Channel: "fast", + CustomResourcePolicy: kyma.CustomResourcePolicyCreateAndDelete, + } + + err := enable(buffer, context.Background(), &client, "keda", "fast", true) + require.Nil(t, err) + require.Equal(t, "adding keda module to the Kyma CR\nkeda module enabled\n", buffer.String()) + require.Equal(t, []fake.FakeEnabledModule{expectedEnabledModule}, kymaClient.EnabledModules) + }) + + t.Run("enable module and add custom cr", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + kymaClient := fake.KymaClient{ + ReturnErr: nil, + } + rootlessDynamicClient := fake.RootlessDynamicClient{} + client := fake.KubeClient{ + TestKymaInterface: &kymaClient, + TestRootlessDynamicInterface: &rootlessDynamicClient, + } + + expectedEnabledModule := fake.FakeEnabledModule{ + Name: "keda", + Channel: "fast", + CustomResourcePolicy: kyma.CustomResourcePolicyIgnore, + } + + err := enable(buffer, context.Background(), &client, "keda", "fast", false, testKedaCR) + require.Nil(t, err) + require.Equal(t, "adding keda module to the Kyma CR\nwaiting for module to be ready\napplying kyma-system/default cr\nkeda module enabled\n", buffer.String()) + require.Equal(t, []fake.FakeEnabledModule{expectedEnabledModule}, kymaClient.EnabledModules) + require.Equal(t, []unstructured.Unstructured{testKedaCR}, rootlessDynamicClient.ApplyObjs) + }) + + t.Run("failed to enable module", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + kymaClient := fake.KymaClient{ + ReturnErr: errors.New("test error"), + } + client := fake.KubeClient{ + TestKymaInterface: &kymaClient, + } + + expectedCliErr := clierror.Wrap( + errors.New("test error"), + clierror.New("failed to enable module"), + ) + + err := enable(buffer, context.Background(), &client, "keda", "fast", true) + require.Equal(t, expectedCliErr, err) + require.Equal(t, "adding keda module to the Kyma CR\n", buffer.String()) + }) + + t.Run("failed to wait for module to be ready", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + kymaClient := fake.KymaClient{ + ReturnWaitForModuleErr: errors.New("test error"), + } + client := fake.KubeClient{ + TestKymaInterface: &kymaClient, + } + + expectedCliErr := clierror.Wrap( + errors.New("test error"), + clierror.New("failed to check module state"), + ) + + err := enable(buffer, context.Background(), &client, "keda", "fast", false, testKedaCR) + require.Equal(t, expectedCliErr, err) + require.Equal(t, "adding keda module to the Kyma CR\nwaiting for module to be ready\n", buffer.String()) + }) + + t.Run("failed to apply custom resource", func(t *testing.T) { + buffer := bytes.NewBuffer([]byte{}) + kymaClient := fake.KymaClient{ + ReturnErr: nil, + } + rootlessDynamicClient := fake.RootlessDynamicClient{ + ReturnErr: errors.New("test error"), + } + client := fake.KubeClient{ + TestKymaInterface: &kymaClient, + TestRootlessDynamicInterface: &rootlessDynamicClient, + } + + expectedCliErr := clierror.Wrap( + errors.New("test error"), + clierror.New("failed to apply custom cr from path"), + ) + + err := enable(buffer, context.Background(), &client, "keda", "fast", false, testKedaCR) + require.Equal(t, expectedCliErr, err) + require.Equal(t, "adding keda module to the Kyma CR\nwaiting for module to be ready\napplying kyma-system/default cr\n", buffer.String()) + }) +} diff --git a/internal/registry/config_test.go b/internal/registry/config_test.go index 285c081a6..981b1a0c7 100644 --- a/internal/registry/config_test.go +++ b/internal/registry/config_test.go @@ -36,7 +36,7 @@ func TestGetExternalConfig(t *testing.T) { }, } - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: client, TestDynamicInterface: dynamic, } @@ -80,7 +80,7 @@ func TestGetInternalConfig(t *testing.T) { }, } - kubeClient := &kube_fake.FakeKubeClient{ + kubeClient := &kube_fake.KubeClient{ TestKubernetesInterface: client, TestDynamicInterface: dynamic, }