Skip to content

Commit

Permalink
Add Ruby support (#50)
Browse files Browse the repository at this point in the history
* Add --imagePullSecret flag to support agent images in private registries

* Add Ruby support

Co-authored-by: Ashley Lowde <[email protected]>
  • Loading branch information
alowde and Ashley Lowde authored Feb 28, 2021
1 parent c20d08f commit ba509a7
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 104 deletions.
8 changes: 8 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,14 @@ jobs:
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
repository: verizondigital/kubectl-flame
tags: ${{ steps.vars.outputs.tag }}-python
- name: Build Ruby Docker Image
uses: docker/build-push-action@v1
with:
dockerfile: 'agent/docker/ruby/Dockerfile'
username: ${{ secrets.DOCKER_HUB_USER }}
password: ${{ secrets.DOCKER_HUB_PASSWORD }}
repository: verizondigital/kubectl-flame
tags: ${{ steps.vars.outputs.tag }}-ruby
- name: Setup Go
uses: actions/setup-go@v1
with:
Expand Down
2 changes: 1 addition & 1 deletion .krew.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ spec:
description: |
Generate CPU flame graphs without restarting pods and with low overhead.
caveats: |
Currently supported languages: Go, Java (any JVM based language) and Python
Currently supported languages: Go, Java (any JVM based language), Python and Ruby.
platforms:
- {{addURIAndSha "https://github.com/VerizonMedia/kubectl-flame/releases/download/{{ .TagName }}/kubectl-flame_{{ .TagName }}_darwin_x86_64.tar.gz" .TagName | indent 6 }}
bin: kubectl-flame
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Running `kubectlf-flame` does **not** require any modification to existing pods.
- [License](#license)

## Requirements
* Supported languages: Go, Java (any JVM based language) and Python
* Supported languages: Go, Java (any JVM based language), Python and Ruby
* Kubernetes cluster that use Docker as the container runtime (tested on GKE, EKS and AKS)

## Usage
Expand Down Expand Up @@ -64,6 +64,7 @@ Under the hood `kubectl-flame` use [async-profiler](https://github.com/jvm-profi
Interaction with the target JVM is done via a shared `/tmp` folder.
Golang support is based on [ebpf profiling](https://en.wikipedia.org/wiki/Berkeley_Packet_Filter).
Python support is based on [py-spy](https://github.com/benfred/py-spy).
Ruby support is based on [rbspy](https://rbspy.github.io/).

## Contribute
Please refer to [the contributing.md file](Contributing.md) for information about how to get involved. We welcome issues, questions, and pull requests.
Expand Down
17 changes: 17 additions & 0 deletions agent/docker/ruby/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM golang:1.14-buster as agentbuild
WORKDIR /go/src/github.com/VerizonMedia/kubectl-flame
ADD . /go/src/github.com/VerizonMedia/kubectl-flame
RUN go get -d -v ./...
RUN cd agent && go build -o /go/bin/agent

FROM rust:1.50 AS rbspybuild
WORKDIR /
RUN git clone https://github.com/rbspy/rbspy
RUN cd rbspy && cargo build --release

FROM bitnami/minideb:stretch
RUN mkdir /app
COPY --from=agentbuild /go/bin/agent /app/agent
COPY --from=rbspybuild /rbspy/target/release/rbspy /app/rbspy

CMD [ "/app/agent" ]
3 changes: 3 additions & 0 deletions agent/profiler/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ var (
jvm = JvmProfiler{}
bpf = BpfProfiler{}
python = PythonProfiler{}
ruby = RubyProfiler{}
)

func ForLanguage(lang api.ProgrammingLanguage) (FlameGraphProfiler, error) {
Expand All @@ -25,6 +26,8 @@ func ForLanguage(lang api.ProgrammingLanguage) (FlameGraphProfiler, error) {
return &bpf, nil
case api.Python:
return &python, nil
case api.Ruby:
return &ruby, nil
default:
return nil, fmt.Errorf("could not find profiler for language %s", lang)
}
Expand Down
42 changes: 42 additions & 0 deletions agent/profiler/ruby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package profiler

import (
"bytes"
"fmt"
"os/exec"
"strconv"

"github.com/VerizonMedia/kubectl-flame/agent/details"
"github.com/VerizonMedia/kubectl-flame/agent/utils"
)

const (
rbspyLocation = "/app/rbspy"
rbspyOutputFileName = "/tmp/rbspy"
)

type RubyProfiler struct{}

func (r *RubyProfiler) SetUp(job *details.ProfilingJob) error {
return nil
}

func (r *RubyProfiler) Invoke(job *details.ProfilingJob) error {
pid, err := utils.FindRootProcessId(job)
if err != nil {
return fmt.Errorf("could not find root process ID: %w", err)
}

duration := strconv.Itoa(int(job.Duration.Seconds()))
cmd := exec.Command(rbspyLocation, "record", "--pid", pid, "--file", rbspyOutputFileName, "--duration", duration, "--format", "flamegraph")
var out bytes.Buffer
var stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
err = cmd.Run()
if err != nil {
return fmt.Errorf("could not launch profiler: %w", err)
}

return utils.PublishFlameGraph(rbspyOutputFileName)
}
1 change: 1 addition & 0 deletions agent/utils/process.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ var (
defaultProcessNames = map[api.ProgrammingLanguage]string{
api.Java: "java",
api.Python: "python",
api.Ruby: "ruby",
}
)

Expand Down
3 changes: 2 additions & 1 deletion api/langs.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,11 @@ const (
Java ProgrammingLanguage = "java"
Go ProgrammingLanguage = "go"
Python ProgrammingLanguage = "python"
Ruby ProgrammingLanguage = "ruby"
)

var (
supportedLangs = []ProgrammingLanguage{Java, Go, Python}
supportedLangs = []ProgrammingLanguage{Java, Go, Python, Ruby}
)

func AvailableLanguages() []ProgrammingLanguage {
Expand Down
29 changes: 15 additions & 14 deletions cli/cmd/data/target.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,18 +9,19 @@ import (
)

type TargetDetails struct {
Namespace string
PodName string
ContainerName string
ContainerId string
Event api.ProfilingEvent
Duration time.Duration
Id string
FileName string
Alpine bool
DryRun bool
Image string
DockerPath string
Language api.ProgrammingLanguage
Pgrep string
Namespace string
PodName string
ContainerName string
ContainerId string
Event api.ProfilingEvent
Duration time.Duration
Id string
FileName string
Alpine bool
DryRun bool
Image string
DockerPath string
Language api.ProgrammingLanguage
Pgrep string
ImagePullSecret string
}
8 changes: 7 additions & 1 deletion cli/cmd/kubernetes/job/bpf.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,17 @@ type bpfCreator struct{}
func (b *bpfCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job, error) {
id := string(uuid.NewUUID())
var imageName string
var imagePullSecret []apiv1.LocalObjectReference
if cfg.TargetConfig.Image != "" {
imageName = cfg.TargetConfig.Image
} else {
imageName = fmt.Sprintf("%s:%s-bpf", baseImageName, version.GetCurrent())
}

if cfg.TargetConfig.ImagePullSecret != "" {
imagePullSecret = []apiv1.LocalObjectReference{{Name: cfg.TargetConfig.ImagePullSecret}}
}

args := []string{
id, string(targetPod.UID),
cfg.TargetConfig.ContainerName, cfg.TargetConfig.ContainerId,
Expand Down Expand Up @@ -85,7 +90,8 @@ func (b *bpfCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string
},
},
},
InitContainers: nil,
ImagePullSecrets: imagePullSecret,
InitContainers: nil,
Containers: []apiv1.Container{
{
ImagePullPolicy: apiv1.PullAlways,
Expand Down
9 changes: 7 additions & 2 deletions cli/cmd/kubernetes/job/jvm.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ func (c *jvmCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string
args = append(args, cfg.TargetConfig.Pgrep)
}

imagePullSecret := []apiv1.LocalObjectReference{}
if cfg.TargetConfig.ImagePullSecret != "" {
imagePullSecret = []apiv1.LocalObjectReference{{Name: cfg.TargetConfig.ImagePullSecret}}
}

commonMeta := metav1.ObjectMeta{
Name: fmt.Sprintf("kubectl-flame-%s", id),
Namespace: cfg.TargetConfig.Namespace,
Expand All @@ -40,7 +45,6 @@ func (c *jvmCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string
"sidecar.istio.io/inject": "false",
},
}

resources, err := cfg.JobConfig.ToResourceRequirements()
if err != nil {
return "", nil, fmt.Errorf("unable to generate resource requirements: %w", err)
Expand Down Expand Up @@ -71,7 +75,8 @@ func (c *jvmCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string
},
},
},
InitContainers: nil,
ImagePullSecrets: imagePullSecret,
InitContainers: nil,
Containers: []apiv1.Container{
{
ImagePullPolicy: apiv1.PullAlways,
Expand Down
8 changes: 7 additions & 1 deletion cli/cmd/kubernetes/job/python.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type pythonCreator struct{}
func (p *pythonCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job, error) {
id := string(uuid.NewUUID())
var imageName string
var imagePullSecret []apiv1.LocalObjectReference
args := []string{
id,
string(targetPod.UID),
Expand All @@ -37,6 +38,10 @@ func (p *pythonCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (str
imageName = fmt.Sprintf("%s:%s-python", baseImageName, version.GetCurrent())
}

if cfg.TargetConfig.ImagePullSecret != "" {
imagePullSecret = []apiv1.LocalObjectReference{{Name: cfg.TargetConfig.ImagePullSecret}}
}

commonMeta := metav1.ObjectMeta{
Name: fmt.Sprintf("kubectl-flame-%s", id),
Namespace: cfg.TargetConfig.Namespace,
Expand Down Expand Up @@ -78,7 +83,8 @@ func (p *pythonCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (str
},
},
},
InitContainers: nil,
ImagePullSecrets: imagePullSecret,
InitContainers: nil,
Containers: []apiv1.Container{
{
ImagePullPolicy: apiv1.PullAlways,
Expand Down
3 changes: 3 additions & 0 deletions cli/cmd/kubernetes/job/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ var (
jvm = jvmCreator{}
bpf = bpfCreator{}
python = pythonCreator{}
ruby = rubyCreator{}
)

type creator interface {
Expand All @@ -35,6 +36,8 @@ func Create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job,
return bpf.create(targetPod, cfg)
case api.Python:
return python.create(targetPod, cfg)
case api.Ruby:
return ruby.create(targetPod, cfg)
}

// Should not happen
Expand Down
112 changes: 112 additions & 0 deletions cli/cmd/kubernetes/job/ruby.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
package job

import (
"fmt"

"github.com/VerizonMedia/kubectl-flame/cli/cmd/data"
"github.com/VerizonMedia/kubectl-flame/cli/cmd/version"
batchv1 "k8s.io/api/batch/v1"
apiv1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/uuid"
)

type rubyCreator struct{}

func (r *rubyCreator) create(targetPod *apiv1.Pod, cfg *data.FlameConfig) (string, *batchv1.Job, error) {
id := string(uuid.NewUUID())
var imageName string
var imagePullSecret []apiv1.LocalObjectReference
args := []string{
id,
string(targetPod.UID),
cfg.TargetConfig.ContainerName,
cfg.TargetConfig.ContainerId,
cfg.TargetConfig.Duration.String(),
string(cfg.TargetConfig.Language),
cfg.TargetConfig.Pgrep,
}

if cfg.TargetConfig.Image != "" {
imageName = cfg.TargetConfig.Image
} else {
imageName = fmt.Sprintf("%s:%s-ruby", baseImageName, version.GetCurrent())
}

if cfg.TargetConfig.ImagePullSecret != "" {
imagePullSecret = []apiv1.LocalObjectReference{{Name: cfg.TargetConfig.ImagePullSecret}}
}

commonMeta := metav1.ObjectMeta{
Name: fmt.Sprintf("kubectl-flame-%s", id),
Namespace: cfg.TargetConfig.Namespace,
Labels: map[string]string{
"kubectl-flame/id": id,
},
Annotations: map[string]string{
"sidecar.istio.io/inject": "false",
},
}

resources, err := cfg.JobConfig.ToResourceRequirements()
if err != nil {
return "", nil, fmt.Errorf("unable to generate resource requirements: %w", err)
}

job := &batchv1.Job{
TypeMeta: metav1.TypeMeta{
Kind: "Job",
APIVersion: "batch/v1",
},
ObjectMeta: commonMeta,
Spec: batchv1.JobSpec{
Parallelism: int32Ptr(1),
Completions: int32Ptr(1),
TTLSecondsAfterFinished: int32Ptr(5),
Template: apiv1.PodTemplateSpec{
ObjectMeta: commonMeta,
Spec: apiv1.PodSpec{
HostPID: true,
Volumes: []apiv1.Volume{
{
Name: "target-filesystem",
VolumeSource: apiv1.VolumeSource{
HostPath: &apiv1.HostPathVolumeSource{
Path: cfg.TargetConfig.DockerPath,
},
},
},
},
ImagePullSecrets: imagePullSecret,
InitContainers: nil,
Containers: []apiv1.Container{
{
ImagePullPolicy: apiv1.PullAlways,
Name: ContainerName,
Image: imageName,
Command: []string{"/app/agent"},
Args: args,
VolumeMounts: []apiv1.VolumeMount{
{
Name: "target-filesystem",
MountPath: "/var/lib/docker",
},
},
SecurityContext: &apiv1.SecurityContext{
Privileged: boolPtr(true),
Capabilities: &apiv1.Capabilities{
Add: []apiv1.Capability{"SYS_PTRACE"},
},
},
Resources: resources,
},
},
RestartPolicy: "Never",
NodeName: targetPod.Spec.NodeName,
},
},
},
}

return id, job, nil
}
1 change: 1 addition & 0 deletions cli/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ func NewFlameCommand(streams genericclioptions.IOStreams) *cobra.Command {
cmd.Flags().StringVar(&jobDetails.RequestConfig.Memory, "mem.requests", "", "Memory requests of the started profiling container")
cmd.Flags().StringVar(&jobDetails.LimitConfig.CPU, "cpu.limits", "", "CPU limits of the started profiling container")
cmd.Flags().StringVar(&jobDetails.LimitConfig.Memory, "mem.limits", "", "Memory limits of the started profiling container")
cmd.Flags().StringVar(&targetDetails.ImagePullSecret, "imagePullSecret", "", "imagePullSecret for agent docker image")

options.configFlags.AddFlags(cmd.Flags())

Expand Down
Loading

0 comments on commit ba509a7

Please sign in to comment.