-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
13 changed files
with
591 additions
and
507 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
}) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.