chore: reorganize, fix goreleaser and update readme

This commit is contained in:
Milos Pejanovic 2023-10-25 16:34:01 +02:00
parent e4b0f420fd
commit a3d29b4915
40 changed files with 260 additions and 281 deletions

View File

@ -2,11 +2,11 @@
# Build customization
builds:
- binary: gograpple
main: ./cmd/main.go
main: ./main.go
env:
- CGO_ENABLED=0
ldflags:
- -s -w -X github.com/foomo/gograpple/cmd/actions.version={{.Version}}
- -s -w -X github.com/foomo/gograpple/cmd.version={{.Version}}
goos:
- darwin
- linux

View File

@ -1,8 +1,8 @@
build:
go build -o bin/gograpple cmd/gograpple/main.go
go build -o bin/gograpple main.go
install:
go build -o /usr/local/bin/gograpple cmd/gograpple/main.go
go build -o /usr/local/bin/gograpple main.go
test:
go test ./...

View File

@ -3,9 +3,16 @@
gograpple that go program and delve into the high seas ...
or in other words: delve debugger injection for your golang code running in k8 pods
## requirements
- helm
- kubectl
- docker
## quick start
```
go install github.com/foomo/gograpple/cmd/gograpple@latest
brew install foomo/gograpple/gograpple
OR
go install github.com/foomo/gograpple@latest
```
start patch debugging in interactive mode
```
@ -15,6 +22,12 @@ when you configure your patch correctly a file will be saved in your cwd and the
## common issues
### stuck with patched deployment
in case your deployment is styck in patched state, use
```
gograpple rollback [namespace] [deployment]
```
### vscode
> The debug session doesnt start until the entrypoint is triggered more than once.

View File

@ -1,11 +1,11 @@
package actions
package cmd
import (
"fmt"
"strconv"
"strings"
"github.com/foomo/gograpple"
"github.com/foomo/gograpple/internal/grapple"
)
type HostPort struct {
@ -14,7 +14,7 @@ type HostPort struct {
}
func NewHostPort(host string, port int) *HostPort {
addr, err := gograpple.CheckTCPConnection(host, port)
addr, err := grapple.CheckTCPConnection(host, port)
if err == nil {
host = addr.IP.String()
port = addr.Port
@ -45,7 +45,7 @@ func (lf *HostPort) Set(value string) error {
default:
return fmt.Errorf("invalid address %q provided", value)
}
addr, err := gograpple.CheckTCPConnection(lf.Host, lf.Port)
addr, err := grapple.CheckTCPConnection(lf.Host, lf.Port)
if err != nil {
return err
}

View File

@ -1,124 +0,0 @@
package actions
import (
"github.com/foomo/gograpple"
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func init() {
rootCmd.PersistentFlags().StringVarP(&flagDir, "dir", "d", ".", "Specifies working directory")
rootCmd.PersistentFlags().StringVarP(&flagNamespace, "namespace", "n", "default", "namespace name")
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")
delveCmd.Flags().StringVar(&flagSourcePath, "source", "", ".go file source path (default cwd)")
delveCmd.Flags().Var(flagArgs, "args", "go file args")
delveCmd.Flags().Var(flagListen, "listen", "delve host:port to listen on")
delveCmd.Flags().BoolVar(&flagVscode, "vscode", false, "launch a debug configuration in vscode")
delveCmd.Flags().BoolVar(&flagContinue, "continue", false, "start delve server execution without waiting for client connection")
delveCmd.Flags().BoolVar(&flagJSONLog, "json-log", false, "log as json")
interactiveCmd.Flags().BoolVar(&flagAttach, "attach", false, "debug with attach (default will patch)")
interactiveCmd.Flags().StringVar(&flagSaveDir, "save", ".", "directory to save interactive configuration")
rootCmd.AddCommand(versionCmd, patchCmd, shellCmd, delveCmd, interactiveCmd)
}
var (
flagImage string
flagDir string
flagVerbose bool
flagNamespace string
flagPod string
flagContainer string
flagRepo string
flagMounts []string
flagSourcePath string
flagArgs = NewStringList(" ")
flagRollback bool
flagListen = NewHostPort("127.0.0.1", 0)
flagVscode bool
flagContinue bool
flagJSONLog bool
flagDebug bool
)
var (
l *logrus.Entry
grapple *gograpple.Grapple
rootCmd = &cobra.Command{
Use: "gograpple",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if cmd.Name() == commandNameVersion || cmd.Name() == commandNameInteractive {
return nil
}
l = newLogger(flagVerbose, flagJSONLog)
var err error
err = gograpple.ValidatePath(".", &flagDir)
if err != nil {
return err
}
grapple, err = gograpple.NewGrapple(l, flagNamespace, args[0], flagDebug)
if err != nil {
return err
}
return gograpple.ValidatePath(flagDir, &flagSourcePath)
},
}
patchCmd = &cobra.Command{
Use: "patch [DEPLOYMENT] -c {CONTAINER} -n {NAMESPACE} -i {IMAGE} -t {TAG} -m {MOUNT}",
Short: "applies a development patch for a deployment",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
if flagRollback {
return grapple.Rollback()
}
mounts, err := gograpple.ValidateMounts(flagDir, flagMounts)
if err != nil {
return err
}
return grapple.Patch(flagImage, flagContainer, mounts)
},
}
shellCmd = &cobra.Command{
Use: "shell [DEPLOYMENT] -n {NAMESPACE} -c {CONTAINER}",
Short: "shell into the dev patched deployment",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return grapple.Shell(flagPod)
},
}
delveCmd = &cobra.Command{
Use: "delve [DEPLOYMENT] --source {SRC} -n {NAMESPACE} -c {CONTAINER}",
Short: "start a headless delve debug server for .go input on a patched deployment",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
return grapple.Delve(flagPod, flagContainer, flagSourcePath, flagArgs.items, flagListen.Host, flagListen.Port, flagVscode, flagContinue)
},
}
)
func Execute() {
if err := rootCmd.Execute(); err != nil {
l = logrus.NewEntry(logrus.New())
l.Fatal(err)
}
}
func newLogger(verbose, jsonLog bool) *logrus.Entry {
logger := logrus.New()
if jsonLog {
logger.SetFormatter(&logrus.JSONFormatter{
DisableTimestamp: true,
})
}
if verbose {
logger.SetLevel(logrus.TraceLevel)
}
return logrus.NewEntry(logger)
}

View File

@ -1,22 +0,0 @@
package actions
import (
"fmt"
"github.com/spf13/cobra"
)
var version = "latest"
const commandNameVersion = "version"
var (
versionCmd = &cobra.Command{
Use: commandNameVersion,
Short: "prints cli version",
Long: "prints the current installed cli version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(version)
},
}
)

View File

@ -1,7 +0,0 @@
package main
import "github.com/foomo/gograpple/cmd/gograpple/actions"
func main() {
actions.Execute()
}

View File

@ -1,15 +1,19 @@
package actions
package cmd
import (
"path"
"github.com/foomo/gograpple"
"github.com/foomo/gograpple/config"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/internal/config"
"github.com/foomo/gograpple/internal/grapple"
"github.com/foomo/gograpple/internal/kubectl"
"github.com/spf13/cobra"
)
const commandNameInteractive = "interactive"
func init() {
interactiveCmd.Flags().BoolVar(&flagAttach, "attach", false, "debug with attach (default will patch)")
interactiveCmd.Flags().StringVar(&flagSaveDir, "save", ".", "directory to save interactive configuration")
rootCmd.AddCommand(interactiveCmd)
}
var (
flagAttach bool
@ -37,7 +41,7 @@ func attachDebug(baseDir string) error {
if err != nil {
return err
}
g, err := gograpple.NewGrapple(newLogger(flagVerbose, flagJSONLog), c.Namespace, c.Deployment, flagDebug)
g, err := grapple.NewGrapple(newLogEntry(flagDebug), c.Namespace, c.Deployment)
if err != nil {
return err
}
@ -48,7 +52,7 @@ func attachDebug(baseDir string) error {
if err := kubectl.SetContext(c.Cluster); err != nil {
return err
}
return g.Attach(c.Namespace, c.Deployment, c.Container, c.AttachTo, c.Arch, host, port)
return g.Attach(c.Namespace, c.Deployment, c.Container, c.AttachTo, c.Arch, host, port, flagDebug)
}
func patchDebug(baseDir string) error {
@ -64,7 +68,7 @@ func patchDebug(baseDir string) error {
if &c == nil {
return nil
}
g, err := gograpple.NewGrapple(newLogger(flagVerbose, flagJSONLog), c.Namespace, c.Deployment, flagDebug)
g, err := grapple.NewGrapple(newLogEntry(flagDebug), c.Namespace, c.Deployment)
if err != nil {
return err
}

25
cmd/rollback.go Normal file
View File

@ -0,0 +1,25 @@
package cmd
import (
"github.com/foomo/gograpple/internal/grapple"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(rollbackCmd)
}
var (
rollbackCmd = &cobra.Command{
Use: "rollback [namespace] [deployment]",
Short: "rollback the patched deployment",
Args: cobra.MinimumNArgs(2),
RunE: func(cmd *cobra.Command, args []string) error {
g, err := grapple.NewGrapple(newLogEntry(flagDebug), args[0], args[1])
if err != nil {
return err
}
return g.Rollback()
},
}
)

50
cmd/root.go Normal file
View File

@ -0,0 +1,50 @@
package cmd
import (
"github.com/sirupsen/logrus"
"github.com/spf13/cobra"
)
func init() {
rootCmd.PersistentFlags().BoolVarP(&flagDebug, "debug", "", false, "debug mode")
}
var (
// flagImage string
// flagDir string
flagDebug bool
// flagNamespace string
// flagPod string
// flagContainer string
// flagRepo string
// flagMounts []string
// flagSourcePath string
// flagArgs = NewStringList(" ")
// flagRollback bool
// flagListen = NewHostPort("127.0.0.1", 0)
// flagVscode bool
// flagContinue bool
// flagJSONLog bool
// flagDebug bool
)
var (
rootCmd = &cobra.Command{
Use: "gograpple",
}
)
func Execute() {
if err := rootCmd.Execute(); err != nil {
le := newLogEntry(flagDebug)
le.Fatal(err)
}
}
func newLogEntry(debug bool) *logrus.Entry {
logger := logrus.New()
if debug {
logger.SetLevel(logrus.TraceLevel)
}
return logrus.NewEntry(logger)
}

32
cmd/version.go Normal file
View File

@ -0,0 +1,32 @@
package cmd
import (
"fmt"
"github.com/spf13/cobra"
)
func init() {
rootCmd.AddCommand(versionCmd)
}
// set on build
var version = ""
var (
versionCmd = &cobra.Command{
Use: "version",
Short: "prints cli version",
Long: "prints the current installed cli version",
Run: func(cmd *cobra.Command, args []string) {
fmt.Println(getVersion())
},
}
)
func getVersion() string {
if version == "" {
return "latest"
}
return version
}

View File

@ -8,8 +8,8 @@ import (
"strings"
"github.com/c-bata/go-prompt"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/suggest"
"github.com/foomo/gograpple/internal/kubectl"
"github.com/foomo/gograpple/internal/suggest"
"gopkg.in/yaml.v3"
)

View File

@ -8,8 +8,8 @@ import (
"strings"
"github.com/c-bata/go-prompt"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/suggest"
"github.com/foomo/gograpple/internal/kubectl"
"github.com/foomo/gograpple/internal/suggest"
"gopkg.in/yaml.v3"
)

View File

@ -5,7 +5,7 @@ import (
"fmt"
"os"
"github.com/foomo/gograpple/exec"
"github.com/foomo/gograpple/internal/exec"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
)

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"fmt"
@ -7,12 +7,12 @@ import (
"runtime"
"github.com/bitfield/script"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/log"
"github.com/foomo/gograpple/internal/kubectl"
"github.com/foomo/gograpple/internal/log"
"github.com/pkg/errors"
)
func (g Grapple) Attach(namespace, deployment, container, bin, arch, host string, port int) error {
func (g Grapple) Attach(namespace, deployment, container, bin, arch, host string, port int, debug bool) error {
pod, err := kubectl.GetMostRecentRunningPodBySelectors(namespace, g.deployment.Spec.Selector.MatchLabels)
if err != nil {
return err
@ -35,7 +35,7 @@ func (g Grapple) Attach(namespace, deployment, container, bin, arch, host string
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)
go attachDelveOnPod(namespace, pod, container, dlvDest, pids[0], host, port, debug)
// launchVSCode(context.Background(), g.l, "./test/app", "", port, 3)
return kubectl.PortForwardPod(namespace, pod, port)
}

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"context"
@ -7,8 +7,9 @@ import (
"path"
"time"
"github.com/foomo/gograpple/delve"
"github.com/foomo/gograpple/exec"
"github.com/foomo/gograpple/internal/delve"
"github.com/foomo/gograpple/internal/exec"
"github.com/foomo/gograpple/util"
"github.com/sirupsen/logrus"
)
@ -41,7 +42,7 @@ func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host
return fmt.Errorf("couldnt find go.mod path for source %q", sourcePath)
}
RunWithInterrupt(g.l, func(ctx context.Context) {
util.RunWithInterrupt(g.l, func(ctx context.Context) {
g.l.Infof("waiting for deployment to get ready")
_, err := g.kubeCmd.WaitForRollout(g.deployment.Name, defaultWaitTimeout).Run(ctx)
if err != nil {

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"net"
@ -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, false)
g, err := NewGrapple(logrus.NewEntry(logrus.StandardLogger()), testNamespace, deployment)
if err != nil {
t.Fatal(err)
}

View File

@ -1,9 +1,9 @@
package gograpple
package grapple
import (
"context"
"github.com/foomo/gograpple/exec"
"github.com/foomo/gograpple/internal/exec"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/apps/v1"
)
@ -30,11 +30,10 @@ type Grapple struct {
kubeCmd *exec.KubectlCmd
dockerCmd *exec.DockerCmd
goCmd *exec.GoCmd
debug bool
}
func NewGrapple(l *logrus.Entry, namespace, deployment string, debug bool) (*Grapple, error) {
g := &Grapple{l: l, debug: debug}
func NewGrapple(l *logrus.Entry, namespace, deployment string) (*Grapple, error) {
g := &Grapple{l: l}
g.kubeCmd = exec.NewKubectlCommand()
g.dockerCmd = exec.NewDockerCommand()
g.goCmd = exec.NewGoCommand()

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"context"
@ -9,6 +9,7 @@ import (
"path"
"path/filepath"
"github.com/foomo/gograpple/util"
"github.com/pkg/errors"
)
@ -109,7 +110,7 @@ func (g Grapple) Patch(image, container string, mounts []Mount) error {
return err
}
// get repo from deployment image
imageRepo, name, tag, err := ParseImage(deploymentImage)
imageRepo, name, tag, err := util.ParseImage(deploymentImage)
if err != nil {
return err
}

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"testing"

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"context"

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"bytes"
@ -7,13 +7,8 @@ import (
"net"
"os"
"path/filepath"
"runtime"
"strings"
"text/template"
"time"
"github.com/foomo/gograpple/exec"
"github.com/sirupsen/logrus"
)
func FindFreePort(host string) (int, error) {
@ -37,21 +32,6 @@ func CheckTCPConnection(host string, port int) (*net.TCPAddr, error) {
return l.Addr().(*net.TCPAddr), nil
}
func Open(l *logrus.Entry, ctx context.Context, path string) (string, error) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "linux":
cmd = exec.NewCommand("xdg-open").Logger(l).Args(path)
case "windows":
cmd = exec.NewCommand("rundll32").Logger(l).Args("url.dll,FileProtocolHandler", path)
case "darwin":
cmd = exec.NewCommand("open").Logger(l).Args(path)
default:
return "", fmt.Errorf("unsupported platform")
}
return cmd.Run(ctx)
}
func TryCall(tries int, waitBetweenAttempts time.Duration, f func(i int) error) error {
var err error
for i := 1; i < tries+1; i++ {
@ -129,24 +109,3 @@ func stringIsInSlice(a string, list []string) bool {
}
return false
}
func GetPlatformInfo(platform string) (os, arch string, err error) {
pieces := strings.Split(platform, "/")
if len(pieces) != 2 {
return os, arch, fmt.Errorf("invalid format for platform %q", platform)
}
return pieces[0], pieces[1], nil
}
func ParseImage(s string) (repo, name, tag string, err error) {
pieces := strings.Split(s, "/")
switch true {
case len(pieces) == 1 && pieces[0] == s:
imageTag := strings.Split(s, ":")
return "", imageTag[0], imageTag[1], nil
case len(pieces) > 1:
imageTag := strings.Split(pieces[len(pieces)-1], ":")
return strings.Join(pieces[:len(pieces)-1], "/"), imageTag[0], imageTag[1], nil
}
return "", "", "", fmt.Errorf("invalid image value %q provided", s)
}

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"fmt"

View File

@ -1,4 +1,4 @@
package gograpple
package grapple
import (
"context"
@ -10,7 +10,8 @@ import (
"strings"
"time"
"github.com/foomo/gograpple/exec"
"github.com/foomo/gograpple/internal/exec"
"github.com/foomo/gograpple/util"
"github.com/sirupsen/logrus"
)
@ -79,7 +80,7 @@ func launchVSCode(ctx context.Context, l *logrus.Entry, goModDir, host string, p
if err != nil {
return err
}
_, err = Open(l, ctx, `vscode://fabiospampinato.vscode-debug-launcher/launch?args=`+url.QueryEscape(la))
_, err = util.Open(l, ctx, `vscode://fabiospampinato.vscode-debug-launcher/launch?args=`+url.QueryEscape(la))
if err != nil {
return err
}

View File

@ -7,8 +7,8 @@ import (
"strings"
"github.com/bitfield/script"
"github.com/foomo/gograpple/log"
"github.com/foomo/gograpple/suggest"
"github.com/foomo/gograpple/internal/log"
"github.com/foomo/gograpple/internal/suggest"
"github.com/life4/genesis/slices"
"github.com/pkg/errors"
apps "k8s.io/api/apps/v1"

View File

@ -1,38 +0,0 @@
package gograpple
import (
"context"
"os"
"os/signal"
"time"
"github.com/sirupsen/logrus"
)
func RunWithInterrupt(l *logrus.Entry, callback func(ctx context.Context)) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
durReload := 3 * time.Second
for {
ctx, cancelCtx := context.WithCancel(context.Background())
// do stuff
go callback(ctx)
select {
case <-signalChan: // first signal
l.Info("-")
l.Infof("interrupt received, trigger one more within %v to terminate", durReload)
cancelCtx()
select {
case <-time.After(durReload): // reloads durReload after first signal
l.Info("-")
l.Info("reloading")
case <-signalChan: // second signal, hard exit
l.Info("-")
l.Info("terminating")
signal.Stop(signalChan)
// exit loop
return
}
}
}
}

7
main.go Normal file
View File

@ -0,0 +1,7 @@
package main
import "github.com/foomo/gograpple/cmd"
func main() {
cmd.Execute()
}

78
util/util.go Normal file
View File

@ -0,0 +1,78 @@
package util
import (
"context"
"fmt"
"os"
"os/signal"
"runtime"
"strings"
"time"
"github.com/foomo/gograpple/internal/exec"
"github.com/sirupsen/logrus"
)
func RunWithInterrupt(l *logrus.Entry, callback func(ctx context.Context)) {
signalChan := make(chan os.Signal, 1)
signal.Notify(signalChan, os.Interrupt)
durReload := 3 * time.Second
for {
ctx, cancelCtx := context.WithCancel(context.Background())
// do stuff
go callback(ctx)
select {
case <-signalChan: // first signal
l.Info("-")
l.Infof("interrupt received, trigger one more within %v to terminate", durReload)
cancelCtx()
select {
case <-time.After(durReload): // reloads durReload after first signal
l.Info("-")
l.Info("reloading")
case <-signalChan: // second signal, hard exit
l.Info("-")
l.Info("terminating")
signal.Stop(signalChan)
// exit loop
return
}
}
}
}
func Open(l *logrus.Entry, ctx context.Context, path string) (string, error) {
var cmd *exec.Cmd
switch runtime.GOOS {
case "linux":
cmd = exec.NewCommand("xdg-open").Logger(l).Args(path)
case "windows":
cmd = exec.NewCommand("rundll32").Logger(l).Args("url.dll,FileProtocolHandler", path)
case "darwin":
cmd = exec.NewCommand("open").Logger(l).Args(path)
default:
return "", fmt.Errorf("unsupported platform")
}
return cmd.Run(ctx)
}
func GetPlatformInfo(platform string) (os, arch string, err error) {
pieces := strings.Split(platform, "/")
if len(pieces) != 2 {
return os, arch, fmt.Errorf("invalid format for platform %q", platform)
}
return pieces[0], pieces[1], nil
}
func ParseImage(s string) (repo, name, tag string, err error) {
pieces := strings.Split(s, "/")
switch true {
case len(pieces) == 1 && pieces[0] == s:
imageTag := strings.Split(s, ":")
return "", imageTag[0], imageTag[1], nil
case len(pieces) > 1:
imageTag := strings.Split(pieces[len(pieces)-1], ":")
return strings.Join(pieces[:len(pieces)-1], "/"), imageTag[0], imageTag[1], nil
}
return "", "", "", fmt.Errorf("invalid image value %q provided", s)
}