diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 93f2076..be361d9 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -11,111 +11,111 @@ jobs: build-darwin-arm: runs-on: macos-latest steps: - - name: Set up Go - uses: actions/setup-go@v4.1.0 - with: - go-version: "1.22.0" - - - name: Check out Code - uses: actions/checkout@v4.0.0 - - - name: Build - run: | - cd src - go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc . - mv kubectl-browse-pvc .. - - - name: Fix permissions - run: chmod +x ./kubectl-browse-pvc - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: kubectl-browse-pvc-darwin-arm - path: | - ./kubectl-browse-pvc - LICENSE + - name: Set up Go + uses: actions/setup-go@v4.1.0 + with: + go-version: "1.22.0" + + - name: Check out Code + uses: actions/checkout@v4.0.0 + + - name: Build + run: | + cd src + go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc cmd/browse-pvc/main.go + mv kubectl-browse-pvc .. + + - name: Fix permissions + run: chmod +x ./kubectl-browse-pvc + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: kubectl-browse-pvc-darwin-arm + path: | + ./kubectl-browse-pvc + LICENSE build-darwin-x86_64: runs-on: macos-13 steps: - - name: Set up Go - uses: actions/setup-go@v4.1.0 - with: - go-version: "1.22.0" - - - name: Check out Code - uses: actions/checkout@v4.0.0 - - - name: Build - run: | - cd src - go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc . - mv kubectl-browse-pvc .. - - - name: Fix permissions - run: chmod +x ./kubectl-browse-pvc - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: kubectl-browse-pvc-darwin-x86_64 - path: | - ./kubectl-browse-pvc - LICENSE + - name: Set up Go + uses: actions/setup-go@v4.1.0 + with: + go-version: "1.22.0" + + - name: Check out Code + uses: actions/checkout@v4.0.0 + + - name: Build + run: | + cd src + go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc cmd/browse-pvc/main.go + mv kubectl-browse-pvc .. + + - name: Fix permissions + run: chmod +x ./kubectl-browse-pvc + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: kubectl-browse-pvc-darwin-x86_64 + path: | + ./kubectl-browse-pvc + LICENSE build-linux-x86_64: runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v4.1.0 - with: - go-version: "1.22.0" - - - name: Check out Code - uses: actions/checkout@v4.0.0 - - - name: Build - run: | - cd src - go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc . - mv kubectl-browse-pvc .. - - - name: Fix permissions - run: chmod +x ./kubectl-browse-pvc - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: kubectl-browse-pvc-linux-x86_64 - path: | - ./kubectl-browse-pvc - LICENSE + - name: Set up Go + uses: actions/setup-go@v4.1.0 + with: + go-version: "1.22.0" + + - name: Check out Code + uses: actions/checkout@v4.0.0 + + - name: Build + run: | + cd src + go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc cmd/browse-pvc/main.go + mv kubectl-browse-pvc .. + + - name: Fix permissions + run: chmod +x ./kubectl-browse-pvc + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: kubectl-browse-pvc-linux-x86_64 + path: | + ./kubectl-browse-pvc + LICENSE build-linux-arm: runs-on: ubuntu-latest steps: - - name: Set up Go - uses: actions/setup-go@v4.1.0 - with: - go-version: "1.22.0" - - - name: Check out Code - uses: actions/checkout@v4.0.0 - - - name: Build - run: | - cd src - GOARCH=arm64 go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc . - mv kubectl-browse-pvc .. - - - name: Fix permissions - run: chmod +x ./kubectl-browse-pvc - - - name: Upload Artifact - uses: actions/upload-artifact@v4 - with: - name: kubectl-browse-pvc-linux-arm - path: | - ./kubectl-browse-pvc - LICENSE + - name: Set up Go + uses: actions/setup-go@v4.1.0 + with: + go-version: "1.22.0" + + - name: Check out Code + uses: actions/checkout@v4.0.0 + + - name: Build + run: | + cd src + GOARCH=arm64 go build -v -ldflags "-X main.Version=${{ inputs.version }}" -o kubectl-browse-pvc cmd/browse-pvc/main.go + mv kubectl-browse-pvc .. + + - name: Fix permissions + run: chmod +x ./kubectl-browse-pvc + + - name: Upload Artifact + uses: actions/upload-artifact@v4 + with: + name: kubectl-browse-pvc-linux-arm + path: | + ./kubectl-browse-pvc + LICENSE diff --git a/README.md b/README.md index 766dd65..be0fc65 100644 --- a/README.md +++ b/README.md @@ -1,28 +1,61 @@ # kubectl-browse-pvc + Kubectl plugin to browse a Kubernetes PVC from the command line -I constantly found myself spinning up dummy pods to exec into them so I could browse a PVC, this takes a few steps out of creating dummy pods to check out the contents of a PVC. +I constantly found myself spinning up dummy pods to exec into them so I could browse a PVC, this takes a few steps out of creating dummy pods to check out the contents of a PVC. + +## Installation Install via krew -``` + +```sh kubectl krew install browse-pvc ``` -Usage +## Usage -``` +```sh kubectl browse-pvc ``` -On a PVC. The tool spins up a pod that mounts the PVC and then execs into it allowing you to modify the contents of the PVC. The Job finishes and cleans up the pod when you disconnect. +On a PVC. The tool spins up a pod that mounts the PVC and then execs into it allowing you to modify the contents of the PVC. The Job finishes and cleans up the pod when you disconnect. Commands can be described to run a command instead of popping a shell + +```sh +kubectl browse-pvc -- ``` -kubectl browse-pvc -- + +A User ID can be described to set the user the container runs as + +```sh +kubectl browse-pvc -u 1000 ``` +### Configuring auto-completion -A User ID can be described to set the user the container runs as +```sh +cat > kubectl_browse-pvc < + +## Dev + +### Test + +```sh +go test -v github.com/clbx/kubectl-browse-pvc/ ``` + +Example: `go test -v github.com/clbx/kubectl-browse-pvc/internal/utils` + +### Build diff --git a/src/main.go b/cmd/browse-pvc/main.go similarity index 88% rename from src/main.go rename to cmd/browse-pvc/main.go index 4bafeb9..a5b6093 100644 --- a/src/main.go +++ b/cmd/browse-pvc/main.go @@ -10,6 +10,8 @@ import ( "time" "github.com/briandowns/spinner" + "github.com/clbx/kubectl-browse-pvc/internal/monitor" + "github.com/clbx/kubectl-browse-pvc/internal/utils" "github.com/spf13/cobra" "golang.org/x/term" @@ -29,6 +31,8 @@ var image string var Version string var containerUser int +var validCommands = []string{"image", "container-user"} + func main() { var kubeConfigFlags = genericclioptions.NewConfigFlags(true) @@ -38,11 +42,12 @@ func main() { } var rootCmd = &cobra.Command{ - Use: "kubectl-browse-pvc [-- COMMAND [ARGS...]]", - Short: "Kubernetes PVC Browser", - Long: `Kubernetes PVC Browser`, - Version: Version, - Args: cobra.MinimumNArgs(1), + Use: "kubectl-browse-pvc [-- COMMAND [ARGS...]]", + Short: "Kubernetes PVC Browser", + Long: `Kubernetes PVC Browser`, + Version: Version, + Args: cobra.MinimumNArgs(1), + ValidArgs: validCommands, Run: func(cmd *cobra.Command, args []string) { pvcName := args[0] commandArgs := args[1:] @@ -97,7 +102,7 @@ func browseCommand(kubeConfigFlags *genericclioptions.ConfigFlags, pvcName strin log.Fatalf("Failed to get pods: %v", err) } - attachedPod := findPodByPVC(*nsPods, *targetPvc) + attachedPod := utils.FindPodByPVC(*nsPods, *targetPvc) manyAccessMode := false for _, mode := range targetPvc.Spec.AccessModes { @@ -114,18 +119,18 @@ func browseCommand(kubeConfigFlags *genericclioptions.ConfigFlags, pvcName strin node = attachedPod.Spec.NodeName } - options := &PodOptions{ - image: image, - namespace: namespace, - pvc: *targetPvc, - cmd: []string{"/bin/sh", "-c", "--"}, - args: commandArgs, - node: node, - user: int64(containerUser), + options := &utils.PodOptions{ + Image: image, + Namespace: namespace, + Pvc: *targetPvc, + Cmd: []string{"/bin/sh", "-c", "--"}, + Args: commandArgs, + Node: node, + User: int64(containerUser), } // Build the Job - pvcbGetJob := buildPvcbGetJob(*options) + pvcbGetJob := utils.BuildPvcbGetJob(*options) // Create Job pvcbGetJob, err = clientset.BatchV1().Jobs(namespace).Create(context.TODO(), pvcbGetJob, metav1.CreateOptions{}) @@ -203,7 +208,7 @@ func browseCommand(kubeConfigFlags *genericclioptions.ConfigFlags, pvcName strin Post(). Resource("pods"). Name(pod.Name). - Namespace(options.namespace). + Namespace(options.Namespace). SubResource("exec"). VersionedParams(&corev1.PodExecOptions{ Command: []string{"sh", "-c", "cd /mnt && (ash || bash || sh)"}, @@ -224,15 +229,15 @@ func browseCommand(kubeConfigFlags *genericclioptions.ConfigFlags, pvcName strin } defer term.Restore(0, oldState) - terminalSizeQueue := &sizeQueue{ - resizeChan: make(chan remotecommand.TerminalSize, 1), - stopResizing: make(chan struct{}), + terminalSizeQueue := &monitor.SizeQueue{ + ResizeChan: make(chan remotecommand.TerminalSize, 1), + StopResizing: make(chan struct{}), } // prime with initial term size width, height, err := term.GetSize(int(os.Stdout.Fd())) if err == nil { - terminalSizeQueue.resizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)} + terminalSizeQueue.ResizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)} } go terminalSizeQueue.MonitorSize() diff --git a/src/go.mod b/go.mod similarity index 98% rename from src/go.mod rename to go.mod index 521f4f6..89fdbfd 100644 --- a/src/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/golang/protobuf v1.5.4 // indirect github.com/google/btree v1.0.1 // indirect github.com/google/gnostic-models v0.6.8 // indirect + github.com/google/go-cmp v0.6.0 // indirect github.com/google/gofuzz v1.2.0 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.3.0 // indirect diff --git a/src/go.sum b/go.sum similarity index 100% rename from src/go.sum rename to go.sum diff --git a/src/monitor_size.go b/internal/monitor/monitor_size.go similarity index 54% rename from src/monitor_size.go rename to internal/monitor/monitor_size.go index 5c673fc..ecaa5c8 100644 --- a/src/monitor_size.go +++ b/internal/monitor/monitor_size.go @@ -1,7 +1,7 @@ //go:build linux || darwin // +build linux darwin -package main +package monitor import ( "os" @@ -12,24 +12,24 @@ import ( "k8s.io/client-go/tools/remotecommand" ) -type sizeQueue struct { - resizeChan chan remotecommand.TerminalSize - stopResizing chan struct{} +type SizeQueue struct { + ResizeChan chan remotecommand.TerminalSize + StopResizing chan struct{} } -func (s *sizeQueue) Next() *remotecommand.TerminalSize { - size, ok := <-s.resizeChan +func (s *SizeQueue) Next() *remotecommand.TerminalSize { + size, ok := <-s.ResizeChan if !ok { return nil } return &size } -func (s *sizeQueue) Stop() { - close(s.stopResizing) +func (s *SizeQueue) Stop() { + close(s.StopResizing) } -func (s *sizeQueue) MonitorSize() { +func (s *SizeQueue) MonitorSize() { sigCh := make(chan os.Signal, 1) signal.Notify(sigCh, syscall.SIGWINCH) @@ -39,12 +39,12 @@ func (s *sizeQueue) MonitorSize() { width, height, err := term.GetSize(int(os.Stdout.Fd())) if err == nil { select { - case s.resizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)}: + case s.ResizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)}: default: } } - case <-s.stopResizing: - close(s.resizeChan) + case <-s.StopResizing: + close(s.ResizeChan) return } } diff --git a/src/monitor_size_windows.go b/internal/monitor/monitor_size_windows.go similarity index 55% rename from src/monitor_size_windows.go rename to internal/monitor/monitor_size_windows.go index 2b4cfa1..84c7f39 100644 --- a/src/monitor_size_windows.go +++ b/internal/monitor/monitor_size_windows.go @@ -1,7 +1,7 @@ //go:build windows // +build windows -package main +package monitor import ( "os" @@ -10,24 +10,24 @@ import ( "k8s.io/client-go/tools/remotecommand" ) -type sizeQueue struct { - resizeChan chan remotecommand.TerminalSize - stopResizing chan struct{} +type SizeQueue struct { + ResizeChan chan remotecommand.TerminalSize + StopResizing chan struct{} } -func (s *sizeQueue) Next() *remotecommand.TerminalSize { - size, ok := <-s.resizeChan +func (s *SizeQueue) Next() *remotecommand.TerminalSize { + size, ok := <-s.ResizeChan if !ok { return nil } return &size } -func (s *sizeQueue) Stop() { - close(s.stopResizing) +func (s *SizeQueue) Stop() { + close(s.StopResizing) } -func (s *sizeQueue) MonitorSize() { +func (s *SizeQueue) MonitorSize() { sigCh := make(chan os.Signal, 1) // Need to fix this to get it working on windows //signal.Notify(sigCh, syscall.SIGWINCH) @@ -38,12 +38,12 @@ func (s *sizeQueue) MonitorSize() { width, height, err := term.GetSize(int(os.Stdout.Fd())) if err == nil { select { - case s.resizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)}: + case s.ResizeChan <- remotecommand.TerminalSize{Width: uint16(width), Height: uint16(height)}: default: } } - case <-s.stopResizing: - close(s.resizeChan) + case <-s.StopResizing: + close(s.ResizeChan) return } } diff --git a/src/util.go b/internal/utils/util.go similarity index 80% rename from src/util.go rename to internal/utils/util.go index b4ad428..8ef7a75 100644 --- a/src/util.go +++ b/internal/utils/util.go @@ -1,4 +1,4 @@ -package main +package utils import ( batchv1 "k8s.io/api/batch/v1" @@ -7,13 +7,13 @@ import ( ) type PodOptions struct { - image string - namespace string - pvc corev1.PersistentVolumeClaim - cmd []string - args []string - node string - user int64 + Image string + Namespace string + Pvc corev1.PersistentVolumeClaim + Cmd []string + Args []string + Node string + User int64 } var script = ` @@ -32,12 +32,12 @@ while :; do shell_processes=$(ls /proc | grep -E '^[0-9]+$' | while read -r pid; do cat /proc/"$pid"/comm 2>/dev/null; done | grep -E "ash|bash|sh" | wc -l) done exit 0 - fi + fi done ` // Finds if a pod that attached to a PVC -func findPodByPVC(podList corev1.PodList, pvc corev1.PersistentVolumeClaim) *corev1.Pod { +func FindPodByPVC(podList corev1.PodList, pvc corev1.PersistentVolumeClaim) *corev1.Pod { for _, pod := range podList.Items { for _, volume := range pod.Spec.Volumes { if volume.PersistentVolumeClaim != nil && volume.PersistentVolumeClaim.ClaimName == pvc.Name { @@ -50,17 +50,17 @@ func findPodByPVC(podList corev1.PodList, pvc corev1.PersistentVolumeClaim) *cor // Returns a job for the get command. // func buildPvcbGetJob(namespace string, image string, pvc corev1.PersistentVolumeClaim) *batchv1.Job { -func buildPvcbGetJob(options PodOptions) *batchv1.Job { +func BuildPvcbGetJob(options PodOptions) *batchv1.Job { //Check if provided arguments is empty. If so use the browsing script - if len(options.args) == 0 { - options.args = []string{script} + if len(options.Args) == 0 { + options.Args = []string{script} } // Setup SecurityContext var allowPrivilegeEscalation bool var runAsNonRoot bool - if options.user == 0 { + if options.User == 0 { runAsNonRoot = false allowPrivilegeEscalation = true } else { @@ -69,7 +69,7 @@ func buildPvcbGetJob(options PodOptions) *batchv1.Job { } securityContext := corev1.SecurityContext{ - RunAsUser: &options.user, + RunAsUser: &options.User, RunAsNonRoot: &runAsNonRoot, AllowPrivilegeEscalation: &allowPrivilegeEscalation, Capabilities: &corev1.Capabilities{ @@ -85,8 +85,8 @@ func buildPvcbGetJob(options PodOptions) *batchv1.Job { job := &batchv1.Job{ ObjectMeta: metav1.ObjectMeta{ - Name: "browse-" + options.pvc.Name, - Namespace: options.namespace, + Name: "browse-" + options.Pvc.Name, + Namespace: options.Namespace, }, Spec: batchv1.JobSpec{ TTLSecondsAfterFinished: TTLSecondsAfterFinished, @@ -94,18 +94,18 @@ func buildPvcbGetJob(options PodOptions) *batchv1.Job { ObjectMeta: metav1.ObjectMeta{ Name: "browse-pvc", Labels: map[string]string{ - "job-name": "browse-" + options.pvc.Name, + "job-name": "browse-" + options.Pvc.Name, }, }, Spec: corev1.PodSpec{ RestartPolicy: "Never", - NodeName: options.node, + NodeName: options.Node, Containers: []corev1.Container{ { Name: "browser", - Image: image, - Command: options.cmd, - Args: options.args, + Image: options.Image, + Command: options.Cmd, + Args: options.Args, SecurityContext: &securityContext, Env: []corev1.EnvVar{ { @@ -126,7 +126,7 @@ func buildPvcbGetJob(options PodOptions) *batchv1.Job { Name: "target-pvc", VolumeSource: corev1.VolumeSource{ PersistentVolumeClaim: &corev1.PersistentVolumeClaimVolumeSource{ - ClaimName: options.pvc.Name, + ClaimName: options.Pvc.Name, }, }, }, diff --git a/internal/utils/util_test.go b/internal/utils/util_test.go new file mode 100644 index 0000000..2968a83 --- /dev/null +++ b/internal/utils/util_test.go @@ -0,0 +1,75 @@ +package utils + +import ( + "context" + "testing" + "time" + + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/client-go/kubernetes/fake" +) + +func TestFindPodByPvc(t *testing.T) { + tests := []struct { + name string + err error + + client *fake.Clientset + }{ + { + name: "Single Pod,Single PVC", + err: nil, + client: fake.NewSimpleClientset( + &v1.PodList{ + Items: []v1.Pod{ + { + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pod", + Namespace: "test-ns", + }, + Spec: v1.PodSpec{ + Volumes: []v1.Volume{ + { + Name: "test-volume", + VolumeSource: v1.VolumeSource{ + PersistentVolumeClaim: &v1.PersistentVolumeClaimVolumeSource{ + ClaimName: "test-pvc", + }, + }, + }, + }, + }, + }, + }, + }, + &v1.PersistentVolumeClaim{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-pvc", + Namespace: "test-ns", + }, + }, + ), + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + podList, err := test.client.CoreV1().Pods("test-ns").List(ctx, metav1.ListOptions{}) + if err != nil { + t.Errorf("Failed to list pods from client: %v", err) + } + pvc, err := test.client.CoreV1().PersistentVolumeClaims("test-ns").Get(ctx, "test-pvc", metav1.GetOptions{}) + if err != nil { + t.Errorf("Failed to list PVCs from client: %v", err) + } + pod := FindPodByPVC(*podList, *pvc) + + if pod.Name != "test-pod" { + t.Errorf("Expected pod name to be 'test-pod', got '%s'", pod.Name) + } + }) + } +}