Skip to content

Commit

Permalink
Feat/select pod (#1)
Browse files Browse the repository at this point in the history
* allow pod selection if no pod name is provided

* refactor to make it testable

* test

* update changelog

* go mod tidy

* go mod tidy

* fix

* fix
  • Loading branch information
Telemaco019 authored Jun 16, 2024
1 parent 0e5ea9a commit ffe2a7c
Show file tree
Hide file tree
Showing 11 changed files with 362 additions and 92 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Changelog

## Unreleased

### New features

* Show prompt for selecting a Pod in the current namespace when no Pod name is provided as argument.

### Chores

* Refactoring to make code testable.

## v0.1.0

Initial release.
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ check: fmt vet test license-check ## Check the code

.PHONY: build
build: fmt vet ## Build binary.
go build -o bin/duplik8s main.go
go build -o bin/duplik8s kubectl-duplicate/kubectl-duplicate.go

##@ Build Dependencies

Expand Down
43 changes: 34 additions & 9 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,34 @@
module github.com/telemaco019/duplik8s

go 1.22.3
go 1.22.4

require (
github.com/spf13/cobra v1.8.0
k8s.io/api v0.30.0
k8s.io/apimachinery v0.30.0
k8s.io/cli-runtime v0.30.0
k8s.io/client-go v0.30.0
github.com/charmbracelet/huh v0.4.2
github.com/spf13/cobra v1.8.1
github.com/stretchr/testify v1.9.0
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/cli-runtime v0.30.2
k8s.io/client-go v0.30.2
)

require (
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
github.com/atotto/clipboard v0.1.4 // indirect
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/catppuccin/go v0.2.0 // indirect
github.com/charmbracelet/bubbles v0.18.0 // indirect
github.com/charmbracelet/bubbletea v0.26.3 // indirect
github.com/charmbracelet/lipgloss v0.11.0 // indirect
github.com/charmbracelet/x/ansi v0.1.1 // indirect
github.com/charmbracelet/x/exp/strings v0.0.0-20240524151031-ff83003bf67a // indirect
github.com/charmbracelet/x/input v0.1.1 // indirect
github.com/charmbracelet/x/term v0.1.1 // indirect
github.com/charmbracelet/x/windows v0.1.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.4.1 // indirect
Expand All @@ -33,23 +48,33 @@ require (
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-localereader v0.0.1 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/moby/term v0.0.0-20221205130635-1aeaba878587 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.23.0 // indirect
golang.org/x/oauth2 v0.10.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.18.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.20.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
golang.org/x/text v0.15.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.33.0 // indirect
Expand Down
98 changes: 76 additions & 22 deletions go.sum

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion kubectl-duplicate/kubectl-duplicate.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,13 @@ package main

import (
"github.com/telemaco019/duplik8s/pkg/cmd"
"os"
)

func main() {
cmd.Execute()
rootCmd := cmd.NewRootCmd(nil)
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
103 changes: 70 additions & 33 deletions pkg/cmd/pod.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
package cmd

import (
"fmt"
"github.com/charmbracelet/huh"
"github.com/spf13/cobra"
"github.com/telemaco019/duplik8s/pkg/cmd/flags"
"github.com/telemaco019/duplik8s/pkg/pods"
Expand All @@ -43,42 +45,54 @@ func NewKubeOptions(cmd *cobra.Command, args []string) (utils.KubeOptions, error
return o, nil
}

// podCmd represents the pod command
var podCmd = &cobra.Command{
Use: "pod",
Short: "Duplicate a Pod.",
Args: cobra.ExactArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts, err := NewKubeOptions(cmd, args)
if err != nil {
return err
}
client, err := pods.NewClient(opts)
if err != nil {
return err
}
cmdOverride, err := cmd.Flags().GetStringSlice(flags.COMMAND_OVERRIDE)
if err != nil {
return err
}
argsOverride, err := cmd.Flags().GetStringSlice(flags.ARGS_OVERRIDE)
if err != nil {
return err
}
func NewPodCmd(podClient pods.PodClient) *cobra.Command {
podCmd := &cobra.Command{
Use: "pod",
Short: "Duplicate a Pod.",
Args: cobra.MaximumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
opts, err := NewKubeOptions(cmd, args)
if err != nil {
return err
}
if podClient == nil {
podClient, err = pods.NewClient(opts)
if err != nil {
return err
}
}
if err != nil {
return err
}
cmdOverride, err := cmd.Flags().GetStringSlice(flags.COMMAND_OVERRIDE)
if err != nil {
return err
}
argsOverride, err := cmd.Flags().GetStringSlice(flags.ARGS_OVERRIDE)
if err != nil {
return err
}

// Avoid printing usage information on errors
cmd.SilenceUsage = true
options := pods.PodOverrideOptions{
Command: cmdOverride,
Args: argsOverride,
}
return client.DuplicatePod(args[0], opts.Namespace, options)
},
}
// Avoid printing usage information on errors
cmd.SilenceUsage = true
options := pods.PodOverrideOptions{
Command: cmdOverride,
Args: argsOverride,
}

func init() {
rootCmd.AddCommand(podCmd)
var podName string
if len(args) == 0 {
podName, err = selectPod(podClient, opts.Namespace)
if err != nil {
return err
}
} else {
podName = args[0]
}

return podClient.DuplicatePod(podName, opts.Namespace, options)
},
}
podCmd.Flags().StringSlice(
flags.COMMAND_OVERRIDE,
[]string{"/bin/sh"},
Expand All @@ -89,4 +103,27 @@ func init() {
[]string{"-c", "trap 'exit 0' INT TERM KILL; while true; do sleep 1; done"},
"Override the command of each container in the Pod.",
)

return podCmd
}

func selectPod(client pods.PodClient, namespace string) (string, error) {
availablePods, err := client.ListPods(namespace)
if err != nil {
return "", err
}
if len(availablePods) == 0 {
return "", fmt.Errorf("no Pods found in namespace %q", namespace)
}
options := make([]huh.Option[string], len(availablePods))
for i, p := range availablePods {
options[i] = huh.NewOption(p, p)
}
var selectedPod string
err = huh.NewSelect[string]().
Title(fmt.Sprintf("Select a Pod [%s]", namespace)).
Options(options...).
Value(&selectedPod).
Run()
return selectedPod, err
}
35 changes: 35 additions & 0 deletions pkg/cmd/pod_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* Copyright 2024 Michele Zanotti <[email protected]>
*
* 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 (
"github.com/stretchr/testify/assert"
"github.com/telemaco019/duplik8s/pkg/test"
"github.com/telemaco019/duplik8s/pkg/test/mocks"
"testing"
)

func Test_NoPodsAvailable(t *testing.T) {
podClient := mocks.NewPodClient(
mocks.ListPodsResult{},
nil,
)
cmd := NewRootCmd(podClient)
output, err := test.ExecuteCommand(cmd, "pod")
assert.NotEmpty(t, output)
assert.Error(t, err)
}
41 changes: 19 additions & 22 deletions pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,42 +18,39 @@ package cmd

import (
"github.com/spf13/cobra"
"github.com/telemaco019/duplik8s/pkg/pods"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/client-go/util/homedir"
"os"
"path/filepath"
)

// rootCmd represents the base command when called without any subcommands
var rootCmd = &cobra.Command{
Use: "kubectl-duplicate",
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "kubectl duplicate",
},
Short: "duplik8s is a kubectl plugin for duplicating Kubernetes resources.",
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
}

// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
err := rootCmd.Execute()
if err != nil {
os.Exit(1)
func NewRootCmd(
c pods.PodClient,
) *cobra.Command {
rootCmd := &cobra.Command{
Use: "kubectl-duplicate",
Annotations: map[string]string{
cobra.CommandDisplayNameAnnotation: "kubectl duplicate",
},
Short: "duplik8s is a kubectl plugin for duplicating Kubernetes resources.",
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
}
}

func init() {
// Setup kubeconfig flags
defaultNamespace := "default"
defaultKubeconfig := ""
if home := homedir.HomeDir(); home != "" {
defaultKubeconfig = filepath.Join(home, ".kube", "config")
}

configFlags := genericclioptions.NewConfigFlags(true)
configFlags.KubeConfig = &defaultKubeconfig
configFlags.Namespace = &defaultNamespace
configFlags.AddFlags(rootCmd.PersistentFlags())

// add subcommands
rootCmd.AddCommand(NewPodCmd(c))

return rootCmd
}
25 changes: 21 additions & 4 deletions pkg/pods/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,22 @@ const (
LABEL_DUPLICATED = "telemaco019.github.com/duplik8ted"
)

type PodClient struct {
type PodClient interface {
ListPods(namespace string) ([]string, error)
DuplicatePod(podName string, namespace string, opts PodOverrideOptions) error
}

type podClient struct {
clientset *kubernetes.Clientset
ctx context.Context
}

func NewClient(opts utils.KubeOptions) (*PodClient, error) {
func NewClient(opts utils.KubeOptions) (PodClient, error) {
clientset, err := utils.NewClientset(opts.Kubeconfig, opts.Kubecontext)
if err != nil {
return nil, err
}
return &PodClient{
return &podClient{
clientset: clientset,
ctx: context.Background(),
}, nil
Expand Down Expand Up @@ -76,7 +81,19 @@ func (o PodOverrideOptions) Apply(pod *v1.Pod) {
pod.Spec.RestartPolicy = v1.RestartPolicyNever
}

func (c PodClient) DuplicatePod(podName string, namespace string, opts PodOverrideOptions) error {
func (c *podClient) ListPods(namespace string) ([]string, error) {
pods, err := c.clientset.CoreV1().Pods(namespace).List(c.ctx, metav1.ListOptions{})
if err != nil {
return nil, err
}
var podNames []string
for _, pod := range pods.Items {
podNames = append(podNames, pod.Name)
}
return podNames, nil
}

func (c *podClient) DuplicatePod(podName string, namespace string, opts PodOverrideOptions) error {
fmt.Printf("duplicating Pod %s\n", podName)

// fetch the pod
Expand Down
Loading

0 comments on commit ffe2a7c

Please sign in to comment.