Skip to content

Commit

Permalink
refactor delve
Browse files Browse the repository at this point in the history
  • Loading branch information
runz0rd committed Oct 10, 2021
1 parent 47883c4 commit 8dd932b
Show file tree
Hide file tree
Showing 13 changed files with 591 additions and 507 deletions.
1 change: 0 additions & 1 deletion cmd/actions/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,6 @@ func Execute() {
if err := rootCmd.Execute(); err != nil {
l.Fatal(err)
}

}

func newLogger(verbose, jsonLog bool) *logrus.Entry {
Expand Down
167 changes: 167 additions & 0 deletions delve.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package gograpple

import (
"fmt"
"os"
"path"
"path/filepath"
"strings"
"time"

"github.com/foomo/gograpple/delve"
)

const delveBin = "dlv"

func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host string, port int, delveContinue, vscode bool) error {
// validate k8s resources for delve session
if err := g.validatePod(&pod); err != nil {
return err
}
if err := g.validateContainer(&container); err != nil {
return err
}
if !g.isPatched() {
return fmt.Errorf("deployment not patched, stopping delve")
}
// populate bin args if empty
if len(binArgs) == 0 {
var err error
d, err := g.kubeCmd.GetDeploymentFromConfigMap(g.deployment.Name, defaultConfigMapDeploymentKey)
if err != nil {
return err
}
c, err := g.kubeCmd.GetContainerFromDeployment(container, d)
if err != nil {
return err
}
binArgs = c.Args
}
// validate sourcePath
goModPath, err := findGoProjectRoot(sourcePath)
if err != nil {
return fmt.Errorf("couldnt find go.mod path for source %q", sourcePath)
}

delveServer := delve.NewKubeDelveServer(g.l, host, port)
return g.registerInterrupt(2*time.Second).wait(
func() error {
g.onExit(delveServer, pod, container)
return nil
},
func() error {
// on(Re)Load
// run pre-start cleanup
if err := g.cleanupDelve(pod, container); err != nil {
return err
}
// deploy bin
if err := g.deployBin(pod, container, goModPath, sourcePath); err != nil {
return err
}
// exec delve server and port-forward to pod
go delveServer.Start(pod, container, g.binDestination(), delveContinue, binArgs)
// check server state with delve client
go g.checkDelveConnection(host, port)
// start vscode
if vscode {
if err := launchVSCode(g.l, goModPath, host, port, 5); err != nil {
return err
}
}
defer g.onExit(delveServer, pod, container)
return nil
},
)
}

func (g Grapple) onExit(ds *delve.KubeDelveServer, pod, container string) {
// onExit
// try stopping the delve server regularly
if err := ds.Stop(); err != nil {
g.l.WithError(err).Warn("could not stop delve regularly")
}
// kill the remaining pids
if err := g.cleanupDelve(pod, container); err != nil {
g.l.WithError(err).Warn("could not cleanup delve")
}
}

func (g Grapple) binName() string {
return g.deployment.Name
}

func (g Grapple) binDestination() string {
return "/" + g.binName()
}

func (g Grapple) cleanupDelve(pod, container string) error {
// get pids of delve and app were debugging
g.l.Info("killing debug processes")
binPids, errBinPids := g.getPIDsOf(pod, container, g.binName())
if errBinPids != nil {
return errBinPids
}
delvePids, errDelvePids := g.getPIDsOf(pod, container, delveBin)
if errDelvePids != nil {
return errDelvePids
}
// kill pids directly on pod container
maxTries := 10
pids := append(binPids, delvePids...)
return tryCall(g.l, maxTries, time.Millisecond*200, func(i int) error {
killErrs := g.kubeCmd.KillPidsOnPod(pod, container, pids, true)
if len(killErrs) == 0 {
return nil
}
return fmt.Errorf("could not kill processes after %v attempts", maxTries)
})
}

func (g Grapple) deployBin(pod, container, goModPath, sourcePath string) error {
binSource := path.Join(os.TempDir(), g.binName())
g.l.Infof("building %q for debug", sourcePath)

var relInputs []string
inputInfo, errInputInfo := os.Stat(sourcePath)
if errInputInfo != nil {
return errInputInfo
}
if inputInfo.IsDir() {
if files, err := os.ReadDir(sourcePath); err != nil {
return err
} else {
for _, file := range files {
if path.Ext(file.Name()) == ".go" {
relInputs = append(relInputs, strings.TrimPrefix(path.Join(sourcePath, file.Name()), goModPath+string(filepath.Separator)))
}
}
}
} else {
relInputs = append(relInputs, strings.TrimPrefix(sourcePath, goModPath+string(filepath.Separator)))
}

_, errBuild := g.goCmd.Build(goModPath, binSource, relInputs, `-gcflags="all=-N -l"`).Env("GOOS=linux").Run()
if errBuild != nil {
return errBuild
}

g.l.Infof("copying binary to pod %v", pod)
_, errCopyToPod := g.kubeCmd.CopyToPod(pod, container, binSource, g.binDestination()).Run()
return errCopyToPod
}

func (g Grapple) checkDelveConnection(host string, port int) error {
return tryCall(g.l, 50, 200*time.Millisecond, func(i int) error {
delveClient, err := delve.NewKubeDelveClient(host, port, 3*time.Second)
if err != nil {
g.l.WithError(err).Warn("couldnt connect to delve server")
return err
}
if err := delveClient.ValidateState(); err != nil {
g.l.WithError(err).Warn("couldnt get running state from delve server")
return err
}
return nil
})
}
66 changes: 66 additions & 0 deletions delve/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package delve

import (
"errors"
"fmt"
"net"
"time"

"github.com/go-delve/delve/service/rpc2"
)

type KubeDelveClient struct {
*rpc2.RPCClient
}

func NewKubeDelveClient(host string, port int, timeout time.Duration) (*KubeDelveClient, error) {
// connection timeouts suck with k8s, because the port is open and you can connect, but ...
// early on the connection is a dead and the timeout does not kick in, despite the fact
// that the connection is not "really" establised
var client *rpc2.RPCClient
conn, err := net.Dial("tcp", fmt.Sprintf("%v:%v", host, port))
if err != nil {
return nil, err
}
chanClient := make(chan struct{})
go func() {
// the next level of suck is the rpc client
// it does not return an error for its constructor,
// even though it actually does make a call and that fails
// and the cient will not do any other calls,
// because it has shutdown internally, without telling us
// at least that is what I understand here
client = rpc2.NewClientFromConn(conn)
chanClient <- struct{}{}
}()
select {
case <-time.After(timeout):
// this is the actual timeout,
// since the connection timeout does not work (on k8s)
conn.Close()
// l.Warn("dlv server check timeout", timeout)
return nil, errors.New("stale connection to dlv, aborting after timeout")
case <-chanClient:
// were good to go
}
return &KubeDelveClient{client}, nil
}

func (kdc KubeDelveClient) ValidateState() error {
state, err := kdc.GetState()
if err != nil {
return err
}
if state.Exited {
// Exited indicates whether the debugged process has exited.
return fmt.Errorf("delve debugged process has exited")
} else if !state.Running {
// Running is true if the process is running and no other information can be collected.
// theres a case when you are in a breakpoint on a zombie process
// dlv will not handle that gracefully
return fmt.Errorf("delve debugged process is not running (is it a zombie, or has it been halted by a breakpoint)")
} else {
// were good
return nil
}
}
67 changes: 67 additions & 0 deletions delve/server.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package delve

import (
"fmt"
"os"

"github.com/foomo/gograpple/exec"
"github.com/sirupsen/logrus"
)

type KubeDelveServer struct {
l *logrus.Entry
host string
port int
kubeCmd *exec.KubectlCmd
process *os.Process
}

func (kds KubeDelveServer) Host() string {
return kds.host
}

func (kds KubeDelveServer) Port() int {
return kds.port
}

func NewKubeDelveServer(l *logrus.Entry, host string, port int) *KubeDelveServer {
return &KubeDelveServer{l, host, port, exec.NewKubectlCommand(l), nil}
}

func (kds *KubeDelveServer) Start(pod, container string, binDest string, useContinue bool, binArgs []string) error {
cmd := []string{
"dlv", "exec", binDest, "--api-version=2", "--headless",
fmt.Sprintf("--listen=:%v", kds.port), "--accept-multiclient",
}
if useContinue {
cmd = append(cmd, "--continue")
}
if len(binArgs) > 0 {
cmd = append(cmd, "--")
cmd = append(cmd, binArgs...)
}

// execute command to run dlv on container
// this will block until is killed or fails
_, err := kds.kubeCmd.ExecPod(pod, container, cmd).PostStart(
func(p *os.Process) error {
kds.process = p
// after starting
// port-forward from localhost to the pod
kds.l.Infof("port-forwarding %v pod for delve server", pod)
_, pfErr := kds.kubeCmd.PortForwardPod(pod, kds.host, kds.port).Run()
return pfErr
},
).Run()
return err
}

func (kds *KubeDelveServer) Stop() error {
if kds.process == nil {
return fmt.Errorf("no process found, run start successfully first")
}
if err := kds.process.Release(); err != nil {
return err
}
return kds.process.Kill()
}
51 changes: 0 additions & 51 deletions dlv.go

This file was deleted.

8 changes: 4 additions & 4 deletions exec/cmd.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import (

type Cmd struct {
l *logrus.Entry
// cmd *exec.Cmd
// actual *exec.Cmd
command []string
cwd string
env []string
Expand All @@ -22,7 +22,7 @@ type Cmd struct {
wait bool
t time.Duration
preStartFunc func() error
postStartFunc func() error
postStartFunc func(p *os.Process) error
postEndFunc func() error
}

Expand Down Expand Up @@ -95,7 +95,7 @@ func (c *Cmd) PreStart(f func() error) *Cmd {
return c
}

func (c *Cmd) PostStart(f func() error) *Cmd {
func (c *Cmd) PostStart(f func(p *os.Process) error) *Cmd {
c.postStartFunc = f
return c
}
Expand Down Expand Up @@ -133,7 +133,7 @@ func (c *Cmd) Run() (string, error) {
}

if c.postStartFunc != nil {
if err := c.postStartFunc(); err != nil {
if err := c.postStartFunc(cmd.Process); err != nil {
return "", err
}
}
Expand Down
Loading

0 comments on commit 8dd932b

Please sign in to comment.