Skip to content

Commit

Permalink
chore(debug): clean up code
Browse files Browse the repository at this point in the history
 * add flag copy: for shared namespaces
 * default to fist pod if flag by-pod is not set
 * update information text

Co-authored-by: Sindre Rødseth Hansen <[email protected]>
Co-authored-by: Carl Hedgren <[email protected]>
  • Loading branch information
3 people committed Nov 7, 2024
1 parent e137f36 commit 851b8a0
Show file tree
Hide file tree
Showing 4 changed files with 175 additions and 37 deletions.
50 changes: 43 additions & 7 deletions cmd/debugcmd/debugcmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package debugcmd

import (
"fmt"

"k8s.io/client-go/kubernetes"

"github.com/nais/cli/pkg/debug"
Expand All @@ -11,21 +12,30 @@ import (

const (
contextFlagName = "context"
copyFlagName = "copy"
namespaceFlagName = "namespace"
byPodFlagName = "by-pod"
debugImageDefault = "europe-north1-docker.pkg.dev/nais-io/nais/images/debug:latest"
)

func Command() *cli.Command {
return &cli.Command{
Name: "debug",
Usage: "Create and attach to a debug container",
ArgsUsage: "workloadname [namespace]",
Description: "Create and attach to a debug container to your specified workload in the current namespace or a specified namespace to \n" +
"debug your workload. The debug container is based on the debug image '" + debugImageDefault + "'.",
ArgsUsage: "workloadname",
Description: "Create and attach to a debug pod or container. \n" +
"When flag '--copy' is set, the command can be used to debug a copy of the original pod, \n" +
"allowing you to troubleshoot without affecting the live pod.\n" +
"To debug a live pod, run the command without the '--copy' flag.\n" +
"You can only reconnect to the debug session if the pod is running.",
Subcommands: []*cli.Command{
tidyCommand(),
},
Flags: []cli.Flag{
kubeConfigFlag(),
copyFlag(),
namespaceFlag(),
byPodFlag(),
},
Before: func(context *cli.Context) error {
if context.Args().Len() < 1 {
Expand Down Expand Up @@ -62,7 +72,6 @@ func setupClient(cfg *debug.Config, cCtx *cli.Context) (kubernetes.Interface, er
return nil, err
}
return clientset, nil

}

func kubeConfigFlag() *cli.StringFlag {
Expand All @@ -74,13 +83,40 @@ func kubeConfigFlag() *cli.StringFlag {
}
}

func byPodFlag() *cli.BoolFlag {
return &cli.BoolFlag{
Name: "by-pod",
Aliases: []string{"p"},
Usage: "Attach to a specific `BY-POD` in a workload",
DefaultText: "The first pod in the workload",
}
}

func copyFlag() *cli.BoolFlag {
return &cli.BoolFlag{
Name: copyFlagName,
Aliases: []string{"cp"},
Usage: "To create or delete a 'COPY' of pod with a debug container. The original pod remains running and unaffected",
DefaultText: "Attach to the current 'live' pod",
}
}

func namespaceFlag() *cli.StringFlag {
return &cli.StringFlag{
Name: namespaceFlagName,
Aliases: []string{"n"},
Usage: "The `NAMESPACE` to use",
DefaultText: "The current namespace in your kubeconfig",
}
}

func makeConfig(cCtx *cli.Context) *debug.Config {
appName := cCtx.Args().First()
namespace := cCtx.Args().Get(1)

return &debug.Config{
WorkloadName: appName,
Namespace: namespace,
Namespace: cCtx.String(namespaceFlagName),
DebugImage: debugImageDefault,
CopyPod: cCtx.Bool(copyFlagName),
ByPod: cCtx.Bool(byPodFlagName),
}
}
8 changes: 5 additions & 3 deletions cmd/debugcmd/tidycmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,13 @@ import (
func tidyCommand() *cli.Command {
return &cli.Command{
Name: "tidy",
Usage: "Clean up debug containers and debug pods from your workload",
Description: "Remove debug containers created by the debug command, the pods will be deleted automatically",
ArgsUsage: "workloadname [namespace]",
Usage: "Clean up debug containers and debug pods",
Description: "Remove debug containers created by the 'debug' command. To delete copy pods set the '--copy' flag.",
ArgsUsage: "workloadname",
Flags: []cli.Flag{
kubeConfigFlag(),
namespaceFlag(),
copyFlag(),
},
Before: func(context *cli.Context) error {
if context.Args().Len() < 1 {
Expand Down
114 changes: 101 additions & 13 deletions pkg/debug/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,19 @@ import (
"os"
"os/exec"

k8serrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/manifoldco/promptui"

core_v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)

const (
debuggerSuffix = "nais-debugger"
)

type Debug struct {
ctx context.Context
client kubernetes.Interface
Expand All @@ -24,6 +30,8 @@ type Config struct {
Context string
WorkloadName string
DebugImage string
CopyPod bool
ByPod bool
}

func Setup(client kubernetes.Interface, cfg *Config) *Debug {
Expand Down Expand Up @@ -51,9 +59,67 @@ func (d *Debug) getPodsForWorkload() (*core_v1.PodList, error) {
return podList, nil
}

func debuggerContainerName(podName string) string {
return fmt.Sprintf("%s-%s", podName, debuggerSuffix)
}

func (d *Debug) debugPod(podName string) error {
if d.cfg.CopyPod {
pN := debuggerContainerName(podName)
_, err := d.client.CoreV1().Pods(d.cfg.Namespace).Get(d.ctx, pN, metav1.GetOptions{})
if err == nil {
fmt.Printf("Debug pod copy %s already exists. Attaching...\n", pN)
// Debug pod copy already exists, attach to it
return d.attachToExistingDebugContainer(pN)
} else if !k8serrors.IsNotFound(err) {
return fmt.Errorf("failed to check for existing debug pod copy %s: %v", pN, err)
}
} else {
pod, err := d.client.CoreV1().Pods(d.cfg.Namespace).Get(d.ctx, podName, metav1.GetOptions{})
if err != nil {
return fmt.Errorf("failed to get pod %s: %v", podName, err)
}

if len(pod.Spec.EphemeralContainers) > 0 {
fmt.Printf("The container %s already has %d terminated debug containers. \n", podName, len(pod.Spec.EphemeralContainers))
fmt.Printf("Please consider using 'nais debug tidy %s' to clean up\n", d.cfg.WorkloadName)
}
}

return d.createDebugPod(podName)
}

func (d *Debug) attachToExistingDebugContainer(podName string) error {
defaultDebuggerName := "debugger"
cmd := exec.Command(
"kubectl",
"attach",
"-n", d.cfg.Namespace,
fmt.Sprintf("pod/%s", podName),
"-c", defaultDebuggerName,
"-i",
"-t",
"--context", d.cfg.Context,
)

cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start attach command: %v", err)
}
fmt.Printf("Attaching to existing debug container %s in pod %s\n", defaultDebuggerName, podName)

if err := cmd.Wait(); err != nil {
return fmt.Errorf("attach command failed: %v", err)
}

return nil
}

func (d *Debug) createDebugPod(podName string) error {
args := []string{
"debug",
"-n", d.cfg.Namespace,
fmt.Sprintf("pod/%s", podName),
Expand All @@ -62,21 +128,40 @@ func (d *Debug) debugPod(podName string) error {
"--tty",
"--context", d.cfg.Context,
"--profile=restricted",
"--image", d.cfg.DebugImage)
"-q",
"--image", d.cfg.DebugImage,
}

if d.cfg.CopyPod {
args = append(args,
"--copy-to", debuggerContainerName(podName),
"-c", "debugger",
)
}

cmd := exec.Command("kubectl", args...)
cmd.Stdin = os.Stdin
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr

if err := cmd.Start(); err != nil {
return fmt.Errorf("failed to start command: %v", err)
return fmt.Errorf("failed to start debug command: %v", err)
}

if d.cfg.CopyPod {
fmt.Printf("Debugging pod copy created, enable process namespace sharing in %s\n", debuggerContainerName(podName))
} else {
fmt.Printf("Debugging container created...\n")
}
fmt.Printf("Using debugger image %s\n", d.cfg.DebugImage)

if err := cmd.Wait(); err != nil {
return fmt.Errorf("command failed: %v", err)
return fmt.Errorf("debug command failed: %v", err)
}

fmt.Printf("Run 'nais debug tidy %s' to clean up debug containers, this will delete pod(s) with debug containers \n", d.cfg.WorkloadName)
if d.cfg.CopyPod {
fmt.Printf("Run 'nais debug -cp %s' command to attach to the debug pod\n", podName)
}

return nil
}
Expand All @@ -97,15 +182,18 @@ func (d *Debug) Debug() error {
return nil
}

prompt := promptui.Select{
Label: "Select pod to Debug",
Items: podNames,
}

_, podName, err := prompt.Run()
if err != nil {
fmt.Printf("prompt failed %v\n", err)
return err
podName := podNames[0]
if d.cfg.ByPod {
prompt := promptui.Select{
Label: "Select pod to Debug",
Items: podNames,
}

_, podName, err = prompt.Run()
if err != nil {
fmt.Printf("prompt failed %v\n", err)
return err
}
}

if err := d.debugPod(podName); err != nil {
Expand Down
40 changes: 26 additions & 14 deletions pkg/debug/tidy.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"fmt"
"strings"

k8serrors "k8s.io/apimachinery/pkg/api/errors"

"github.com/manifoldco/promptui"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
Expand All @@ -25,42 +27,52 @@ func (d *Debug) Tidy() error {
return nil
}

epHConTotal := 0
for _, pod := range pods.Items {
if len(pod.Spec.EphemeralContainers) == 0 {
podName := pod.Name
if d.cfg.CopyPod {
podName = debuggerContainerName(pod.Name)
}

if !d.cfg.CopyPod && len(pod.Spec.EphemeralContainers) == 0 {
fmt.Printf("No debug container found for: %s\n", pod.Name)
continue
}

epHConTotal += len(pod.Spec.EphemeralContainers)
_, err := d.client.CoreV1().Pods(d.cfg.Namespace).Get(d.ctx, podName, metav1.GetOptions{})
if err != nil {
if k8serrors.IsNotFound(err) {
fmt.Printf("No debug pod found for: %s\n", pod.Name)
continue
}
fmt.Printf("Failed to get pod %s: %v\n", podName, err)
return err
}

prompt := promptui.Prompt{
Label: fmt.Sprintf("Pod '%s' contains '%d' debug container(s), do you want to clean up", pod.Name, len(pod.Spec.EphemeralContainers)),
Label: fmt.Sprintf("Pod '%s' with debug container, do you want to clean up", podName),
IsConfirm: true,
}

answer, err := prompt.Run()
if err != nil {
if errors.Is(err, promptui.ErrAbort) {
fmt.Printf("Skipping deletion for pod: %s\n", pod.Name)
fmt.Printf("Skipping deletion for pod: %s\n", podName)
continue
}
fmt.Printf("Error reading input for pod %s: %v\n", pod.Name, err)
fmt.Printf("Error reading input for pod %s: %v\n", podName, err)
return err
}

// Delete pod if user confirms with "y" or "yes"
if strings.ToLower(answer) == "y" || strings.ToLower(answer) == "yes" {
if err := d.client.CoreV1().Pods(d.cfg.Namespace).Delete(d.ctx, pod.Name, metav1.DeleteOptions{}); err != nil {
fmt.Printf("Failed to delete pod %s: %v\n", pod.Name, err)
if err := d.client.CoreV1().Pods(d.cfg.Namespace).Delete(d.ctx, podName, metav1.DeleteOptions{}); err != nil {
fmt.Printf("Failed to delete pod %s: %v\n", podName, err)
} else {
fmt.Println("Deleted pod:", pod.Name)
fmt.Println("Deleted pod:", podName)
}
} else {
fmt.Println("Skipped pod:", pod.Name)
fmt.Println("Skipped pod:", podName)
}
}

if epHConTotal == 0 {
fmt.Printf("Workload '%s' does not contain any debug containers\n", d.cfg.WorkloadName)
}
return nil
}

0 comments on commit 851b8a0

Please sign in to comment.