diff --git a/cli/cmd/data/target.go b/cli/cmd/data/target.go index a457dcb..e6903c1 100644 --- a/cli/cmd/data/target.go +++ b/cli/cmd/data/target.go @@ -13,4 +13,5 @@ type TargetDetails struct { Id string FileName string Alpine bool + DryRun bool } diff --git a/cli/cmd/kubernetes/launch.go b/cli/cmd/kubernetes/launch.go index 1a5caff..fdffc92 100644 --- a/cli/cmd/kubernetes/launch.go +++ b/cli/cmd/kubernetes/launch.go @@ -5,12 +5,15 @@ package kubernetes import ( "context" "fmt" + "k8s.io/apimachinery/pkg/runtime" + "os" "github.com/VerizonMedia/kubectl-flame/cli/cmd/data" batchv1 "k8s.io/api/batch/v1" apiv1 "k8s.io/api/core/v1" v1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime/serializer/json" "k8s.io/apimachinery/pkg/util/uuid" ) @@ -30,6 +33,10 @@ func LaunchFlameJob(targetPod *v1.Pod, targetDetails *data.TargetDetails, ctx co } job := &batchv1.Job{ + TypeMeta: metav1.TypeMeta{ + Kind: "Job", + APIVersion: "batch/v1", + }, ObjectMeta: commonMeta, Spec: batchv1.JobSpec{ Parallelism: int32Ptr(1), @@ -93,6 +100,11 @@ func LaunchFlameJob(targetPod *v1.Pod, targetDetails *data.TargetDetails, ctx co }, } + if targetDetails.DryRun { + err := printJob(job) + return "", nil, err + } + createJob, err := clientSet. BatchV1(). Jobs(targetDetails.Namespace). @@ -105,6 +117,20 @@ func LaunchFlameJob(targetPod *v1.Pod, targetDetails *data.TargetDetails, ctx co return id, createJob, nil } +func printJob(job *batchv1.Job) error { + scheme := runtime.NewScheme() + err := metav1.AddMetaToScheme(scheme) + if err != nil { + return err + } + + encoder := json.NewSerializerWithOptions(json.DefaultMetaFactory, scheme, scheme, json.SerializerOptions{ + Yaml: true, + }) + + return encoder.Encode(job, os.Stdout) +} + func DeleteProfilingJob(job *batchv1.Job, targetDetails *data.TargetDetails, ctx context.Context) error { deleteStrategy := metav1.DeletePropagationForeground return clientSet. diff --git a/cli/cmd/logic.go b/cli/cmd/logic.go index 8e8f96c..e3cae4b 100644 --- a/cli/cmd/logic.go +++ b/cli/cmd/logic.go @@ -17,6 +17,7 @@ import ( func Flame(target *data.TargetDetails, configFlags *genericclioptions.ConfigFlags) { ns, err := kubernetes.Connect(configFlags) + p := NewPrinter(target.DryRun) if err != nil { fmt.Printf("Failed connecting to kubernetes cluster: %v\n", err) os.Exit(1) @@ -24,55 +25,59 @@ func Flame(target *data.TargetDetails, configFlags *genericclioptions.ConfigFlag target.Namespace = ns ctx := context.Background() - fmt.Print("Verifying target pod ... ") + p.Print("Verifying target pod ... ") pod, err := kubernetes.GetPodDetails(target.PodName, target.Namespace, ctx) if err != nil { - PrintError() + p.PrintError() fmt.Println(err.Error()) os.Exit(1) } containerName, err := validatePod(pod, target.ContainerName) if err != nil { - PrintError() + p.PrintError() fmt.Println(err.Error()) os.Exit(1) } containerId, err := kubernetes.GetContainerId(containerName, pod) if err != nil { - PrintError() + p.PrintError() fmt.Println(err.Error()) os.Exit(1) } - PrintSuccess() + p.PrintSuccess() target.ContainerName = containerName target.ContainerId = containerId - fmt.Print("Launching profiler ... ") + p.Print("Launching profiler ... ") profileId, job, err := kubernetes.LaunchFlameJob(pod, target, ctx) if err != nil { - PrintError() + p.PrintError() fmt.Print(err.Error()) os.Exit(1) } + if target.DryRun { + return + } + target.Id = profileId profilerPod, err := kubernetes.WaitForPodStart(target, ctx) if err != nil { - PrintError() + p.PrintError() fmt.Println(err.Error()) os.Exit(1) } - PrintSuccess() + p.PrintSuccess() apiHandler := &handler.ApiEventsHandler{ Job: job, Target: target, } done, err := kubernetes.GetLogsFromPod(profilerPod, apiHandler, ctx) if err != nil { - PrintError() + p.PrintError() fmt.Println(err.Error()) } diff --git a/cli/cmd/root.go b/cli/cmd/root.go index e0155c4..1f42f4a 100644 --- a/cli/cmd/root.go +++ b/cli/cmd/root.go @@ -71,6 +71,7 @@ func NewFlameCommand(streams genericclioptions.IOStreams) *cobra.Command { cmd.Flags().DurationVarP(&targetDetails.Duration, "time", "t", defaultDuration, "Enter max scan Duration") cmd.Flags().StringVarP(&targetDetails.FileName, "file", "f", "flamegraph.svg", "Optional file location") cmd.Flags().BoolVar(&targetDetails.Alpine, "alpine", false, "Target image is based on Alpine") + cmd.Flags().BoolVar(&targetDetails.DryRun, "dry-run", false, "simulate profiling") options.configFlags.AddFlags(cmd.Flags()) return cmd diff --git a/cli/cmd/utils.go b/cli/cmd/utils.go index 042a466..b185b3a 100644 --- a/cli/cmd/utils.go +++ b/cli/cmd/utils.go @@ -4,10 +4,34 @@ package cmd import "fmt" -func PrintSuccess() { - fmt.Printf("✔\n") +type Printer interface { + Print(str string) + PrintSuccess() + PrintError() } -func PrintError() { +type dryRunPrinter struct { + dryRun bool +} + +func (p *dryRunPrinter) Print(str string) { + if !p.dryRun { + fmt.Print(str) + } +} + +func (p *dryRunPrinter) PrintSuccess() { + if !p.dryRun { + fmt.Printf("✔\n") + } +} + +func (p *dryRunPrinter) PrintError() { fmt.Printf("❌\n") } + +func NewPrinter(dryRun bool) Printer { + return &dryRunPrinter{ + dryRun: dryRun, + } +}