diff --git a/.gitignore b/.gitignore index ef66526..d2f8437 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,5 @@ go.work go.work.sum bin/ dist/ + +.DS_Store \ No newline at end of file diff --git a/pkg/clients/duplik8s_client.go b/pkg/clients/duplik8s_client.go new file mode 100644 index 0000000..2d9f2d5 --- /dev/null +++ b/pkg/clients/duplik8s_client.go @@ -0,0 +1,60 @@ +/* + * 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 clients + +import ( + "context" + "github.com/telemaco019/duplik8s/pkg/core" + "github.com/telemaco019/duplik8s/pkg/utils" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/client-go/dynamic" +) + +type Duplik8sClient struct { + client dynamic.Interface +} + +func NewDuplik8sClient(opts utils.KubeOptions) (*Duplik8sClient, error) { + client, err := utils.NewDynamicClient(opts.Kubeconfig, opts.Kubecontext) + if err != nil { + return nil, err + } + return &Duplik8sClient{ + client: client, + }, nil +} + +func (c Duplik8sClient) ListDuplicable( + ctx context.Context, + resource schema.GroupVersionResource, + namespace string, +) ([]core.DuplicableObject, error) { + unstructuredList, err := c.client.Resource(resource).Namespace(namespace).List(ctx, metav1.ListOptions{}) + if err != nil { + return nil, err + } + var objs []core.DuplicableObject + for _, u := range unstructuredList.Items { + objs = append(objs, core.NewDuplicable(u)) + } + return objs, nil +} + +func (c Duplik8sClient) ListDuplicated(ctx context.Context) ([]core.DuplicableObject, error) { + return nil, nil +} diff --git a/pkg/cmd/deployment.go b/pkg/cmd/deployment.go index 060eaed..66e7aa5 100644 --- a/pkg/cmd/deployment.go +++ b/pkg/cmd/deployment.go @@ -18,24 +18,29 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" + "github.com/telemaco019/duplik8s/pkg/duplicators" "github.com/telemaco019/duplik8s/pkg/utils" + "k8s.io/apimachinery/pkg/runtime/schema" ) -func NewDeployCmd(client core.Duplik8sClient) *cobra.Command { - factory := func(opts utils.KubeOptions) (core.Duplik8sClient, error) { - if client == nil { - return clients.NewDeploymentClient(opts) +func NewDeployCmd(duplicator core.Duplicator, client core.Client) *cobra.Command { + factory := func(opts utils.KubeOptions) (core.Duplicator, error) { + if duplicator == nil { + return duplicators.NewDeploymentClient(opts) } - return client, nil + return duplicator, nil } deployCmd := &cobra.Command{ Use: "deploy", Short: "Duplicate a Deployment.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - run := newDuplicateCmd(factory, "Select a Deployment") + run := newDuplicateCmd(factory, client, schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "deployments", + }) return run(cmd, args) }, } diff --git a/pkg/cmd/pod.go b/pkg/cmd/pod.go index 24646fe..58f3eaf 100644 --- a/pkg/cmd/pod.go +++ b/pkg/cmd/pod.go @@ -18,24 +18,29 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" + "github.com/telemaco019/duplik8s/pkg/duplicators" "github.com/telemaco019/duplik8s/pkg/utils" + "k8s.io/apimachinery/pkg/runtime/schema" ) -func NewPodCmd(podClient core.Duplik8sClient) *cobra.Command { - factory := func(opts utils.KubeOptions) (core.Duplik8sClient, error) { - if podClient == nil { - return clients.NewPodClient(opts) +func NewPodCmd(duplicator core.Duplicator, client core.Client) *cobra.Command { + factory := func(opts utils.KubeOptions) (core.Duplicator, error) { + if duplicator == nil { + return duplicators.NewPodClient(opts) } - return podClient, nil + return duplicator, nil } podCmd := &cobra.Command{ Use: "pod", Short: "Duplicate a Pod.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - run := newDuplicateCmd(factory, "Select a Pod") + run := newDuplicateCmd(factory, client, schema.GroupVersionResource{ + Group: "", + Version: "v1", + Resource: "pods", + }) return run(cmd, args) }, } diff --git a/pkg/cmd/pod_test.go b/pkg/cmd/pod_test.go index c40fcef..dda1734 100644 --- a/pkg/cmd/pod_test.go +++ b/pkg/cmd/pod_test.go @@ -29,7 +29,7 @@ func Test_NoPodsAvailable(t *testing.T) { mocks.ListPodsResult{}, nil, ) - cmd := NewRootCmd(podClient, nil) + cmd := NewRootCmd(podClient, podClient) output, err := test.ExecuteCommand(cmd, "pod") assert.NotEmpty(t, output) assert.Error(t, err) @@ -40,7 +40,7 @@ func Test_Success(t *testing.T) { mocks.ListPodsResult{}, nil, ) - cmd := NewRootCmd(podClient, nil) + cmd := NewRootCmd(podClient, podClient) _, err := test.ExecuteCommand(cmd, "pod", "pod-1") assert.NoError(t, err) } @@ -50,7 +50,7 @@ func Test_DuplicateError(t *testing.T) { mocks.ListPodsResult{}, fmt.Errorf("error"), ) - cmd := NewRootCmd(podClient, nil) + cmd := NewRootCmd(podClient, podClient) _, err := test.ExecuteCommand(cmd, "pod", "pod-1") assert.EqualError(t, err, "error") } diff --git a/pkg/cmd/root.go b/pkg/cmd/root.go index 0c44f8e..46d8333 100644 --- a/pkg/cmd/root.go +++ b/pkg/cmd/root.go @@ -25,8 +25,8 @@ import ( ) func NewRootCmd( - podClient core.Duplik8sClient, - deployClient core.Duplik8sClient, + duplicator core.Duplicator, + client core.Client, ) *cobra.Command { rootCmd := &cobra.Command{ Use: "kubectl-duplicate", @@ -51,9 +51,9 @@ func NewRootCmd( configFlags.AddFlags(rootCmd.PersistentFlags()) // add subcommands - rootCmd.AddCommand(NewPodCmd(podClient)) - rootCmd.AddCommand(NewDeployCmd(deployClient)) - rootCmd.AddCommand(NewStatefulSetCmd(deployClient)) + rootCmd.AddCommand(NewPodCmd(duplicator, client)) + rootCmd.AddCommand(NewDeployCmd(duplicator, client)) + rootCmd.AddCommand(NewStatefulSetCmd(duplicator, client)) return rootCmd } diff --git a/pkg/cmd/shared.go b/pkg/cmd/shared.go index 0231c20..7538456 100644 --- a/pkg/cmd/shared.go +++ b/pkg/cmd/shared.go @@ -17,24 +17,36 @@ package cmd import ( + "context" + "fmt" "github.com/spf13/cobra" + "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/cmd/flags" "github.com/telemaco019/duplik8s/pkg/core" "github.com/telemaco019/duplik8s/pkg/utils" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "k8s.io/apimachinery/pkg/runtime/schema" ) -type duplik8sClientFactory func(opts utils.KubeOptions) (core.Duplik8sClient, error) +type duplicatorFactory func(opts utils.KubeOptions) (core.Duplicator, error) -func newDuplicateCmd(factory duplik8sClientFactory, selectMessage string) func(cmd *cobra.Command, args []string) error { +func newDuplicateCmd(newDuplicator duplicatorFactory, client core.Client, gvr schema.GroupVersionResource) func(cmd *cobra.Command, args []string) error { return func(cmd *cobra.Command, args []string) error { opts, err := NewKubeOptions(cmd, args) if err != nil { return err } - client, err := factory(opts) + duplicator, err := newDuplicator(opts) if err != nil { return err } + if client == nil { + client, err = clients.NewDuplik8sClient(opts) + if err != nil { + return err + } + } cmdOverride, err := cmd.Flags().GetStringSlice(flags.COMMAND_OVERRIDE) if err != nil { return err @@ -51,17 +63,37 @@ func newDuplicateCmd(factory duplik8sClientFactory, selectMessage string) func(c Args: argsOverride, } + // If available, duplicate the resource provided as argument var obj core.DuplicableObject - if len(args) == 0 { - obj, err = utils.SelectItem(client, opts.Namespace, selectMessage) - if err != nil { - return err + if len(args) > 0 { + obj = core.DuplicableObject{ + Name: args[0], + Namespace: opts.Namespace, } - } else { - obj = core.NewPod(args[0], opts.Namespace) + return duplicator.Duplicate(obj, options) } - return client.Duplicate(obj, options) + // Otherwise, list available resources + objs, err := client.ListDuplicable( + context.Background(), + gvr, + opts.Namespace, + ) + if err != nil { + return err + } + if len(objs) == 0 { + return fmt.Errorf("no %s available in namespace %q", gvr.Resource, opts.Namespace) + } + caser := cases.Title(language.English) + obj, err = utils.SelectItem( + objs, + fmt.Sprintf("%s [%s]", caser.String(gvr.Resource), opts.Namespace), + ) + if err != nil { + return err + } + return duplicator.Duplicate(obj, options) } } diff --git a/pkg/cmd/statefulset.go b/pkg/cmd/statefulset.go index 65cde3b..59b3c85 100644 --- a/pkg/cmd/statefulset.go +++ b/pkg/cmd/statefulset.go @@ -18,24 +18,29 @@ package cmd import ( "github.com/spf13/cobra" - "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" + "github.com/telemaco019/duplik8s/pkg/duplicators" "github.com/telemaco019/duplik8s/pkg/utils" + "k8s.io/apimachinery/pkg/runtime/schema" ) -func NewStatefulSetCmd(client core.Duplik8sClient) *cobra.Command { - factory := func(opts utils.KubeOptions) (core.Duplik8sClient, error) { - if client == nil { - return clients.NewStatefulSetClient(opts) +func NewStatefulSetCmd(duplicator core.Duplicator, client core.Client) *cobra.Command { + factory := func(opts utils.KubeOptions) (core.Duplicator, error) { + if duplicator == nil { + return duplicators.NewStatefulSetClient(opts) } - return client, nil + return duplicator, nil } deployCmd := &cobra.Command{ Use: "statefulset", Short: "Duplicate a StatefulSet.", Args: cobra.MaximumNArgs(1), RunE: func(cmd *cobra.Command, args []string) error { - run := newDuplicateCmd(factory, "Select a StatefulSet") + run := newDuplicateCmd(factory, client, schema.GroupVersionResource{ + Group: "apps", + Version: "v1", + Resource: "statefulsets", + }) return run(cmd, args) }, } diff --git a/pkg/core/client.go b/pkg/core/client.go deleted file mode 100644 index 619347a..0000000 --- a/pkg/core/client.go +++ /dev/null @@ -1,22 +0,0 @@ -/* - * 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 core - -type Duplik8sClient interface { - ListDuplicable(namespace string) ([]DuplicableObject, error) - Duplicate(obj DuplicableObject, opts PodOverrideOptions) error -} diff --git a/pkg/core/types.go b/pkg/core/types.go index 6b29913..e4b3f3c 100644 --- a/pkg/core/types.go +++ b/pkg/core/types.go @@ -16,15 +16,20 @@ package core -import v1 "k8s.io/api/core/v1" +import ( + "context" + v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/apimachinery/pkg/runtime/schema" +) -type DuplicableObjectKind string +type Duplicator interface { + Duplicate(obj DuplicableObject, opts PodOverrideOptions) error +} -const ( - KindDeployment DuplicableObjectKind = "Deployment" - KindStatefulSet DuplicableObjectKind = "StatefulSet" - KindPod DuplicableObjectKind = "Pod" -) +type Client interface { + ListDuplicable(ctx context.Context, resource schema.GroupVersionResource, namespace string) ([]DuplicableObject, error) +} type PodOverrideOptions struct { // Command overrides the default command of each container. @@ -39,32 +44,20 @@ type PodOverrideOptions struct { StartupProbe *v1.Probe } +type DuplicatedObject struct { + Name string + Namespace string + ObjectKind schema.ObjectKind +} + type DuplicableObject struct { Name string Namespace string - Kind DuplicableObjectKind -} - -func NewPod(name, namespace string) DuplicableObject { - return DuplicableObject{ - Kind: KindPod, - Name: name, - Namespace: namespace, - } -} - -func NewDeployment(name, namespace string) DuplicableObject { - return DuplicableObject{ - Kind: KindDeployment, - Name: name, - Namespace: namespace, - } } -func NewStatefulSet(name, namespace string) DuplicableObject { +func NewDuplicable(u unstructured.Unstructured) DuplicableObject { return DuplicableObject{ - Kind: KindStatefulSet, - Name: name, - Namespace: namespace, + Name: u.GetName(), + Namespace: u.GetNamespace(), } } diff --git a/pkg/clients/deployment_client.go b/pkg/duplicators/deployment_duplicator.go similarity index 84% rename from pkg/clients/deployment_client.go rename to pkg/duplicators/deployment_duplicator.go index cb4eb4f..2467d69 100644 --- a/pkg/clients/deployment_client.go +++ b/pkg/duplicators/deployment_duplicator.go @@ -14,11 +14,12 @@ * limitations under the License. */ -package clients +package duplicators import ( "context" "fmt" + "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" "github.com/telemaco019/duplik8s/pkg/utils" appsv1 "k8s.io/api/apps/v1" @@ -42,18 +43,6 @@ func NewDeploymentClient(opts utils.KubeOptions) (*DeploymentClient, error) { }, nil } -func (c *DeploymentClient) ListDuplicable(namespace string) ([]core.DuplicableObject, error) { - deployments, err := c.clientset.AppsV1().Deployments(namespace).List(c.ctx, core.NewDuplicableListOptions()) - if err != nil { - return nil, err - } - var objs []core.DuplicableObject - for _, d := range deployments.Items { - objs = append(objs, core.NewDeployment(d.Name, d.Namespace)) - } - return objs, nil -} - func (c *DeploymentClient) Duplicate(obj core.DuplicableObject, opts core.PodOverrideOptions) error { fmt.Printf("duplicating deployment %s\n", obj.Name) @@ -84,7 +73,7 @@ func (c *DeploymentClient) Duplicate(obj core.DuplicableObject, opts core.PodOve } // override the spec of the deployment's pod - configurator := NewConfigurator(c.clientset, opts) + configurator := clients.NewConfigurator(c.clientset, opts) err = configurator.OverrideSpec(c.ctx, obj.Namespace, &newDeploy.Spec.Template.Spec) if err != nil { return err diff --git a/pkg/clients/pod_client.go b/pkg/duplicators/pod_duplicator.go similarity index 84% rename from pkg/clients/pod_client.go rename to pkg/duplicators/pod_duplicator.go index 2ea0db8..fc14a0e 100644 --- a/pkg/clients/pod_client.go +++ b/pkg/duplicators/pod_duplicator.go @@ -14,11 +14,12 @@ * limitations under the License. */ -package clients +package duplicators import ( "context" "fmt" + "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" "github.com/telemaco019/duplik8s/pkg/utils" v1 "k8s.io/api/core/v1" @@ -42,18 +43,6 @@ func NewPodClient(opts utils.KubeOptions) (*PodClient, error) { }, nil } -func (c *PodClient) ListDuplicable(namespace string) ([]core.DuplicableObject, error) { - pods, err := c.clientset.CoreV1().Pods(namespace).List(c.ctx, core.NewDuplicableListOptions()) - if err != nil { - return nil, err - } - var objs []core.DuplicableObject - for _, pod := range pods.Items { - objs = append(objs, core.NewPod(pod.Name, pod.Namespace)) - } - return objs, nil -} - func (c *PodClient) Duplicate(obj core.DuplicableObject, opts core.PodOverrideOptions) error { fmt.Printf("duplicating pod %s\n", obj.Name) @@ -84,7 +73,7 @@ func (c *PodClient) Duplicate(obj core.DuplicableObject, opts core.PodOverrideOp } // override the pod spec - configurator := NewConfigurator(c.clientset, opts) + configurator := clients.NewConfigurator(c.clientset, opts) err = configurator.OverrideSpec(c.ctx, obj.Namespace, &newPod.Spec) if err != nil { return err diff --git a/pkg/clients/statefulset_client.go b/pkg/duplicators/statefulset_duplicator.go similarity index 84% rename from pkg/clients/statefulset_client.go rename to pkg/duplicators/statefulset_duplicator.go index 6a7f2c6..f239bdf 100644 --- a/pkg/clients/statefulset_client.go +++ b/pkg/duplicators/statefulset_duplicator.go @@ -14,11 +14,12 @@ * limitations under the License. */ -package clients +package duplicators import ( "context" "fmt" + "github.com/telemaco019/duplik8s/pkg/clients" "github.com/telemaco019/duplik8s/pkg/core" "github.com/telemaco019/duplik8s/pkg/utils" appsv1 "k8s.io/api/apps/v1" @@ -42,18 +43,6 @@ func NewStatefulSetClient(opts utils.KubeOptions) (*StatefulSetClient, error) { }, nil } -func (c *StatefulSetClient) ListDuplicable(namespace string) ([]core.DuplicableObject, error) { - statefulSets, err := c.clientset.AppsV1().StatefulSets(namespace).List(c.ctx, core.NewDuplicableListOptions()) - if err != nil { - return nil, err - } - var objs []core.DuplicableObject - for _, s := range statefulSets.Items { - objs = append(objs, core.NewStatefulSet(s.Name, s.Namespace)) - } - return objs, nil -} - func (c *StatefulSetClient) Duplicate(obj core.DuplicableObject, opts core.PodOverrideOptions) error { fmt.Printf("duplicating statefulset %s\n", obj.Name) @@ -84,7 +73,7 @@ func (c *StatefulSetClient) Duplicate(obj core.DuplicableObject, opts core.PodOv } // override the spec of the statefulset's pod - configurator := NewConfigurator(c.clientset, opts) + configurator := clients.NewConfigurator(c.clientset, opts) err = configurator.OverrideSpec(c.ctx, obj.Namespace, &newStatefulSet.Spec.Template.Spec) if err != nil { return err diff --git a/pkg/test/mocks/pod_client.go b/pkg/test/mocks/pod_client.go index 58535a8..761f95d 100644 --- a/pkg/test/mocks/pod_client.go +++ b/pkg/test/mocks/pod_client.go @@ -17,7 +17,9 @@ package mocks import ( + "context" "github.com/telemaco019/duplik8s/pkg/core" + "k8s.io/apimachinery/pkg/runtime/schema" ) type ListPodsResult struct { @@ -28,7 +30,10 @@ type ListPodsResult struct { func NewListPodResults(pods []string, namespace string, err error) ListPodsResult { var objs = make([]core.DuplicableObject, 0) for _, pod := range pods { - objs = append(objs, core.NewPod(pod, namespace)) + objs = append(objs, core.DuplicableObject{ + Name: pod, + Namespace: namespace, + }) } return ListPodsResult{ Objs: objs, @@ -51,7 +56,7 @@ func NewPodClient( } } -func (c *PodClient) ListDuplicable(_ string) ([]core.DuplicableObject, error) { +func (c *PodClient) ListDuplicable(ctx context.Context, resource schema.GroupVersionResource, namespace string) ([]core.DuplicableObject, error) { return c.ListPodsResult.Objs, c.ListPodsResult.Err } diff --git a/pkg/utils/kubernetes.go b/pkg/utils/kubernetes.go index 563a080..f4c0516 100644 --- a/pkg/utils/kubernetes.go +++ b/pkg/utils/kubernetes.go @@ -17,6 +17,7 @@ package utils import ( + "k8s.io/client-go/dynamic" "k8s.io/client-go/kubernetes" "k8s.io/client-go/tools/clientcmd" clientcmdapi "k8s.io/client-go/tools/clientcmd/api" @@ -43,6 +44,26 @@ func NewClientset(kubeconfig, context string) (*kubernetes.Clientset, error) { return clientSet, nil } +func NewDynamicClient(kubeconfig, context string) (*dynamic.DynamicClient, error) { + config, err := clientcmd.NewNonInteractiveDeferredLoadingClientConfig( + &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeconfig}, + &clientcmd.ConfigOverrides{ + ClusterInfo: clientcmdapi.Cluster{Server: ""}, + CurrentContext: context, + }, + ).ClientConfig() + if err != nil { + return nil, err + } + + clientSet, err := dynamic.NewForConfig(config) + if err != nil { + return nil, err + } + + return clientSet, nil +} + type KubeOptions struct { Kubeconfig string Kubecontext string diff --git a/pkg/utils/select.go b/pkg/utils/select.go index ae677d8..869ccbe 100644 --- a/pkg/utils/select.go +++ b/pkg/utils/select.go @@ -17,26 +17,18 @@ package utils import ( - "fmt" "github.com/charmbracelet/huh" "github.com/telemaco019/duplik8s/pkg/core" ) -func SelectItem(client core.Duplik8sClient, namespace, selectMessage string) (core.DuplicableObject, error) { - var selected = core.DuplicableObject{} - objs, err := client.ListDuplicable(namespace) - if err != nil { - return selected, err - } - if len(objs) == 0 { - return selected, fmt.Errorf("no Pods found in namespace %q", namespace) - } - options := make([]huh.Option[core.DuplicableObject], len(objs)) - for i, o := range objs { +func SelectItem(items []core.DuplicableObject, selectMessage string) (core.DuplicableObject, error) { + var selected core.DuplicableObject + options := make([]huh.Option[core.DuplicableObject], len(items)) + for i, o := range items { options[i] = huh.NewOption(o.Name, o) } - err = huh.NewSelect[core.DuplicableObject](). - Title(fmt.Sprintf("%s [%s]", selectMessage, namespace)). + err := huh.NewSelect[core.DuplicableObject](). + Title(selectMessage). Options(options...). Value(&selected). Run()