Skip to content

Commit

Permalink
Merge pull request #6 from foomo/feature/config-interactive
Browse files Browse the repository at this point in the history
feature/config-interactive
  • Loading branch information
runz0rd authored Jan 26, 2023
2 parents db6d512 + 011d583 commit 13c5e6b
Show file tree
Hide file tree
Showing 27 changed files with 711 additions and 103 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@

.todo
.idea/
.vscode/

bin/
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,10 @@

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

## common issues

### vscode
> The debug session doesnt start until the entrypoint is triggered more than once.
Review and remove any extra breakpoints you may have enabled, that you dont need (Run and Debug > Breakpoints panel). Vscode seems to like them saved across projects.
Binary file removed app
Binary file not shown.
40 changes: 40 additions & 0 deletions cmd/actions/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package actions

import (
"github.com/foomo/gograpple"
"github.com/foomo/gograpple/kubectl"
"github.com/spf13/cobra"
)

const commandNameConfig = "config"

var (
configCmd = &cobra.Command{
Use: "config [PATH]",
Short: "load/create config and run patch and delve",
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
c, err := gograpple.LoadConfig(args[0])
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 {
return err
}
g, err := gograpple.NewGrapple(newLogger(flagVerbose, flagJSONLog), c.Namespace, c.Deployment)
if err != nil {
return err
}
if err := g.Patch(c.Repository, 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, addr.Host, addr.Port, c.LaunchVscode, c.DelveContinue)
},
}
)
25 changes: 18 additions & 7 deletions cmd/actions/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,26 @@ func NewHostPort(host string, port int) *HostPort {

func (lf *HostPort) Set(value string) error {
pieces := strings.Split(value, ":")
if pieces[0] != "" {
switch true {
case len(pieces) == 1 && pieces[0] != value:
lf.Host = "127.0.0.1"
var err error
lf.Port, err = strconv.Atoi(pieces[0])
if err != nil {
return err
}
case len(pieces) == 2:
lf.Host = pieces[0]
}
var err error
if len(pieces) == 2 && pieces[1] != "" {
if pieces[0] == "" {
lf.Host = "127.0.0.1"
}
var err error
lf.Port, err = strconv.Atoi(pieces[1])
}
if err != nil {
return err
if err != nil {
return err
}
default:
return fmt.Errorf("invalid address %q provided", value)
}
addr, err := gograpple.CheckTCPConnection(lf.Host, lf.Port)
if err != nil {
Expand Down
18 changes: 9 additions & 9 deletions cmd/actions/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,39 +8,39 @@ import (

func init() {

rootCmd.PersistentFlags().StringVarP(&flagTag, "tag", "t", "latest", "Specifies the image tag")
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)")
patchCmd.Flags().StringVarP(&flagImage, "image", "i", "", "image to be used for patching (default deployment image)")
patchCmd.Flags().StringVar(&flagImage, "image", "alpine:latest", "image to be used for patching (default alpine:latest)")
patchCmd.Flags().StringVarP(&flagRepo, "repo", "r", "", "repository to be used for pushing patched image (default none)")
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")
delveCmd.Flags().BoolVar(&flagContinue, "continue", false, "start delve server execution without wiating for client connection")
rootCmd.AddCommand(versionCmd, patchCmd, shellCmd, delveCmd)
rootCmd.AddCommand(versionCmd, patchCmd, shellCmd, delveCmd, configCmd)
}

var (
flagTag string
flagImage string
flagDir string
flagVerbose bool
flagNamespace string
flagPod string
flagContainer string
flagImage string
flagRepo string
flagMounts []string
flagSourcePath string
flagArgs = NewStringList(" ")
flagRollback bool
flagContinue bool
flagListen = NewHostPort("127.0.0.1", 0)
flagVscode bool
flagContinue bool
flagJSONLog bool
)

Expand All @@ -51,7 +51,7 @@ var (
rootCmd = &cobra.Command{
Use: "gograpple",
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
if cmd.Name() == commandNameVersion {
if cmd.Name() == commandNameVersion || cmd.Name() == commandNameConfig {
return nil
}
l = newLogger(flagVerbose, flagJSONLog)
Expand Down Expand Up @@ -79,7 +79,7 @@ var (
if err != nil {
return err
}
return grapple.Patch(flagImage, flagTag, flagContainer, mounts)
return grapple.Patch(flagRepo, flagImage, flagContainer, mounts)
},
}
shellCmd = &cobra.Command{
Expand Down
166 changes: 166 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
package gograpple

import (
"fmt"
"io/ioutil"
"os"
"path"
"strings"

"github.com/c-bata/go-prompt"
"github.com/c-bata/go-prompt/completer"
"github.com/foomo/gograpple/kubectl"
"github.com/foomo/gograpple/suggest"
"github.com/runz0rd/gencon"
"gopkg.in/yaml.v3"
)

type Config struct {
SourcePath string `yaml:"source_path"`
Cluster string `yaml:"cluster"`
Namespace string `yaml:"namespace" depends:"Cluster"`
Deployment string `yaml:"deployment" depends:"Namespace"`
Container string `yaml:"container,omitempty" depends:"Deployment"`
Repository string `yaml:"repository,omitempty" depends:"Deployment"`
LaunchVscode bool `yaml:"launch_vscode"`
ListenAddr string `yaml:"listen_addr,omitempty"`
DelveContinue bool `yaml:"delve_continue"`
Image string `yaml:"image,omitempty"`
}

func (c Config) MarshalYAML() (interface{}, error) {
// marshal relative paths into absolute
cwd, err := os.Getwd()
if err != nil {
return nil, err
}
c.SourcePath = path.Join(cwd, c.SourcePath)
type alias Config
node := yaml.Node{}
err = node.Encode(alias(c))
if err != nil {
return nil, err
}
return node, nil
}

func (c Config) SourcePathSuggest(d prompt.Document) []prompt.Suggest {
completer := completer.FilePathCompleter{
IgnoreCase: true,
Filter: func(fi os.FileInfo) bool {
return fi.IsDir() || strings.HasSuffix(fi.Name(), ".go")
},
}
return completer.Complete(d)
}

func (c Config) ClusterSuggest(d prompt.Document) []prompt.Suggest {
return suggest.Completer(d, suggest.MustList(kubectl.ListContexts))
}

func (c Config) NamespaceSuggest(d prompt.Document) []prompt.Suggest {
kubectl.SetContext(c.Cluster)
return suggest.Completer(d, suggest.MustList(kubectl.ListNamespaces))
}

func (c Config) DeploymentSuggest(d prompt.Document) []prompt.Suggest {
return suggest.Completer(d, suggest.MustList(func() ([]string, error) {
return kubectl.ListDeployments(c.Namespace)
}))
}

func (c Config) ContainerSuggest(d prompt.Document) []prompt.Suggest {
return suggest.Completer(d, suggest.MustList(func() ([]string, error) {
return kubectl.ListContainers(c.Namespace, c.Deployment)
}))
}

func (c Config) RepositorySuggest(d prompt.Document) []prompt.Suggest {
return suggest.Completer(d, suggest.MustList(func() ([]string, error) {
return kubectl.ListRepositories(c.Namespace, c.Deployment)
}))
}

func (c Config) LaunchVscodeSuggest(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{{Text: "true"}, {Text: "false"}}
}

func (c Config) ListenAddrSuggest(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{{Text: ":2345"}}
}

func (c Config) DelveContinueSuggest(d prompt.Document) []prompt.Suggest {
return []prompt.Suggest{{Text: "true"}, {Text: "false"}}
}

func (c Config) DockerfileSuggest(d prompt.Document) []prompt.Suggest {
completer := completer.FilePathCompleter{
IgnoreCase: true,
Filter: func(fi os.FileInfo) bool {
return fi.IsDir() || strings.Contains(fi.Name(), "Dockerfile")
},
}
return completer.Complete(d)
}

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

func (c Config) ImageSuggest(d prompt.Document) []prompt.Suggest {
suggestions := suggest.Completer(d, suggest.MustList(func() ([]string, error) {
return kubectl.ListImages(c.Namespace, c.Deployment)
}))
return append(suggestions, prompt.Suggest{Text: defaultImage})
}

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()
// if the config path doesnt exist
// run configuration create with suggestions
gencon.New(
prompt.OptionShowCompletionAtStart(),
prompt.OptionPrefixTextColor(prompt.Fuchsia),
// since we have a file completer
prompt.OptionCompletionWordSeparator("/"),
// handle ctrl+c exit
prompt.OptionAddKeyBind(prompt.KeyBind{
Key: prompt.ControlC,
Fn: promptExit,
}),
).Run(&c)
// save yaml file
data, err := yaml.Marshal(c)
if err != nil {
return c, err
}
err = ioutil.WriteFile(path, data, 0644)
if err != nil {
return c, err
}
}
err := LoadYaml(path, &c)
return c, err
}

type Exit int

func promptExit(_ *prompt.Buffer) {
panic(Exit(0))
}

func handleExit() {
v := recover()
switch v.(type) {
case nil:
return
case Exit:
vInt, _ := v.(int)
os.Exit(vInt)
default:
fmt.Printf("%+v", v)
}
}
35 changes: 22 additions & 13 deletions delve.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,15 @@ const delveBin = "dlv"

func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host string,
port int, vscode, delveContinue bool) error {
validateCtx := context.Background()
// validate k8s resources for delve session
if err := g.kubeCmd.ValidatePod(validateCtx, g.deployment, &pod); err != nil {
return err
}
if err := g.kubeCmd.ValidateContainer(g.deployment, &container); err != nil {
return err
}
ctx := context.Background()
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(validateCtx, g.DeploymentConfigMapName(),
d, err := g.kubeCmd.GetDeploymentFromConfigMap(ctx, g.DeploymentConfigMapName(),
defaultConfigMapDeploymentKey)
if err != nil {
return err
Expand All @@ -48,6 +42,22 @@ func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host
}

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 {
g.l.Error(err)
return
}
// validate and get k8s resources for delve session
if err := g.kubeCmd.ValidatePod(context.Background(), g.deployment, &pod); err != nil {
g.l.Error(err)
return
}
if err := g.kubeCmd.ValidateContainer(g.deployment, &container); err != nil {
g.l.Error(err)
return
}

// run pre-start cleanup
clog := g.componentLog("cleanup")
clog.Info("running pre-start cleanup")
Expand All @@ -59,7 +69,7 @@ func (g Grapple) Delve(pod, container, sourcePath string, binArgs []string, host
// deploy bin
dlog := g.componentLog("deploy")
dlog.Info("building and deploying bin")
// get image used in the deployment so we can and platform
// get image used in the deployment so we can get platform
deploymentImage, err := g.kubeCmd.GetImage(ctx, g.deployment, container)
if err != nil {
dlog.Error(err)
Expand Down Expand Up @@ -136,9 +146,8 @@ func (g Grapple) cleanupPIDs(ctx context.Context, pod, container string) error {
func (g Grapple) deployBin(ctx context.Context, pod, container, goModPath, sourcePath string, p *exec.Platform) error {
// build bin
binSource := path.Join(os.TempDir(), g.binName())
_, err := g.goCmd.Build(binSource, []string{"."}, "-gcflags", "-N -l").
Env(fmt.Sprintf("GOOS=%v", p.OS), fmt.Sprintf("GOARCH=%v", p.Arch)).
Cwd(sourcePath).Run(ctx)
_, err := g.goCmd.Build(binSource, []string{sourcePath}, "-gcflags", "-N -l").
Env(fmt.Sprintf("GOOS=%v", p.OS), fmt.Sprintf("GOARCH=%v", p.Arch), fmt.Sprintf("CGO_ENABLED=%v", 0)).Run(ctx)
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit 13c5e6b

Please sign in to comment.