-
Notifications
You must be signed in to change notification settings - Fork 70
add output flag for advise #990
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from 12 commits
Commits
Show all changes
14 commits
Select commit
Hold shift + click to select a range
1b68b30
add skeleton for advise command
jasonhawkharris cd461a5
add helpers; get pods, get containers
jasonhawkharris 4a6b464
large scale refactor for scout
jasonhawkharris 8d0b96a
linter fixes
jasonhawkharris 65cdfec
add constants and helpers; refactor k8s functions
jasonhawkharris e6a8502
bulk advise on all pods
jasonhawkharris 672d9c4
linter fix
jasonhawkharris bde2f5f
small fixes and refactors
jasonhawkharris 877dedd
fig bug where scout was failing on minikube and Kind k8s clusters
jasonhawkharris 5bae496
add output flag for advise
jasonhawkharris 52d9599
linter fixes
jasonhawkharris 89296cd
favor Fprintf to file.WriteString()
jasonhawkharris 41ba7aa
Merge branch 'main' into jhh/scout-advise-output-flag
jasonhawkharris 265bb14
Update cmd/src/scout_advise.go
jasonhawkharris File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| package main | ||
|
|
||
| import ( | ||
| "context" | ||
| "flag" | ||
| "fmt" | ||
| "path/filepath" | ||
|
|
||
| "github.com/docker/docker/client" | ||
| "github.com/sourcegraph/sourcegraph/lib/errors" | ||
| "github.com/sourcegraph/src-cli/internal/scout/advise" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/tools/clientcmd" | ||
| "k8s.io/client-go/util/homedir" | ||
| metricsv "k8s.io/metrics/pkg/client/clientset/versioned" | ||
| ) | ||
|
|
||
| func init() { | ||
| cmdUsage := `'src scout advise' is a tool that makes resource allocation recommendations. Based on current usage. | ||
| Part of the EXPERIMENTAL "src scout" tool. | ||
|
|
||
| Examples | ||
| Make recommendations for all pods in a kubernetes deployment of Sourcegraph. | ||
| $ src scout advise | ||
|
|
||
| Make recommendations for specific pod: | ||
| $ src scout advise --pod <podname> | ||
|
|
||
| Add namespace if using namespace in a Kubernetes cluster | ||
| $ src scout advise --namespace <namespace> | ||
|
|
||
| Output advice to file | ||
| $ src scout advise --o path/to/file | ||
| ` | ||
|
|
||
| flagSet := flag.NewFlagSet("advise", flag.ExitOnError) | ||
| usage := func() { | ||
| fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src scout %s':\n", flagSet.Name()) | ||
| flagSet.PrintDefaults() | ||
| fmt.Println(cmdUsage) | ||
| } | ||
|
|
||
| var ( | ||
| kubeConfig *string | ||
| namespace = flagSet.String("namespace", "", "(optional) specify the kubernetes namespace to use") | ||
| pod = flagSet.String("pod", "", "(optional) specify a single pod") | ||
| container = flagSet.String("container", "", "(optional) specify a single container") | ||
| output = flagSet.String("o", "", "(optional) output advice to file") | ||
| docker = flagSet.Bool("docker", false, "(optional) using docker deployment") | ||
| ) | ||
|
|
||
| if home := homedir.HomeDir(); home != "" { | ||
| kubeConfig = flagSet.String( | ||
| "kubeconfig", | ||
| filepath.Join(home, ".kube", "config"), | ||
| "(optional) absolute path to the kubeconfig file", | ||
| ) | ||
| } else { | ||
| kubeConfig = flagSet.String("kubeconfig", "", "absolute path to the kubeconfig file") | ||
| } | ||
|
|
||
| handler := func(args []string) error { | ||
| if err := flagSet.Parse(args); err != nil { | ||
| return err | ||
| } | ||
|
|
||
| config, err := clientcmd.BuildConfigFromFlags("", *kubeConfig) | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to load .kube config: ") | ||
| } | ||
|
|
||
| clientSet, err := kubernetes.NewForConfig(config) | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to initiate kubernetes client: ") | ||
| } | ||
|
|
||
| metricsClient, err := metricsv.NewForConfig(config) | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to initiate metrics client") | ||
| } | ||
|
|
||
| var options []advise.Option | ||
|
|
||
| if *namespace != "" { | ||
| options = append(options, advise.WithNamespace(*namespace)) | ||
| } | ||
| if *pod != "" { | ||
| options = append(options, advise.WithPod(*pod)) | ||
| } | ||
| if *output != "" { | ||
| options = append(options, advise.WithOutput(*output)) | ||
| } | ||
| if *container != "" || *docker { | ||
| if *container != "" { | ||
| options = append(options, advise.WithContainer(*container)) | ||
| } | ||
|
|
||
| dockerClient, err := client.NewClientWithOpts(client.FromEnv) | ||
| if err != nil { | ||
| return errors.Wrap(err, "error creating docker client: ") | ||
| } | ||
|
|
||
| return advise.Docker(context.Background(), *dockerClient, options...) | ||
| } | ||
|
|
||
| return advise.K8s( | ||
| context.Background(), | ||
| clientSet, | ||
| metricsClient, | ||
| config, | ||
| options..., | ||
| ) | ||
| } | ||
|
|
||
| scoutCommands = append(scoutCommands, &command{ | ||
| flagSet: flagSet, | ||
| handler: handler, | ||
| usageFunc: usage, | ||
| }) | ||
|
|
||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,36 @@ | ||
| package advise | ||
|
|
||
| import "github.com/sourcegraph/src-cli/internal/scout" | ||
|
|
||
| type Option = func(config *scout.Config) | ||
|
|
||
| const ( | ||
| OVER_100 = "\t%s %s: Your %s usage is over 100%% (%.2f%%). Add more %s." | ||
| OVER_80 = "\t%s %s: Your %s usage is over 80%% (%.2f%%). Consider raising limits." | ||
| OVER_40 = "\t%s %s: Your %s usage is under 80%% (%.2f%%). Keep %s allocation the same." | ||
| UNDER_40 = "\t%s %s: Your %s usage is under 40%% (%.2f%%). Consider lowering limits." | ||
| ) | ||
|
|
||
| func WithNamespace(namespace string) Option { | ||
| return func(config *scout.Config) { | ||
| config.Namespace = namespace | ||
| } | ||
| } | ||
|
|
||
| func WithPod(podname string) Option { | ||
| return func(config *scout.Config) { | ||
| config.Pod = podname | ||
| } | ||
| } | ||
|
|
||
| func WithContainer(containerName string) Option { | ||
| return func(config *scout.Config) { | ||
| config.Container = containerName | ||
| } | ||
| } | ||
|
|
||
| func WithOutput(pathToFile string) Option { | ||
| return func(config *scout.Config) { | ||
| config.Output = pathToFile | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,40 @@ | ||
| package advise | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
|
|
||
| "github.com/docker/docker/api/types" | ||
| "github.com/docker/docker/client" | ||
| "github.com/sourcegraph/sourcegraph/lib/errors" | ||
| "github.com/sourcegraph/src-cli/internal/scout" | ||
jasonhawkharris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| func Docker(ctx context.Context, client client.Client, opts ...Option) error { | ||
| cfg := &scout.Config{ | ||
| Namespace: "default", | ||
| Docker: true, | ||
| Pod: "", | ||
| Container: "", | ||
| Spy: false, | ||
| DockerClient: &client, | ||
| } | ||
|
|
||
| for _, opt := range opts { | ||
| opt(cfg) | ||
| } | ||
|
|
||
| containers, err := client.ContainerList(ctx, types.ContainerListOptions{}) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not get list of containers") | ||
| } | ||
|
|
||
| PrintContainers(containers) | ||
| return nil | ||
| } | ||
|
|
||
| func PrintContainers(containers []types.Container) { | ||
| for _, c := range containers { | ||
| fmt.Println(c.Image) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,195 @@ | ||
| package advise | ||
|
|
||
| import ( | ||
| "context" | ||
| "fmt" | ||
| "os" | ||
|
|
||
| "github.com/sourcegraph/sourcegraph/lib/errors" | ||
| "github.com/sourcegraph/src-cli/internal/scout" | ||
| "github.com/sourcegraph/src-cli/internal/scout/kube" | ||
| v1 "k8s.io/api/core/v1" | ||
| "k8s.io/client-go/kubernetes" | ||
| "k8s.io/client-go/rest" | ||
| metricsv "k8s.io/metrics/pkg/client/clientset/versioned" | ||
jasonhawkharris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ) | ||
|
|
||
| func K8s( | ||
| ctx context.Context, | ||
| k8sClient *kubernetes.Clientset, | ||
| metricsClient *metricsv.Clientset, | ||
| restConfig *rest.Config, | ||
| opts ...Option, | ||
| ) error { | ||
| cfg := &scout.Config{ | ||
| Namespace: "default", | ||
| Pod: "", | ||
| Container: "", | ||
| Output: "", | ||
| Spy: false, | ||
| Docker: false, | ||
| RestConfig: restConfig, | ||
| K8sClient: k8sClient, | ||
| DockerClient: nil, | ||
| MetricsClient: metricsClient, | ||
| } | ||
|
|
||
| for _, opt := range opts { | ||
| opt(cfg) | ||
| } | ||
|
|
||
| pods, err := kube.GetPods(ctx, cfg) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not get list of pods") | ||
| } | ||
|
|
||
| if cfg.Pod != "" { | ||
| pod, err := kube.GetPod(cfg.Pod, pods) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not get pod") | ||
| } | ||
|
|
||
| err = Advise(ctx, cfg, pod) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not advise") | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| for _, pod := range pods { | ||
| err = Advise(ctx, cfg, pod) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not advise") | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // Advise generates resource allocation advice for a Kubernetes pod. | ||
| // The function fetches usage metrics for each container in the pod. It then | ||
| // checks the usage percentages against thresholds to determine if more or less | ||
| // of a resource is needed. Advice is generated and either printed to the console | ||
| // or output to a file depending on the cfg.Output field. | ||
| func Advise(ctx context.Context, cfg *scout.Config, pod v1.Pod) error { | ||
| var advice []string | ||
| usageMetrics, err := getUsageMetrics(ctx, cfg, pod) | ||
| if err != nil { | ||
| return errors.Wrap(err, "could not get usage metrics") | ||
| } | ||
|
|
||
| for _, metrics := range usageMetrics { | ||
| cpuAdvice := checkUsage(metrics.CpuUsage, "CPU", metrics.ContainerName, pod.Name) | ||
| advice = append(advice, cpuAdvice) | ||
|
|
||
| memoryAdvice := checkUsage(metrics.MemoryUsage, "memory", metrics.ContainerName, pod.Name) | ||
| advice = append(advice, memoryAdvice) | ||
|
|
||
| if metrics.Storage != nil { | ||
| storageAdvice := checkUsage(metrics.StorageUsage, "storage", metrics.ContainerName, pod.Name) | ||
| advice = append(advice, storageAdvice) | ||
| } | ||
|
|
||
| if cfg.Output != "" { | ||
| outputToFile(ctx, cfg, pod, advice) | ||
jasonhawkharris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } else { | ||
| for _, msg := range advice { | ||
| fmt.Println(msg) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return nil | ||
| } | ||
|
|
||
| // outputToFile writes resource allocation advice for a Kubernetes pod to a file. | ||
| func outputToFile(ctx context.Context, cfg *scout.Config, pod v1.Pod, advice []string) error { | ||
jasonhawkharris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| file, err := os.OpenFile(cfg.Output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644) | ||
| if err != nil { | ||
| return errors.Wrap(err, "failed to open file") | ||
| } | ||
| defer file.Close() | ||
|
|
||
| if _, err := fmt.Fprintf(file, "- %s\n", pod.Name); err != nil { | ||
| return errors.Wrap(err, "failed to write pod name to file") | ||
| } | ||
|
|
||
| for _, msg := range advice { | ||
| if _, err := fmt.Fprintf(file, "%s\n", msg); err != nil { | ||
| return errors.Wrap(err, "failed to write container advice to file") | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| // getUsageMetrics generates resource usage statistics for containers in a Kubernetes pod. | ||
| func getUsageMetrics(ctx context.Context, cfg *scout.Config, pod v1.Pod) ([]scout.UsageStats, error) { | ||
| var usages []scout.UsageStats | ||
| var usage scout.UsageStats | ||
| podMetrics, err := kube.GetPodMetrics(ctx, cfg, pod) | ||
| if err != nil { | ||
| return usages, errors.Wrap(err, "while attempting to fetch pod metrics") | ||
| } | ||
|
|
||
| containerMetrics := &scout.ContainerMetrics{ | ||
| PodName: cfg.Pod, | ||
| Limits: map[string]scout.Resources{}, | ||
| } | ||
|
|
||
| if err = kube.AddLimits(ctx, cfg, &pod, containerMetrics); err != nil { | ||
| return usages, errors.Wrap(err, "failed to get get container metrics") | ||
| } | ||
|
|
||
| for _, container := range podMetrics.Containers { | ||
| usage, err = kube.GetUsage(ctx, cfg, *containerMetrics, pod, container) | ||
| if err != nil { | ||
| return usages, errors.Wrapf(err, "could not compile usages data for row: %s\n", container.Name) | ||
| } | ||
| usages = append(usages, usage) | ||
| } | ||
|
|
||
| return usages, nil | ||
| } | ||
|
|
||
| func checkUsage(usage float64, resourceType, container, pod string) string { | ||
jasonhawkharris marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| var message string | ||
|
|
||
| switch { | ||
| case usage >= 100: | ||
| message = fmt.Sprintf( | ||
| OVER_100, | ||
| scout.FlashingLightEmoji, | ||
| container, | ||
| resourceType, | ||
| usage, | ||
| resourceType, | ||
| ) | ||
| case usage >= 80 && usage < 100: | ||
| message = fmt.Sprintf( | ||
| OVER_80, | ||
| scout.WarningSign, | ||
| container, | ||
| resourceType, | ||
| usage, | ||
| ) | ||
| case usage >= 40 && usage < 80: | ||
| message = fmt.Sprintf( | ||
| OVER_40, | ||
| scout.SuccessEmoji, | ||
| container, | ||
| resourceType, | ||
| usage, | ||
| resourceType, | ||
| ) | ||
| default: | ||
| message = fmt.Sprintf( | ||
| UNDER_40, | ||
| scout.WarningSign, | ||
| container, | ||
| resourceType, | ||
| usage, | ||
| ) | ||
| } | ||
|
|
||
| return message | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.