From db3f04f8aa92482a611fc9ee1ba1a8e8e07ba39a Mon Sep 17 00:00:00 2001 From: Ivan Velichko Date: Sun, 25 Feb 2024 13:44:32 +0000 Subject: [PATCH] fix cdebug exec for Kubernetes short-lived shells losing the output issue --- cmd/exec/exec_kubernetes.go | 38 +++++++++++------ e2e/exec/docker_test.go | 2 +- e2e/exec/kubernetes_test.go | 74 +++++++++++++++++++++++++++++++++ e2e/internal/fixture/fixture.go | 35 ++++++++++++++++ 4 files changed, 135 insertions(+), 14 deletions(-) create mode 100644 e2e/exec/kubernetes_test.go diff --git a/cmd/exec/exec_kubernetes.go b/cmd/exec/exec_kubernetes.go index 5e0821c..91ee8e4 100644 --- a/cmd/exec/exec_kubernetes.go +++ b/cmd/exec/exec_kubernetes.go @@ -11,6 +11,7 @@ import ( "strings" "time" + "github.com/sirupsen/logrus" corev1 "k8s.io/api/core/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -34,6 +35,8 @@ import ( "github.com/iximiuz/cdebug/pkg/uuid" ) +// TODO: Handle exit codes - terminate the `cdebug exec` command with the same exit code as the debugger container. + func runDebuggerKubernetes(ctx context.Context, cli cliutil.CLI, opts *options) error { if opts.autoRemove { return fmt.Errorf("--rm flag is not supported for Kubernetes") @@ -309,6 +312,8 @@ func attachPodDebugger( if status == nil { return fmt.Errorf("error getting debugger container %q status: %+v", debuggerName, err) } + logrus.Debugf("Debugger container %q status: %+v", debuggerName, status) + if status.State.Terminated != nil { dumpDebuggerLogs(ctx, client, ns, podName, debuggerName, cli.OutputStream()) @@ -355,18 +360,26 @@ func attachPodDebugger( TTY: opts.tty, }, scheme.ParameterCodec) - ctx, cancel := context.WithCancel(ctx) - defer cancel() + streamingCtx, cancelStreamingCtx := context.WithCancel(ctx) + defer cancelStreamingCtx() + go func() { - // Container is not running anymore, stop streaming. _, _ = waitForContainer(ctx, client, ns, podName, debuggerName, false) - cli.PrintAux("Debugger container %q is not running...\n", debuggerName) - cancel() - - dumpDebuggerLogs(ctx, client, ns, podName, debuggerName, cli.OutputStream()) + // Debugger container is not running anymore - streaming no longer needed. + cancelStreamingCtx() }() - return stream(ctx, cli, req.URL(), config, opts.tty) + if err := stream(streamingCtx, cli, req.URL(), config, opts.tty); err != nil { + return fmt.Errorf("error streaming to/from debugger container: %v", err) + } + + cli.PrintAux("Debugger container %q terminated...\n", debuggerName) + + if err := dumpDebuggerLogs(ctx, client, ns, podName, debuggerName, cli.OutputStream()); err != nil { + return fmt.Errorf("error dumping debugger logs: %v", err) + } + + return nil } func stream( @@ -439,13 +452,12 @@ func dumpDebuggerLogs( if _, err := out.Write(bytes); err != nil { return err } - - if err != nil { - if err != io.EOF { - return err - } + if err == io.EOF { return nil } + if err != nil { + return err + } } } diff --git a/e2e/exec/docker_test.go b/e2e/exec/docker_test.go index ba353bc..930f17b 100644 --- a/e2e/exec/docker_test.go +++ b/e2e/exec/docker_test.go @@ -29,7 +29,7 @@ func TestExecDockerShell(t *testing.T) { defer cleanup() res := icmd.RunCmd( - icmd.Command("cdebug", "exec", "--rm", "-i", targetID), + icmd.Command("cdebug", "exec", "--rm", "-q", "-i", targetID), icmd.WithStdin(strings.NewReader("echo \"hello $((6*7)) world\"\nexit 0\n")), ) diff --git a/e2e/exec/kubernetes_test.go b/e2e/exec/kubernetes_test.go new file mode 100644 index 0000000..314d320 --- /dev/null +++ b/e2e/exec/kubernetes_test.go @@ -0,0 +1,74 @@ +package exec + +import ( + "strings" + "testing" + "text/template" + + "gotest.tools/assert" + "gotest.tools/assert/cmp" + "gotest.tools/v3/icmd" + + "github.com/iximiuz/cdebug/e2e/internal/fixture" + "github.com/iximiuz/cdebug/pkg/uuid" +) + +var ( + simplePod = template.Must(template.New("simple-pod").Parse(`--- +apiVersion: v1 +kind: Pod +metadata: + name: {{.PodName}} + namespace: default +spec: + restartPolicy: Never + containers: + - image: {{.Image}} + imagePullPolicy: IfNotPresent + name: app +`)) +) + +func TestExecKubernetesSimple(t *testing.T) { + podName := "cdebug-" + strings.ToLower(t.Name()) + "-" + uuid.ShortID() + cleanup := fixture.KubectlApply(t, simplePod, map[string]string{ + "PodName": podName, + "Image": fixture.ImageNginx, + }) + defer cleanup() + + fixture.KubectlWaitFor(t, "pod", podName, "Ready") + + // Exec in the pod + res := icmd.RunCmd( + icmd.Command("cdebug", "exec", "-q", "pod/"+podName, "busybox"), + ) + res.Assert(t, icmd.Success) + assert.Check(t, cmp.Contains(res.Stdout(), "BusyBox v1")) + + // Exec in the pod's container + res = icmd.RunCmd( + icmd.Command("cdebug", "exec", "-q", "pod/"+podName+"/app", "cat", "/etc/os-release"), + ) + res.Assert(t, icmd.Success) + assert.Check(t, cmp.Contains(res.Stdout(), "debian")) +} + +func TestExecKubernetesShell(t *testing.T) { + podName := "cdebug-" + strings.ToLower(t.Name()) + "-" + uuid.ShortID() + cleanup := fixture.KubectlApply(t, simplePod, map[string]string{ + "PodName": podName, + "Image": fixture.ImageNginx, + }) + defer cleanup() + + fixture.KubectlWaitFor(t, "pod", podName, "Ready") + + res := icmd.RunCmd( + icmd.Command("cdebug", "exec", "-q", "-i", "pod/"+podName+"/app"), + icmd.WithStdin(strings.NewReader("echo \"hello $((6*7)) world\"\nexit 0\n")), + ) + res.Assert(t, icmd.Success) + assert.Equal(t, res.Stderr(), "") + assert.Check(t, cmp.Contains(res.Stdout(), "hello 42 world")) +} diff --git a/e2e/internal/fixture/fixture.go b/e2e/internal/fixture/fixture.go index 11d4fa7..db508a5 100644 --- a/e2e/internal/fixture/fixture.go +++ b/e2e/internal/fixture/fixture.go @@ -4,6 +4,7 @@ import ( "os/exec" "strings" "testing" + "text/template" "gotest.tools/icmd" @@ -137,3 +138,37 @@ func NerdctlRunBackground( return contID, cleanup } + +func KubectlApply( + t *testing.T, + manifestTmpl *template.Template, + data interface{}, +) func() { + var buf strings.Builder + if err := manifestTmpl.Execute(&buf, data); err != nil { + t.Fatalf("cannot execute template: %v", err) + } + + manifest := buf.String() + + cmd := icmd.Command("kubectl", "apply", "-f", "-") + res := icmd.RunCmd(cmd, icmd.WithStdin(strings.NewReader(manifest))) + res.Assert(t, icmd.Success) + + return func() { + cmd := icmd.Command("kubectl", "delete", "-f", "-") + res := icmd.RunCmd(cmd, icmd.WithStdin(strings.NewReader(manifest))) + res.Assert(t, icmd.Success) + } +} + +func KubectlWaitFor( + t *testing.T, + kind string, + name string, + condition string, +) { + cmd := icmd.Command("kubectl", "wait", kind, name, "--for=condition="+condition, "--timeout=60s") + res := icmd.RunCmd(cmd) + res.Assert(t, icmd.Success) +}