diff --git a/internal/cmd/alpha/module/manage.go b/internal/cmd/alpha/module/manage.go new file mode 100644 index 000000000..e002e2e2c --- /dev/null +++ b/internal/cmd/alpha/module/manage.go @@ -0,0 +1,64 @@ +package module + +import ( + "fmt" + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/spf13/cobra" +) + +type manageConfig struct { + *cmdcommon.KymaConfig + + module string + policy string +} + +func newManageCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { + cfg := manageConfig{ + KymaConfig: kymaConfig, + } + + cmd := &cobra.Command{ + Use: "manage ", + Short: "Manage module.", + Long: "Use this command to manage an existing module.", + + PreRun: func(_ *cobra.Command, args []string) { + clierror.Check(cfg.validate()) + }, + Run: func(cmd *cobra.Command, args []string) { + clierror.Check(runManage(&cfg)) + }, + } + + cmd.Flags().StringVar(&cfg.module, "module", "", "Name of the module to manage") + cmd.Flags().StringVar(&cfg.policy, "policy", "CreateAndDelete", "Set custom resource policy. (Possible values: CreateAndDelete, Ignore)") + _ = cmd.MarkFlagRequired("module") + return cmd +} + +func (mc *manageConfig) validate() clierror.Error { + if mc.policy != "CreateAndDelete" && mc.policy != "Ignore" { + return clierror.New(fmt.Sprintf("invalid policy %q, only CreateAndDelete and Ignore are allowed", mc.policy)) + } + + return nil +} + +func runManage(cfg *manageConfig) clierror.Error { + client, clierr := cfg.GetKubeClientWithClierr() + if clierr != nil { + return clierr + } + + err := client.Kyma().ManageModule(cfg.Ctx, cfg.module, cfg.policy) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to set module as managed")) + } + err = client.Kyma().WaitForModuleState(cfg.Ctx, cfg.module, "Ready", "Warning") + if err != nil { + return clierror.Wrap(err, clierror.New("failed to check module state")) + } + return nil +} diff --git a/internal/cmd/alpha/module/module.go b/internal/cmd/alpha/module/module.go index d2bd07f8b..b64537459 100644 --- a/internal/cmd/alpha/module/module.go +++ b/internal/cmd/alpha/module/module.go @@ -16,6 +16,8 @@ func NewModuleCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { cmd.AddCommand(newListCMD(kymaConfig)) cmd.AddCommand(newAddCMD(kymaConfig)) cmd.AddCommand(newDeleteCMD(kymaConfig)) + cmd.AddCommand(newManageCMD(kymaConfig)) + cmd.AddCommand(newUnmanageCMD(kymaConfig)) return cmd } diff --git a/internal/cmd/alpha/module/unmanage.go b/internal/cmd/alpha/module/unmanage.go new file mode 100644 index 000000000..8e749a204 --- /dev/null +++ b/internal/cmd/alpha/module/unmanage.go @@ -0,0 +1,50 @@ +package module + +import ( + "github.com/kyma-project/cli.v3/internal/clierror" + "github.com/kyma-project/cli.v3/internal/cmdcommon" + "github.com/spf13/cobra" +) + +type unmanageConfig struct { + *cmdcommon.KymaConfig + + module string +} + +func newUnmanageCMD(kymaConfig *cmdcommon.KymaConfig) *cobra.Command { + cfg := unmanageConfig{ + KymaConfig: kymaConfig, + } + + cmd := &cobra.Command{ + Use: "unmanage ", + Short: "Unmanage module.", + Long: "Use this command to unmanage an existing module.", + Run: func(cmd *cobra.Command, args []string) { + clierror.Check(runUnmanage(&cfg)) + }, + } + + cmd.Flags().StringVar(&cfg.module, "module", "", "Name of the module to unmanage") + _ = cmd.MarkFlagRequired("module") + return cmd +} + +func runUnmanage(cfg *unmanageConfig) clierror.Error { + client, clierr := cfg.GetKubeClientWithClierr() + if clierr != nil { + return clierr + } + + err := client.Kyma().UnmanageModule(cfg.Ctx, cfg.module) + if err != nil { + return clierror.Wrap(err, clierror.New("failed to set module as unmanaged")) + } + + err = client.Kyma().WaitForModuleState(cfg.Ctx, cfg.module, "Unmanaged") + if err != nil { + return clierror.Wrap(err, clierror.New("failed to check module state")) + } + return nil +} diff --git a/internal/kube/fake/kyma.go b/internal/kube/fake/kyma.go index 487adb773..39f5ba98d 100644 --- a/internal/kube/fake/kyma.go +++ b/internal/kube/fake/kyma.go @@ -78,3 +78,11 @@ func (c *KymaClient) DisableModule(_ context.Context, module string) error { c.DisabledModules = append(c.DisabledModules, module) return c.ReturnDisableModuleErr } + +func (c *KymaClient) ManageModule(_ context.Context, _, _ string) error { + return c.ReturnWaitForModuleErr +} + +func (c *KymaClient) UnmanageModule(_ context.Context, _ string) error { + return c.ReturnWaitForModuleErr +} diff --git a/internal/kube/kyma/kyma.go b/internal/kube/kyma/kyma.go index b9f3e7c9a..25623d678 100644 --- a/internal/kube/kyma/kyma.go +++ b/internal/kube/kyma/kyma.go @@ -2,7 +2,9 @@ package kyma import ( "context" + "errors" "fmt" + "k8s.io/utils/ptr" "slices" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -30,6 +32,8 @@ type Interface interface { WaitForModuleState(context.Context, string, ...string) error EnableModule(context.Context, string, string, string) error DisableModule(context.Context, string) error + ManageModule(context.Context, string, string) error + UnmanageModule(context.Context, string) error } type client struct { @@ -179,6 +183,36 @@ func (c *client) DisableModule(ctx context.Context, moduleName string) error { return c.UpdateDefaultKyma(ctx, kymaCR) } +// ManageModule configures given module as managed and updates Kyma CR in the kyma-system namespace with it +func (c *client) ManageModule(ctx context.Context, moduleName, policy string) error { + kymaCR, err := c.GetDefaultKyma(ctx) + if err != nil { + return err + } + + kymaCR, err = manageModule(kymaCR, moduleName, policy) + if err != nil { + return err + } + + return c.UpdateDefaultKyma(ctx, kymaCR) +} + +// UnmanageModule configures given module as unmanaged and updates Kyma CR in the kyma-system namespace with it +func (c *client) UnmanageModule(ctx context.Context, moduleName string) error { + kymaCR, err := c.GetDefaultKyma(ctx) + if err != nil { + return err + } + + kymaCR, err = unmanageModule(kymaCR, moduleName) + if err != nil { + return err + } + + return c.UpdateDefaultKyma(ctx, kymaCR) +} + func checkModuleState(kymaObj runtime.Object, moduleName string, expectedStates ...string) error { kyma := &Kyma{} err := runtime.DefaultUnstructuredConverter.FromUnstructured(kymaObj.(*unstructured.Unstructured).Object, kyma) @@ -241,6 +275,33 @@ func disableModule(kymaCR *Kyma, moduleName string) *Kyma { return kymaCR } +func manageModule(kymaCR *Kyma, moduleName, policy string) (*Kyma, error) { + for i, m := range kymaCR.Spec.Modules { + if m.Name == moduleName { + // module exists, update managed + kymaCR.Spec.Modules[i].Managed = ptr.To(true) + kymaCR.Spec.Modules[i].CustomResourcePolicy = policy + + return kymaCR, nil + } + } + + return kymaCR, errors.New("module not found") +} + +func unmanageModule(kymaCR *Kyma, moduleName string) (*Kyma, error) { + for i, m := range kymaCR.Spec.Modules { + if m.Name == moduleName { + // module exists, update managed + kymaCR.Spec.Modules[i].Managed = ptr.To(false) + + return kymaCR, nil + } + } + + return kymaCR, errors.New("module not found") +} + func list[T any](ctx context.Context, client dynamic.Interface, gvr schema.GroupVersionResource) (*T, error) { list, err := client.Resource(gvr). List(ctx, metav1.ListOptions{}) diff --git a/internal/kube/kyma/kyma_test.go b/internal/kube/kyma/kyma_test.go index 476ef04c4..0453ca89d 100644 --- a/internal/kube/kyma/kyma_test.go +++ b/internal/kube/kyma/kyma_test.go @@ -3,6 +3,7 @@ package kyma import ( "context" "encoding/json" + "k8s.io/utils/ptr" "reflect" "testing" @@ -627,3 +628,200 @@ func fixModuleTemplate(moduleName string) *unstructured.Unstructured { }, } } + +func Test_manageModule(t *testing.T) { + t.Parallel() + tests := []struct { + name string + kymaCR *Kyma + moduleName string + policy string + want *Kyma + }{ + { + name: "unchanged module", + moduleName: "module", + policy: "CreateAndDelete", + kymaCR: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + CustomResourcePolicy: "CreateAndDelete", + }, + }, + }, + }, + want: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + CustomResourcePolicy: "CreateAndDelete", + }, + }, + }, + }, + }, + { + name: "already managed, configuration changed", + moduleName: "module", + policy: "Ignore", + kymaCR: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + CustomResourcePolicy: "CreateAndDelete", + }, + }, + }, + }, + want: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + CustomResourcePolicy: "Ignore", + }, + }, + }, + }, + }, + { + name: "module updated", + moduleName: "module", + policy: "CreateAndDelete", + kymaCR: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(false), + CustomResourcePolicy: "Ignore", + }, + }, + }, + }, + want: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + CustomResourcePolicy: "CreateAndDelete", + }, + }, + }, + }, + }, + } + for _, tt := range tests { + kymaCR := tt.kymaCR + moduleName := tt.moduleName + want := tt.want + policy := tt.policy + t.Run(tt.name, func(t *testing.T) { + got, err := manageModule(kymaCR, moduleName, policy) + require.NoError(t, err) + gotBytes, err := json.Marshal(got) + require.NoError(t, err) + wantBytes, err := json.Marshal(want) + require.NoError(t, err) + var gotInterface map[string]interface{} + var wantInterface map[string]interface{} + err = json.Unmarshal(gotBytes, &gotInterface) + require.NoError(t, err) + err = json.Unmarshal(wantBytes, &wantInterface) + require.NoError(t, err) + if !reflect.DeepEqual(gotInterface, wantInterface) { + t.Errorf("updateCR() = %v, want %v", gotInterface, wantInterface) + } + }) + } +} + +func Test_unmanageModule(t *testing.T) { + t.Parallel() + tests := []struct { + name string + kymaCR *Kyma + moduleName string + want *Kyma + }{ + { + name: "unchanged module", + moduleName: "module", + kymaCR: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(false), + }, + }, + }, + }, + want: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(false), + }, + }, + }, + }, + }, + { + name: "module updated", + moduleName: "module", + kymaCR: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(true), + }, + }, + }, + }, + want: &Kyma{ + Spec: KymaSpec{ + Modules: []Module{ + { + Name: "module", + Managed: ptr.To(false), + }, + }, + }, + }, + }, + } + for _, tt := range tests { + kymaCR := tt.kymaCR + moduleName := tt.moduleName + want := tt.want + t.Run(tt.name, func(t *testing.T) { + got, err := unmanageModule(kymaCR, moduleName) + require.NoError(t, err) + gotBytes, err := json.Marshal(got) + require.NoError(t, err) + wantBytes, err := json.Marshal(want) + require.NoError(t, err) + var gotInterface map[string]interface{} + var wantInterface map[string]interface{} + err = json.Unmarshal(gotBytes, &gotInterface) + require.NoError(t, err) + err = json.Unmarshal(wantBytes, &wantInterface) + require.NoError(t, err) + if !reflect.DeepEqual(gotInterface, wantInterface) { + t.Errorf("updateCR() = %v, want %v", gotInterface, wantInterface) + } + }) + } +} diff --git a/internal/kube/kyma/types.go b/internal/kube/kyma/types.go index 1724658c9..c46b332b1 100644 --- a/internal/kube/kyma/types.go +++ b/internal/kube/kyma/types.go @@ -114,7 +114,7 @@ type Module struct { ControllerName string `json:"controller,omitempty"` Channel string `json:"channel,omitempty"` CustomResourcePolicy string `json:"customResourcePolicy,omitempty"` - Managed bool `json:"managed,omitempty"` + Managed *bool `json:"managed,omitempty"` } // KymaStatus defines the observed state of Kyma diff --git a/internal/modules/enable.go b/internal/modules/enable.go index 8db03aea4..b3d780365 100644 --- a/internal/modules/enable.go +++ b/internal/modules/enable.go @@ -14,7 +14,7 @@ import ( ) // 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 +// 1. add module to the Kyma CR with CustomResourcePolicy set to CreateAndDelete if defaultCR is true and to Ignore 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...) diff --git a/internal/modules/list.go b/internal/modules/list.go index 16c0f895c..bff41813b 100644 --- a/internal/modules/list.go +++ b/internal/modules/list.go @@ -287,7 +287,9 @@ func getInstallDetails(kyma *kyma.Kyma, releaseMetas kyma.ModuleReleaseMetaList, func getManaged(specModules []kyma.Module, moduleName string) Managed { for _, module := range specModules { if module.Name == moduleName { - return Managed(strconv.FormatBool(module.Managed)) + if module.Managed != nil { + return Managed(strconv.FormatBool(*module.Managed)) + } } }