Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/attach #7

Merged
merged 9 commits into from
Feb 6, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,5 +17,6 @@
.todo
.idea/
.vscode/
!test/app/.vscode/

bin/
100 changes: 100 additions & 0 deletions attach.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package gograpple

import (
"fmt"
"os"
"os/signal"
"runtime"

"github.com/bitfield/script"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/log"
"github.com/pkg/errors"
)

func (g Grapple) Attach(namespace, deployment, container, bin, arch, host string, port int) error {
pod, err := kubectl.GetMostRecentRunningPodBySelectors(namespace, g.deployment.Spec.Selector.MatchLabels)
if err != nil {
return err
}
go handleExit(namespace, pod, container)
// check if delve is available
dlvDest := "dlv"
_, err = kubectl.ExecPod(namespace, pod, container, []string{"which", "dlv"}).String()
if err != nil {
if err := copyDelve(namespace, pod, container, arch, dlvDest); err != nil {
return err
}
dlvDest = "/dlv"
}
// find pid of bin by name
pids, err := kubectl.GetPIDsOf(namespace, pod, container, bin)
if err != nil {
return err
}
if len(pids) != 1 {
return fmt.Errorf("found none or more than one process named %q", bin)
}
go attachDelveOnPod(namespace, pod, container, dlvDest, pids[0], host, port, g.debug)
// launchVSCode(context.Background(), g.l, "./test/app", "", port, 3)
return kubectl.PortForwardPod(namespace, pod, port)
}

func attachCmd(dlvPath, binPid, host string, port int, debug bool) []string {
cmd := []string{dlvPath, "--headless", "attach", binPid, "--api-version=2",
"--continue", "--accept-multiclient", fmt.Sprintf("--listen=%v:%v", host, port)}
if debug {
cmd = append(cmd, "--log", "--log-output=rpc,dap,debugger")
}
return cmd
}

func dapCmd(dlvPath string, port int, debug bool) []string {
cmd := []string{dlvPath, "dap", "--listen",
fmt.Sprintf("127.0.0.1:%v", port)}
if debug {
cmd = append(cmd, "--log", "--log-output=rpc,dap,debugger")
}
return cmd
}

func attachDelveOnPod(namespace, pod, container, dlvPath, binPid, host string, port int, debug bool) error {
_, err := kubectl.ExecPod(namespace, pod, container, attachCmd(dlvPath, binPid, host, port, debug)).WithStdout(log.Writer("dlv")).Stdout()
return err
}

func cleanup(namespace, pod, container string) {
pss := []string{"dlv"}
for _, ps := range pss {
kubectl.ExecPod(namespace, pod, container, []string{"pkill", ps}).WithStdout(log.Writer("cleanup")).Stdout()
// script.Exec(fmt.Sprintf("pkill %v", ps)).Stdout()
}
}

func copyDelve(namespace, pod, container, arch, dlvDest string) error {
// build dlv for given arch
dlvSrc := fmt.Sprintf("%v/go/bin/linux_%v/dlv", os.Getenv("HOME"), arch)
if runtime.GOOS == "linux" && arch == runtime.GOARCH {
// if its the same os and arch use a different location
os.Setenv("GOBIN", "/tmp/")
dlvSrc = "/tmp/dlv"
}
os.Setenv("CGO_ENABLED", "0")
os.Setenv("GOOS", "linux")
os.Setenv("GOARCH", arch)
if out, err := script.Exec(
`go install -ldflags "-s -w -extldflags '-static'" github.com/go-delve/delve/cmd/dlv@latest`).String(); err != nil {
return errors.WithMessage(err, out)
}
// copy dlv to pod
return kubectl.CopyToPod(namespace, pod, container, dlvSrc, dlvDest)
}

func handleExit(namespace, pod, container string) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
<-signalChan
log.Entry("cleanup").Info("exiting")
cleanup(namespace, pod, container)
os.Exit(0)
}
23 changes: 13 additions & 10 deletions cmd/actions/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,23 +18,26 @@ var (
if err != nil {
return err
}
addr := HostPort{}
if err := addr.Set(c.ListenAddr); err != nil {
return err
}
if err := kubectl.SetContext(c.Cluster); err != nil {
g, err := gograpple.NewGrapple(newLogger(flagVerbose, flagJSONLog), c.Namespace, c.Deployment, flagDebug)
if err != nil {
return err
}
g, err := gograpple.NewGrapple(newLogger(flagVerbose, flagJSONLog), c.Namespace, c.Deployment)
host, port, err := c.Addr()
if err != nil {
return err
}
if err := g.Patch(c.Image, c.Container, nil); err != nil {
if err := kubectl.SetContext(c.Cluster); err != nil {
return err
}
defer g.Rollback()
// todo support binargs from config
return g.Delve("", c.Container, c.SourcePath, nil, addr.Host, addr.Port, c.LaunchVscode, c.DelveContinue)
if c.AttachTo == "" {
if err := g.Patch(c.Image, c.Container, nil); err != nil {
return err
}
defer g.Rollback()
// todo support binargs from config
return g.Delve("", c.Container, c.SourcePath, nil, host, port, c.LaunchVscode, c.DelveContinue)
}
return g.Attach(c.Namespace, c.Deployment, c.Container, c.AttachTo, c.Arch, host, port)
},
}
)
4 changes: 3 additions & 1 deletion cmd/actions/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ func init() {
rootCmd.PersistentFlags().BoolVarP(&flagVerbose, "verbose", "v", false, "Specifies should command output be displayed")
rootCmd.PersistentFlags().StringVarP(&flagPod, "pod", "p", "", "pod name (default most recent one)")
rootCmd.PersistentFlags().StringVarP(&flagContainer, "container", "c", "", "container name (default deployment name)")
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "", false, "debug mode")
patchCmd.Flags().StringVar(&flagImage, "image", "alpine:latest", "image to be used for patching (default alpine:latest)")
patchCmd.Flags().StringArrayVarP(&flagMounts, "mount", "m", []string{}, "host path to be mounted (default none)")
patchCmd.Flags().BoolVar(&flagRollback, "rollback", false, "rollback deployment to a previous state")
Expand Down Expand Up @@ -41,6 +42,7 @@ var (
flagVscode bool
flagContinue bool
flagJSONLog bool
flagDebug bool
)

var (
Expand All @@ -59,7 +61,7 @@ var (
if err != nil {
return err
}
grapple, err = gograpple.NewGrapple(l, flagNamespace, args[0])
grapple, err = gograpple.NewGrapple(l, flagNamespace, args[0], flagDebug)
if err != nil {
return err
}
Expand Down
50 changes: 45 additions & 5 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path"
"strconv"
"strings"

"github.com/c-bata/go-prompt"
Expand All @@ -20,11 +21,28 @@ type Config struct {
Cluster string `yaml:"cluster"`
Namespace string `yaml:"namespace" depends:"Cluster"`
Deployment string `yaml:"deployment" depends:"Namespace"`
Container string `yaml:"container,omitempty" depends:"Deployment"`
Container string `yaml:"container" depends:"Deployment"`
AttachTo string `yaml:"attach_to,omitempty" depends:"Container"`
LaunchVscode bool `yaml:"launch_vscode" default:"false"`
ListenAddr string `yaml:"listen_addr,omitempty" default:":2345"`
DelveContinue bool `yaml:"delve_continue" default:"false"`
ListenAddr string `yaml:"listen_addr,omitempty" default:"127.0.0.1:2345"`
DelveContinue bool `yaml:"delve_continue,omitempty" default:"false"`
Image string `yaml:"image,omitempty" default:"alpine:latest"`
Arch string `yaml:"arch,omitempty" default:"amd64"`
}

func (c Config) Addr() (host string, port int, err error) {
pieces := strings.Split(c.ListenAddr, ":")
if len(pieces) != 2 {
return host, port, fmt.Errorf("unable to parse addr from %q", c.ListenAddr)
}
host = pieces[0]
if host == "" {
host = "127.0.0.1"
}
if port, err = strconv.Atoi(pieces[1]); err != nil {
return host, port, err
}
return host, port, err
}

func (c Config) MarshalYAML() (interface{}, error) {
Expand Down Expand Up @@ -74,6 +92,24 @@ func (c Config) ContainerSuggest(d prompt.Document) []prompt.Suggest {
}))
}

func (c Config) AttachToSuggest(d prompt.Document) []prompt.Suggest {
return suggest.Completer(d, suggest.MustList(func() ([]string, error) {
d, err := kubectl.GetDeployment(c.Namespace, c.Deployment)
if err != nil {
return nil, err
}
pod, err := kubectl.GetMostRecentRunningPodBySelectors(c.Namespace, d.Spec.Selector.MatchLabels)
if err != nil {
return nil, err
}
ps, err := kubectl.ExecPod(c.Namespace, pod, c.Container, []string{"ps", "-o", "comm"}).Replace("COMMAND", "").String()
if err != nil {
return nil, err
}
return strings.Split(strings.Trim(ps, "\n"), "\n"), nil
}))
}

func (c Config) LaunchVscodeSuggest(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{{Text: "true"}, {Text: "false"}}
}
Expand All @@ -93,11 +129,15 @@ func (c Config) ImageSuggest(d prompt.Document) []prompt.Suggest {
return append(suggestions, prompt.Suggest{Text: defaultImage})
}

func (c Config) ArchSuggest(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{{Text: "amd64"}, {Text: "arm64"}}
}

func LoadConfig(path string) (Config, error) {
var c Config
if _, err := os.Stat(path); err != nil {
// needed due to panicking in ctrl+c binding (library limitation)
defer handleExit()
defer handlePromptExit()
// if the config path doesnt exist
// run configuration create with suggestions
gencon.New(
Expand Down Expand Up @@ -131,7 +171,7 @@ func promptExit(_ *prompt.Buffer) {
panic(Exit(0))
}

func handleExit() {
func handlePromptExit() {
v := recover()
switch v.(type) {
case nil:
Expand Down
1 change: 1 addition & 0 deletions delve.go
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ func (g Grapple) deployBin(ctx context.Context, pod, container, goModPath, sourc
func (g Grapple) portForwardDelve(l *logrus.Entry, ctx context.Context, pod, host string, port int) {
l.Info("port-forwarding pod for delve server")
cmd := g.kubeCmd.PortForwardPod(pod, host, port)
cmd.Stdout(os.Stdout)
go func() {
_, err := cmd.Run(ctx)
if err != nil && err.Error() != "signal: killed" {
Expand Down
2 changes: 1 addition & 1 deletion delve_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import (
const testNamespace = "test"

func testGrapple(t *testing.T, deployment string) *Grapple {
g, err := NewGrapple(logrus.NewEntry(logrus.StandardLogger()), testNamespace, deployment)
g, err := NewGrapple(logrus.NewEntry(logrus.StandardLogger()), testNamespace, deployment, false)
if err != nil {
t.Fatal(err)
}
Expand Down
5 changes: 3 additions & 2 deletions gograpple.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,11 @@ type Grapple struct {
kubeCmd *exec.KubectlCmd
dockerCmd *exec.DockerCmd
goCmd *exec.GoCmd
debug bool
}

func NewGrapple(l *logrus.Entry, namespace, deployment string) (*Grapple, error) {
g := &Grapple{l: l}
func NewGrapple(l *logrus.Entry, namespace, deployment string, debug bool) (*Grapple, error) {
g := &Grapple{l: l, debug: debug}
g.kubeCmd = exec.NewKubectlCommand()
g.dockerCmd = exec.NewDockerCommand()
g.goCmd = exec.NewGoCommand()
Expand Down
78 changes: 78 additions & 0 deletions kubectl/kubectl.go
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
package kubectl

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/bitfield/script"
"github.com/foomo/gograpple/log"
"github.com/foomo/gograpple/suggest"
"github.com/life4/genesis/slices"
"github.com/pkg/errors"
apps "k8s.io/api/apps/v1"
)

func Exists() bool {
Expand Down Expand Up @@ -124,6 +127,44 @@ func FilterImages(namespace, deployment string, filter func(s string) string) ([
return results, err
}

func ExecPod(namespace, pod, container string, cmd []string) *script.Pipe {
return script.Exec(fmt.Sprintf(
"kubectl -n %v exec %v -c %v -- %v", namespace, pod, container, strings.Join(cmd, " ")))
}

func GetDeployment(namespace, deployment string) (*apps.Deployment, error) {
out, err := script.Exec(fmt.Sprintf(
"kubectl -n %v get deployment %v -o json", namespace, deployment)).String()
if err != nil {
return nil, err
}
var d apps.Deployment
if err := json.Unmarshal([]byte(out), &d); err != nil {
return nil, err
}
return &d, nil
}

func GetMostRecentRunningPodBySelectors(namespace string, selectors map[string]string) (string, error) {
var selector []string
for k, v := range selectors {
selector = append(selector, fmt.Sprintf("%v=%v", k, v))
}
cmd := fmt.Sprintf(
"kubectl -n %v --selector %v get pods --field-selector=status.phase=Running --sort-by=.status.startTime -o name",
namespace, strings.Join(selector, ","))
pods, err := script.Exec(cmd).FilterLine(func(s string) string {
return strings.TrimLeft(s, "pod/")
}).Slice()
if err != nil {
return "", err
}
if len(pods) > 0 {
return pods[len(pods)-1], nil
}
return "", fmt.Errorf("no pods found")
}

func TempSwitchContext(context string, cb func() error) error {
currentCtx, err := GetCurrentContext()
if err != nil {
Expand All @@ -135,3 +176,40 @@ func TempSwitchContext(context string, cb func() error) error {
}
return cb()
}

func PortForwardPod(namespace, pod string, port int) error {
cmd := fmt.Sprintf("kubectl -n %v port-forward pods/%v %v:%v", namespace, pod, port, port)
_, err := script.Exec(cmd).WithStdout(log.Writer("kubectl")).Stdout()
// if err != nil {
// return errors.WithMessage(err, out)
// }
return err
}

func GetPIDsOf(namespace, pod, container, process string) (pids []string, err error) {
return ExecPod(namespace, pod, container, []string{"pidof", process}).Replace(" ", "\n").Slice()
}

func KillPidsOnPod(namespace, pod, container string, pids []string, murder bool) []error {
var errs []error
for _, pid := range pids {
cmd := []string{"kill"}
if murder {
cmd = append(cmd, "-s", "9")
}
cmd = append(cmd, pid)
_, err := ExecPod(namespace, pod, container, cmd).Stdout()
if err != nil {
errs = append(errs, err)
}
}
return errs
}

func CopyToPod(namespace, pod, container, source, destination string) error {
out, err := script.Exec(fmt.Sprintf("kubectl -n %v cp %v %v:%v -c %v", namespace, source, pod, destination, container)).String()
if err != nil {
return errors.WithMessage(err, out)
}
return nil
}
Loading