Skip to content

Commit b1f213a

Browse files
authored
Merge pull request #455 from nais/test_debug
Test debug
2 parents 6b710c2 + fb19966 commit b1f213a

File tree

8 files changed

+322
-2
lines changed

8 files changed

+322
-2
lines changed

cmd/cmd.go

+3
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"log"
55
"os"
66

7+
"github.com/nais/cli/cmd/debugcmd"
8+
79
"github.com/nais/cli/cmd/aivencmd"
810
"github.com/nais/cli/cmd/devicecmd"
911
"github.com/nais/cli/cmd/kubeconfigcmd"
@@ -28,6 +30,7 @@ func commands() []*cli.Command {
2830
kubeconfigcmd.Command(),
2931
postgrescmd.Command(),
3032
validatecmd.Command(),
33+
debugcmd.Command(),
3134
)
3235
}
3336

cmd/debugcmd/debugcmd.go

+75
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package debugcmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/nais/cli/pkg/debug"
7+
"github.com/nais/cli/pkg/k8s"
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
const (
12+
namespaceFlagName = "namespace"
13+
contextFlagName = "context"
14+
debugImageDefault = "europe-north1-docker.pkg.dev/nais-io/nais/images/debug:latest"
15+
)
16+
17+
func Command() *cli.Command {
18+
return &cli.Command{
19+
Name: "debug",
20+
Usage: "Create and attach to a debug container",
21+
ArgsUsage: "appname",
22+
Subcommands: []*cli.Command{
23+
tidyCommand(),
24+
},
25+
Flags: []cli.Flag{
26+
kubeConfigFlag(),
27+
},
28+
Before: func(context *cli.Context) error {
29+
if context.Args().Len() < 1 {
30+
return fmt.Errorf("missing required arguments: %v", context.Command.ArgsUsage)
31+
}
32+
33+
return nil
34+
},
35+
Action: func(cCtx *cli.Context) error {
36+
cfg := makeConfig(cCtx)
37+
cluster := cCtx.String(contextFlagName)
38+
client := k8s.SetupControllerRuntimeClient(k8s.WithKubeContext(cluster))
39+
if cfg.Namespace == "" {
40+
cfg.Namespace = client.CurrentNamespace
41+
}
42+
43+
clientset, err := k8s.SetupClientGo(cluster)
44+
if err != nil {
45+
return err
46+
}
47+
48+
dg := debug.Setup(clientset, cfg)
49+
if err := dg.Debug(); err != nil {
50+
return fmt.Errorf("debugging instance: %w", err)
51+
}
52+
return nil
53+
},
54+
}
55+
}
56+
57+
func kubeConfigFlag() *cli.StringFlag {
58+
return &cli.StringFlag{
59+
Name: contextFlagName,
60+
Aliases: []string{"c"},
61+
Usage: "The kubeconfig `CONTEXT` to use",
62+
DefaultText: "The current context in your kubeconfig",
63+
}
64+
}
65+
66+
func makeConfig(cCtx *cli.Context) debug.Config {
67+
appName := cCtx.Args().First()
68+
namespace := cCtx.Args().Get(1)
69+
70+
return debug.Config{
71+
AppName: appName,
72+
Namespace: namespace,
73+
DebugImage: debugImageDefault,
74+
}
75+
}

cmd/debugcmd/tidycmd.go

+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package debugcmd
2+
3+
import (
4+
"fmt"
5+
6+
"github.com/nais/cli/pkg/debug"
7+
"github.com/nais/cli/pkg/k8s"
8+
"github.com/urfave/cli/v2"
9+
)
10+
11+
func tidyCommand() *cli.Command {
12+
return &cli.Command{
13+
Name: "tidy",
14+
Usage: "Clean up ephemeral containers and debug pods",
15+
ArgsUsage: "appname",
16+
Flags: []cli.Flag{
17+
kubeConfigFlag(),
18+
},
19+
Before: func(context *cli.Context) error {
20+
if context.Args().Len() < 1 {
21+
return fmt.Errorf("missing required arguments: %v", context.Command.ArgsUsage)
22+
}
23+
24+
return nil
25+
},
26+
Action: func(cCtx *cli.Context) error {
27+
cfg := makeConfig(cCtx)
28+
cluster := cCtx.String(contextFlagName)
29+
namespace := cCtx.String(namespaceFlagName)
30+
client := k8s.SetupControllerRuntimeClient(k8s.WithKubeContext(cluster))
31+
cfg.Namespace = client.CurrentNamespace
32+
if namespace != "" {
33+
cfg.Namespace = namespace
34+
}
35+
36+
clientset, err := k8s.SetupClientGo(cluster)
37+
if err != nil {
38+
return err
39+
}
40+
41+
dg := debug.Setup(clientset, cfg)
42+
if err := dg.Tidy(); err != nil {
43+
return fmt.Errorf("debugging instance: %w", err)
44+
}
45+
return nil
46+
},
47+
}
48+
}

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ require (
88
github.com/go-logr/logr v1.4.2
99
github.com/goccy/go-yaml v1.13.2
1010
github.com/mailgun/raymond/v2 v2.0.48
11+
github.com/manifoldco/promptui v0.9.0
1112
github.com/mitchellh/go-ps v1.0.0
1213
github.com/nais/device v0.0.0-20231030073017-240e8a0234d9
1314
github.com/nais/liberator v0.0.0-20241101125801-cc4bc469b422
@@ -38,6 +39,7 @@ require (
3839

3940
require (
4041
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
42+
github.com/chzyer/readline v1.5.1 // indirect
4143
github.com/grpc-ecosystem/grpc-gateway/v2 v2.22.0 // indirect
4244
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
4345
github.com/klauspost/cpuid/v2 v2.2.7 // indirect

go.sum

+13
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,15 @@ github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyY
3939
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
4040
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
4141
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
42+
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
43+
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
44+
github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ=
45+
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI=
46+
github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI=
47+
github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk=
48+
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
49+
github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04=
50+
github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8=
4251
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
4352
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
4453
github.com/containerd/console v1.0.3 h1:lIr7SlA5PxZyMV30bDW0MGbiOPXwc63yRuCP0ARubLw=
@@ -207,6 +216,8 @@ github.com/mailgun/raymond/v2 v2.0.48 h1:5dmlB680ZkFG2RN/0lvTAghrSxIESeu9/2aeDqA
207216
github.com/mailgun/raymond/v2 v2.0.48/go.mod h1:lsgvL50kgt1ylcFJYZiULi5fjPBkkhNfj4KA0W54Z18=
208217
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
209218
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
219+
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
220+
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
210221
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
211222
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
212223
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
@@ -380,6 +391,7 @@ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
380391
golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
381392
golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
382393
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
394+
golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
383395
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
384396
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
385397
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -389,6 +401,7 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
389401
golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
390402
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
391403
golang.org/x/sys v0.0.0-20211013075003-97ac67df715c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
404+
golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
392405
golang.org/x/sys v0.0.0-20220319134239-a9b59b0215f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
393406
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
394407
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=

pkg/debug/debug.go

+114
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package debug
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"os"
7+
"os/exec"
8+
9+
"github.com/manifoldco/promptui"
10+
11+
core_v1 "k8s.io/api/core/v1"
12+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
13+
"k8s.io/client-go/kubernetes"
14+
)
15+
16+
type Debug struct {
17+
ctx context.Context
18+
client kubernetes.Interface
19+
cfg Config
20+
}
21+
22+
type Config struct {
23+
Namespace string
24+
Context string
25+
AppName string
26+
DebugImage string
27+
}
28+
29+
func Setup(client kubernetes.Interface, cfg Config) *Debug {
30+
return &Debug{
31+
ctx: context.Background(),
32+
client: client,
33+
cfg: cfg,
34+
}
35+
}
36+
37+
func (d *Debug) getPods() (*core_v1.PodList, error) {
38+
var podList *core_v1.PodList
39+
var err error
40+
podList, err = d.client.CoreV1().Pods(d.cfg.Namespace).List(d.ctx, metav1.ListOptions{
41+
LabelSelector: fmt.Sprintf("app.kubernetes.io/name=%s", d.cfg.AppName),
42+
})
43+
if len(podList.Items) == 0 {
44+
podList, err = d.client.CoreV1().Pods(d.cfg.Namespace).List(d.ctx, metav1.ListOptions{
45+
LabelSelector: fmt.Sprintf("app=%s", d.cfg.AppName),
46+
})
47+
}
48+
if err != nil {
49+
return nil, fmt.Errorf("failed to get pods: %w", err)
50+
}
51+
return podList, nil
52+
}
53+
54+
func (d *Debug) debugPod(podName string) error {
55+
cmd := exec.Command(
56+
"kubectl",
57+
"debug",
58+
"-n", d.cfg.Namespace,
59+
fmt.Sprintf("pod/%s", podName),
60+
"-it",
61+
"--stdin",
62+
"--tty",
63+
"--context", d.cfg.Context,
64+
"--profile=restricted",
65+
"--image", d.cfg.DebugImage)
66+
67+
cmd.Stdin = os.Stdin
68+
cmd.Stdout = os.Stdout
69+
cmd.Stderr = os.Stderr
70+
71+
if err := cmd.Start(); err != nil {
72+
return fmt.Errorf("failed to start command: %v", err)
73+
}
74+
75+
if err := cmd.Wait(); err != nil {
76+
return fmt.Errorf("command failed: %v", err)
77+
}
78+
79+
return nil
80+
}
81+
82+
func (d *Debug) Debug() error {
83+
pods, err := d.getPods()
84+
if err != nil {
85+
return err
86+
}
87+
88+
var podNames []string
89+
for _, pod := range pods.Items {
90+
podNames = append(podNames, pod.Name)
91+
}
92+
93+
if len(podNames) == 0 {
94+
fmt.Println("No pods found.")
95+
return nil
96+
}
97+
98+
prompt := promptui.Select{
99+
Label: "Select pod to Debug",
100+
Items: podNames,
101+
}
102+
103+
_, podName, err := prompt.Run()
104+
if err != nil {
105+
fmt.Printf("prompt failed %v\n", err)
106+
return err
107+
}
108+
109+
if err := d.debugPod(podName); err != nil {
110+
fmt.Printf("failed to debug pod %s: %v\n", podName, err)
111+
}
112+
113+
return nil
114+
}

pkg/debug/tidy.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package debug
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
7+
"github.com/manifoldco/promptui"
8+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
9+
)
10+
11+
func (d *Debug) Tidy() error {
12+
pods, err := d.getPods()
13+
if err != nil {
14+
return err
15+
}
16+
17+
var podNames []string
18+
for _, pod := range pods.Items {
19+
podNames = append(podNames, pod.Name)
20+
}
21+
22+
if len(podNames) == 0 {
23+
fmt.Println("No pods found.")
24+
return nil
25+
}
26+
27+
deleted := 0
28+
for _, pod := range pods.Items {
29+
if len(pod.Spec.EphemeralContainers) == 0 {
30+
continue
31+
}
32+
33+
prompt := promptui.Prompt{
34+
Label: fmt.Sprintf("Do you want to delete pod %s", pod.Name),
35+
IsConfirm: true,
36+
}
37+
38+
answer, err := prompt.Run()
39+
if err != nil {
40+
if err == promptui.ErrAbort {
41+
fmt.Printf("Skipping deletion for pod: %s\n", pod.Name)
42+
continue
43+
}
44+
fmt.Printf("Error reading input for pod %s: %v\n", pod.Name, err)
45+
return err
46+
}
47+
48+
// Delete pod if user confirms with "y" or "yes"
49+
if strings.ToLower(answer) == "y" || strings.ToLower(answer) == "yes" {
50+
if err := d.client.CoreV1().Pods(d.cfg.Namespace).Delete(d.ctx, pod.Name, metav1.DeleteOptions{}); err != nil {
51+
fmt.Printf("Failed to delete pod %s: %v\n", pod.Name, err)
52+
} else {
53+
deleted++
54+
fmt.Println("Deleted pod:", pod.Name)
55+
}
56+
} else {
57+
fmt.Println("Skipped pod:", pod.Name)
58+
}
59+
}
60+
61+
if deleted == 0 {
62+
fmt.Println("No pods with ephemeral containers found.")
63+
}
64+
return nil
65+
}

0 commit comments

Comments
 (0)