-
Notifications
You must be signed in to change notification settings - Fork 60
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
scout/advise: first iteration of the
src scout advise
subcommand (#988
- Loading branch information
1 parent
7fc6627
commit 23e8eb2
Showing
14 changed files
with
924 additions
and
529 deletions.
There are no files selected for viewing
This file contains 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,120 @@ | ||
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 all containers in a Docker deployment of Sourcegraph. | ||
$ src scout advise | ||
Make recommendations for specific pod: | ||
$ src scout advise --pod <podname> | ||
Make recommendations for specific container: | ||
$ src scout advise --container <containername> | ||
Add namespace if using namespace in a Kubernetes cluster | ||
$ src scout advise --namespace <namespace> | ||
` | ||
|
||
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") | ||
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 *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 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,30 @@ | ||
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 | ||
} | ||
} |
This file contains 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" | ||
) | ||
|
||
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 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,164 @@ | ||
package advise | ||
|
||
import ( | ||
"context" | ||
"fmt" | ||
|
||
"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" | ||
) | ||
|
||
func K8s( | ||
ctx context.Context, | ||
k8sClient *kubernetes.Clientset, | ||
metricsClient *metricsv.Clientset, | ||
restConfig *rest.Config, | ||
opts ...Option, | ||
) error { | ||
cfg := &scout.Config{ | ||
Namespace: "default", | ||
Pod: "", | ||
Container: "", | ||
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 | ||
} | ||
|
||
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) | ||
} | ||
|
||
fmt.Println(scout.EmojiFingerPointRight, pod.Name) | ||
for _, msg := range advice { | ||
fmt.Println(msg) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
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 { | ||
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 | ||
} |
This file contains 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,9 @@ | ||
package scout | ||
|
||
const ( | ||
ABillion float64 = 1_000_000_000 | ||
EmojiFingerPointRight = "👉" | ||
FlashingLightEmoji = "🚨" | ||
SuccessEmoji = "✅" | ||
WarningSign = "⚠️ " // why does this need an extra space to align?!?! | ||
) |
This file contains 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,24 @@ | ||
package scout | ||
|
||
// contains checks if a string slice contains a given value. | ||
func Contains(slice []string, value string) bool { | ||
for _, v := range slice { | ||
if v == value { | ||
return true | ||
} | ||
} | ||
return false | ||
} | ||
|
||
// getPercentage calculates the percentage of x in relation to y. | ||
func GetPercentage(x, y float64) float64 { | ||
if x == 0 { | ||
return 0 | ||
} | ||
|
||
if y == 0 { | ||
return 0 | ||
} | ||
|
||
return x * 100 / y | ||
} |
Oops, something went wrong.