diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e0f97e..390bb34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,22 @@ ### New features -* Add command `list` for showing duplicated resources. +* Add command `list` for showing duplicated resources. Example: + +```shell +kubectl duplicate list -n my-namespace +``` + +* Add command `cleanup` for deleting duplicated resources. Example: + +```shell +kubectl duplicate cleanup -n my-namespace +``` ### Chores -* Switch to [Dynamic Client](https://github.com/kubernetes/client-go/blob/master/examples/dynamic-create-update-delete-deployment/README.md), +* Switch + to [Dynamic Client](https://github.com/kubernetes/client-go/blob/master/examples/dynamic-create-update-delete-deployment/README.md), opening the door for duplicating any resource type. ## v0.2.1 diff --git a/pkg/clients/duplik8s_client.go b/pkg/clients/duplik8s_client.go index 83fe141..87e0589 100644 --- a/pkg/clients/duplik8s_client.go +++ b/pkg/clients/duplik8s_client.go @@ -24,6 +24,7 @@ import ( "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/client-go/discovery" "k8s.io/client-go/dynamic" + "k8s.io/client-go/restmapper" "slices" "strings" ) @@ -68,6 +69,29 @@ func (c Duplik8sClient) ListDuplicable( return objs, nil } +func (c Duplik8sClient) Delete( + ctx context.Context, + obj core.DuplicatedObject, +) error { + // Get a RESTMapper + resources, err := restmapper.GetAPIGroupResources(c.discovery) + if err != nil { + panic(err.Error()) + } + restMapper := restmapper.NewDiscoveryRESTMapper(resources) + + // Convert GroupVersionKind to GroupVersionResource + mapping, err := restMapper.RESTMapping( + obj.ObjectKind.GroupVersionKind().GroupKind(), + obj.ObjectKind.GroupVersionKind().Version, + ) + if err != nil { + panic(err.Error()) + } + + return c.dynamic.Resource(mapping.Resource).Namespace(obj.Namespace).Delete(ctx, obj.Name, metav1.DeleteOptions{}) +} + func (c Duplik8sClient) ListDuplicated( ctx context.Context, namespace string, diff --git a/pkg/cmd/cleanup.go b/pkg/cmd/cleanup.go new file mode 100644 index 0000000..a8093aa --- /dev/null +++ b/pkg/cmd/cleanup.go @@ -0,0 +1,81 @@ +/* + * Copyright 2024 Michele Zanotti + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cmd + +import ( + "context" + "fmt" + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "github.com/telemaco019/duplik8s/pkg/clients" + "github.com/telemaco019/duplik8s/pkg/core" +) + +func cleanup(client core.Client, namespace string) error { + duplicated, err := client.ListDuplicated(context.Background(), namespace) + if err != nil { + return err + } + if len(duplicated) == 0 { + fmt.Printf("No duplicated resources found in namespace %q\n", namespace) + return nil + } + + renderDuplicatedObjects(duplicated) + + var shouldDelete bool + err = huh.NewConfirm().Title("Do you want to delete the following resources?").Value(&shouldDelete).Run() + if err != nil { + return err + } + + if shouldDelete { + for _, obj := range duplicated { + err = client.Delete(context.Background(), obj) + if err != nil { + return err + } + fmt.Printf("deleted %s %s/%s\n", obj.ObjectKind.GroupVersionKind().Kind, obj.Namespace, obj.Name) + } + } + + return nil +} + +func NewCleanupCmd(client core.Client) *cobra.Command { + podCmd := &cobra.Command{ + Use: "cleanup", + Short: "Cleanup duplicated resources.", + Args: cobra.MaximumNArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cmd.SilenceUsage = true + opts, err := NewKubeOptions(cmd, args) + if err != nil { + return err + } + if client == nil { + client, err = clients.NewDuplik8sClient(opts) + if err != nil { + return err + } + } + return cleanup(client, opts.Namespace) + }, + } + addOverrideFlags(podCmd) + return podCmd +} diff --git a/pkg/cmd/list_duplicated.go b/pkg/cmd/list_duplicated.go index da5a015..24704b2 100644 --- a/pkg/cmd/list_duplicated.go +++ b/pkg/cmd/list_duplicated.go @@ -19,12 +19,9 @@ package cmd import ( "context" "fmt" - "github.com/charmbracelet/lipgloss" - "github.com/charmbracelet/lipgloss/table" "github.com/spf13/cobra" "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" - "github.com/telemaco019/duplik8s/pkg/utils" ) func listDuplicatedResources(client core.Client, namespace string) error { @@ -32,33 +29,13 @@ func listDuplicatedResources(client core.Client, namespace string) error { if err != nil { return err } + if len(duplicatedObjs) == 0 { fmt.Printf("No duplicated resources found in namespace %q\n", namespace) return nil } - headerStyle := lipgloss.NewStyle().Bold(true).Padding(0, 1) - defaultStyle := lipgloss.NewStyle().Padding(0, 1) - t := table.New().Border(lipgloss.HiddenBorder()). - StyleFunc(func(row, col int) lipgloss.Style { - switch { - case row == 0: - return headerStyle - default: - return defaultStyle - } - }). - Headers("Namespace", "Kind", "Name", "Age") - for _, obj := range duplicatedObjs { - t.Row( - obj.Namespace, - obj.ObjectKind.GroupVersionKind().Kind, - obj.Name, - utils.FormatAge(obj.CreationTimestamp), - ) - } - fmt.Print(t.Render() + "\n") + renderDuplicatedObjects(duplicatedObjs) return nil - } func NewListDuplicatedCmd(client core.Client) *cobra.Command { diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 73f7e97..e724a8c 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -52,9 +52,10 @@ func NewRootCmd( // add subcommands rootCmd.AddCommand(NewPodCmd(duplicator, client)) - rootCmd.AddCommand(NewListDuplicatedCmd(client)) rootCmd.AddCommand(NewDeployCmd(duplicator, client)) rootCmd.AddCommand(NewStatefulSetCmd(duplicator, client)) + rootCmd.AddCommand(NewListDuplicatedCmd(client)) + rootCmd.AddCommand(NewCleanupCmd(client)) return rootCmd } diff --git a/pkg/cmd/shared.go b/pkg/cmd/shared.go index 7538456..8276ebd 100644 --- a/pkg/cmd/shared.go +++ b/pkg/cmd/shared.go @@ -19,6 +19,8 @@ package cmd import ( "context" "fmt" + "github.com/charmbracelet/lipgloss" + "github.com/charmbracelet/lipgloss/table" "github.com/spf13/cobra" "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/cmd/flags" @@ -109,3 +111,27 @@ func addOverrideFlags(cmd *cobra.Command) { "Override the command of each container in the Pod.", ) } + +func renderDuplicatedObjects(duplicatedObjs []core.DuplicatedObject) { + headerStyle := lipgloss.NewStyle().Bold(true).Padding(0, 1) + defaultStyle := lipgloss.NewStyle().Padding(0, 1) + t := table.New().Border(lipgloss.HiddenBorder()). + StyleFunc(func(row, col int) lipgloss.Style { + switch { + case row == 0: + return headerStyle + default: + return defaultStyle + } + }). + Headers("Namespace", "Kind", "Name", "Age") + for _, obj := range duplicatedObjs { + t.Row( + obj.Namespace, + obj.ObjectKind.GroupVersionKind().Kind, + obj.Name, + utils.FormatAge(obj.CreationTimestamp), + ) + } + fmt.Print(t.Render() + "\n") +} diff --git a/pkg/core/types.go b/pkg/core/types.go index c6794e4..9395a20 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -35,6 +35,7 @@ type Client interface { namespace string, ) ([]DuplicableObject, error) ListDuplicated(ctx context.Context, namespace string) ([]DuplicatedObject, error) + Delete(ctx context.Context, obj DuplicatedObject) error } type PodOverrideOptions struct { diff --git a/pkg/test/mocks/pod_client.go b/pkg/test/mocks/pod_client.go index b5d5d72..e55de76 100644 --- a/pkg/test/mocks/pod_client.go +++ b/pkg/test/mocks/pod_client.go @@ -76,3 +76,7 @@ func (c *PodClient) ListDuplicated( ) ([]core.DuplicatedObject, error) { return c.ListDuplicatedResult, nil } + +func (c *PodClient) Delete(ctx context.Context, obj core.DuplicatedObject) error { + return nil +}