Skip to content

Commit

Permalink
initial commit (#1)
Browse files Browse the repository at this point in the history
  • Loading branch information
cbuto committed Jul 30, 2023
1 parent 55d959d commit 7b208e9
Show file tree
Hide file tree
Showing 12 changed files with 1,002 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea/

dist/
36 changes: 36 additions & 0 deletions .goreleaser.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
before:
hooks:
- go mod tidy
builds:
- main: ./cmd/
env:
- CGO_ENABLED=0
goos:
- linux
- windows
- darwin

archives:
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of uname.
name_template: >-
{{ .ProjectName }}_
{{- title .Os }}_
{{- if eq .Arch "amd64" }}x86_64
{{- else if eq .Arch "386" }}i386
{{- else }}{{ .Arch }}{{ end }}
{{- if .Arm }}v{{ .Arm }}{{ end }}
# use zip for windows archives
format_overrides:
- goos: windows
format: zip
checksum:
name_template: 'checksums.txt'
snapshot:
name_template: "{{ incpatch .Version }}-next"
changelog:
sort: asc
filters:
exclude:
- '^docs:'
- '^test:'
1 change: 1 addition & 0 deletions .tool-versions
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
golang 1.20
33 changes: 33 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# kubectl pprof plugin

A simple kubectl plugin to collect Go pprof profiles from pods that
expose the ["net/http/pprof"](https://pkg.go.dev/net/http/pprof) endpoints on a local port.

The plugin will port-forward to the specified pod and write the pprof profile to the filesystem
to be analyze with `go tool pprof`.

## Installing

1. Download the binary from the GH release
2. Move the `kubectl-pprof` binary to anywhere in your `$PATH`
3. Run `kubectl pprof -h` to validate the plugin is working

## Example usage

### Collecting a CPU profile

```bash
kubectl pprof <pod> --port 8080 -n <namespace> --profile cpu
```

### Collecting a heap profile for 30 seconds

```bash
kubectl pprof <pod> --port 8080 -n <namespace> --profile heap --seconds 30 --output /tmp/
```

### Pass a profile directly to `go tool pprof` (suppress output with `-q`)

```bash
kubectl pprof <pod> --port 8080 -n <namespace> --profile cpu -q | xargs go tool pprof -http=:8080
```
195 changes: 195 additions & 0 deletions cmd/pprof/pprof.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
package pprof

import (
"context"
"fmt"
"net/url"
"os"
"path"
"runtime/pprof"
"time"

"github.com/spf13/cobra"
"k8s.io/cli-runtime/pkg/genericclioptions"
"k8s.io/cli-runtime/pkg/genericiooptions"
"k8s.io/client-go/rest"

"github.com/cbuto/kubectl-pprof/internal/portforward"
"github.com/cbuto/kubectl-pprof/internal/pprofgetter"
)

var pprofExample = `
# collect a heap profile a pod
%[1]s pprof <pod name> --profile heap --seconds 10
# collect a cpu profile and output profile to /tmp/
%[1]s pprof <pod name> --port 8080 -n <namespace> --profile cpu --output /tmp/ --seconds 30
# pass a profile directly to go tool pprof (suppress output with -q)
%[1]s pprof <pod> --port 8080 -n <namespace> --profile cpu -q | xargs go tool pprof -http=:8080
`

const (
DefaultProfileSeconds = 10
DefaultPProfPort = 8080
)

type pprofCmdOptions struct {
genericiooptions.IOStreams
restConfig *rest.Config
configFlags *genericclioptions.ConfigFlags
profile string
seconds int
port int
outDir string
namespace string
pod string
quiet bool
}

func newPProfOptions(streams genericiooptions.IOStreams) *pprofCmdOptions {
return &pprofCmdOptions{
configFlags: genericclioptions.NewConfigFlags(true),
IOStreams: streams,
}
}

func NewPProfCmd(streams genericiooptions.IOStreams) *cobra.Command {
opts := newPProfOptions(streams)

cmd := &cobra.Command{
Use: "pprof [pod] [flags]",
Short: "Collects the specified pprof profile from a pod",
Example: fmt.Sprintf(pprofExample, "kubectl"),
SilenceUsage: true,
RunE: func(c *cobra.Command, args []string) error {
if err := opts.Complete(c, args); err != nil {
return err
}
if err := opts.Validate(); err != nil {
return err
}
if err := opts.Run(); err != nil {
return err
}

return nil
},
}

cmd.Flags().BoolVarP(&opts.quiet, "quiet", "q", false, "suppresses output and only prints the output file")
cmd.Flags().StringVar(&opts.profile,
"profile",
"cpu", "type of profile to collect (heap, cpu, block, goroutine, mutex, or threadcreate)")
cmd.Flags().StringVar(&opts.outDir, "output", "./", "path to a directory to write the profile")
cmd.Flags().IntVar(&opts.seconds, "seconds", DefaultProfileSeconds, "amount of seconds to collect the profile")
cmd.Flags().IntVar(&opts.port, "port", DefaultPProfPort, "pprof port")

opts.configFlags.AddFlags(cmd.Flags())

return cmd
}

func (o *pprofCmdOptions) Complete(cmd *cobra.Command, args []string) error {
var err error
o.namespace, _, err = o.configFlags.ToRawKubeConfigLoader().Namespace()

if err != nil {
return fmt.Errorf("failed to get namespace from config: %w", err)
}

if len(args) != 1 {
return fmt.Errorf("invalid number of arguments, use --help to see example usage")
}

o.pod = args[0]
o.restConfig, err = o.configFlags.ToRESTConfig()

if err != nil {
return fmt.Errorf("failed to get REST config: %w", err)
}

return nil
}

func (o *pprofCmdOptions) Validate() error {
if o.profile == "" {
return fmt.Errorf("must select type of profile to collect")
}

if o.profile != "cpu" {
profile := pprof.Lookup(o.profile)
if profile == nil {
return fmt.Errorf("unknown profile: %s", o.profile)
}
}

fileInfo, err := os.Stat(o.outDir)

if err != nil {
return fmt.Errorf("failed to check if --output is a dir: %w", err)
}

if !fileInfo.IsDir() {
return fmt.Errorf("--output flag must be set to a directory: %s", o.outDir)
}

return nil
}

func (o *pprofCmdOptions) Run() error {
var err error

ctx := context.TODO()

out, errOut := o.IOStreams.Out, o.IOStreams.ErrOut
if o.quiet {
out, errOut = nil, nil
}

portforwarder, err := portforward.NewPortForward(
portforward.WithRESTConfig(o.restConfig),
portforward.WithOutput(out, errOut))
if err != nil {
return fmt.Errorf("failed to get port-forwarder: %w", err)
}

localPort, stopCh, err := portforwarder.PortForward(ctx, o.namespace, o.pod, o.port)

if err != nil {
return fmt.Errorf("port-forwarding failed: %w", err)
}

defer close(stopCh)

if !o.quiet {
_, err = o.IOStreams.Out.Write([]byte(fmt.Sprintf("collecting profile from %s (for %d secs)...\n", o.pod, o.seconds)))
if err != nil {
return fmt.Errorf("failed to write to output: %w", err)
}
}

outFilePath := path.Join(o.outDir, fmt.Sprintf("%s_%s_%d.out", o.pod, o.profile, time.Now().Unix()))
pprofHostURL := &url.URL{
Scheme: "http",
Host: fmt.Sprintf("localhost:%d", localPort),
}

getter, err := pprofgetter.NewPProfGetter(pprofgetter.WithHostURL(pprofHostURL))
if err != nil {
return fmt.Errorf("failed to build pprof getter: %w", err)
}

err = getter.Get(ctx, o.profile, o.seconds, outFilePath)
if err != nil {
return fmt.Errorf("failed to collect profile: %w", err)
}

_, err = o.IOStreams.Out.Write([]byte(fmt.Sprintf("%s\n", outFilePath)))
if err != nil {
return fmt.Errorf("failed to output profile filename: %w", err)
}

return nil
}
20 changes: 20 additions & 0 deletions cmd/root.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package main

import (
"os"

"github.com/cbuto/kubectl-pprof/cmd/pprof"

"github.com/spf13/pflag"
"k8s.io/cli-runtime/pkg/genericiooptions"
)

func main() {
flags := pflag.NewFlagSet("kubectl-pprof", pflag.ExitOnError)
pflag.CommandLine = flags

root := pprof.NewPProfCmd(genericiooptions.IOStreams{In: os.Stdin, Out: os.Stdout, ErrOut: os.Stderr})
if err := root.Execute(); err != nil {
os.Exit(1)
}
}
69 changes: 69 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
module github.com/cbuto/kubectl-pprof

go 1.20

require (
github.com/spf13/cobra v1.7.0
github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.8.2
k8s.io/cli-runtime v0.0.0-20230718062906-448da40f6f16
k8s.io/client-go v0.0.0-20230718055620-74c18d3a4044
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/go-errors/errors v1.4.2 // indirect
github.com/go-logr/logr v1.2.4 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.3 // indirect
github.com/google/btree v1.0.1 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.5.9 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7 // indirect
github.com/imdario/mergo v0.3.6 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.2.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/xlab/treeprint v1.2.0 // indirect
go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect
golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.2.0 // indirect
golang.org/x/sys v0.10.0 // indirect
golang.org/x/term v0.10.0 // indirect
golang.org/x/text v0.11.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/api v0.0.0-20230718054858-1b0ec3bb3296 // indirect
k8s.io/apimachinery v0.0.0-20230718054246-5cb236977966 // indirect
k8s.io/klog/v2 v2.100.1 // indirect
k8s.io/kube-openapi v0.0.0-20230717233707-2695361300d9 // indirect
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/kustomize/api v0.13.5-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/kustomize/kyaml v0.14.3-0.20230601165947-6ce0bf390ce3 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)
Loading

0 comments on commit 7b208e9

Please sign in to comment.