diff --git a/cmd/src/scout.go b/cmd/src/scout.go deleted file mode 100644 index d5df574a80..0000000000 --- a/cmd/src/scout.go +++ /dev/null @@ -1,43 +0,0 @@ -package main - -import ( - "flag" - "fmt" -) - -var scoutCommands commander - -func init() { - usage := `'src scout' is a tool that provides monitoring for Sourcegraph resources - - EXPERIMENTAL: 'scout' is an experimental command in the 'src' tool. To use, you must - point your .kube config to your Sourcegraph instance. - - Usage: - - src scout command [command options] - - The commands are: - - resource print all known sourcegraph resources and their allocations - usage get CPU, memory and current disk usage - advise recommend lowering or raising resource allocations based on actual usage - - Use "src scout [command] -h" for more information about a command. - ` - - flagSet := flag.NewFlagSet("scout", flag.ExitOnError) - handler := func(args []string) error { - scoutCommands.run(flagSet, "src scout", usage, args) - return nil - } - - commands = append(commands, &command{ - flagSet: flagSet, - aliases: []string{"scout"}, - handler: handler, - usageFunc: func() { - fmt.Println(usage) - }, - }) -} diff --git a/cmd/src/scout_advise.go b/cmd/src/scout_advise.go deleted file mode 100644 index 129a13c660..0000000000 --- a/cmd/src/scout_advise.go +++ /dev/null @@ -1,113 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "path/filepath" - - "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" - - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -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 - - Add namespace if using namespace in a Kubernetes cluster - $ src scout advise --namespace - - Output advice to file - $ src scout advise --o path/to/file - - Output with warnings - $ src scout advise --warnings - ` - - 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") - output = flagSet.String("o", "", "(optional) output advice to file") - warnings = flagSet.Bool("warnings", false, "(optional) output advice with warnings") - ) - - 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 *warnings { - options = append(options, advise.WithWarnings(true)) - } - - return advise.K8s( - context.Background(), - clientSet, - metricsClient, - config, - options..., - ) - } - - scoutCommands = append(scoutCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usage, - }) -} diff --git a/cmd/src/scout_resource.go b/cmd/src/scout_resource.go deleted file mode 100644 index f4c8cf2b5b..0000000000 --- a/cmd/src/scout_resource.go +++ /dev/null @@ -1,82 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "path/filepath" - - "github.com/sourcegraph/sourcegraph/lib/errors" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/tools/clientcmd" - "k8s.io/client-go/util/homedir" - - "github.com/sourcegraph/src-cli/internal/scout/resource" -) - -func init() { - usage := `'src scout resource' is a tool that provides an overview of resource usage - across an instance of Sourcegraph. Part of the EXPERIMENTAL "src scout" tool. - - Examples - List pods and resource allocations in a Kubernetes deployment: - $ src scout resource - - Add namespace if using namespace in a Kubernetes cluster - $ src scout resource --namespace sg - ` - - flagSet := flag.NewFlagSet("resource", flag.ExitOnError) - usageFunc := func() { - fmt.Fprintf(flag.CommandLine.Output(), "Usage of 'src scout %s':\n", flagSet.Name()) - flagSet.PrintDefaults() - fmt.Println(usage) - } - - var ( - kubeConfig *string - namespace = flagSet.String("namespace", "", "(optional) specify the kubernetes namespace to use") - // TODO: option for getting resource allocation of the Node - // nodes = flagSet.Bool("node", false, "(optional) view resources for node(s)") - ) - - 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.New(fmt.Sprintf("%v: failed to load kubernetes config", err)) - } - - clientSet, err := kubernetes.NewForConfig(config) - if err != nil { - return errors.New(fmt.Sprintf("%v: failed to load kubernetes config", err)) - } - - var options []resource.Option - - if *namespace != "" { - options = append(options, resource.WithNamespace(*namespace)) - } - - return resource.K8s(context.Background(), clientSet, config, options...) - } - - scoutCommands = append(scoutCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) -} diff --git a/cmd/src/scout_usage.go b/cmd/src/scout_usage.go deleted file mode 100644 index ba12fd6c11..0000000000 --- a/cmd/src/scout_usage.go +++ /dev/null @@ -1,98 +0,0 @@ -package main - -import ( - "context" - "flag" - "fmt" - "path/filepath" - - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/src-cli/internal/scout/usage" - "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 usage' is a tool that tracks resource usage for Sourcegraph instances. - Part of the EXPERIMENTAL "src scout" tool. - - Examples - List pods and resource usage in a Kubernetes deployment: - $ src scout usage - - Check usage for specific pod - $ src scout usage --pod - - Add namespace if using namespace in a Kubernetes cluster - $ src scout usage --namespace - ` - - flagSet := flag.NewFlagSet("usage", flag.ExitOnError) - usageFunc := 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") - ) - - 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 []usage.Option - if *namespace != "" { - options = append(options, usage.WithNamespace(*namespace)) - } - if *pod != "" { - options = append(options, usage.WithPod(*pod)) - } - - return usage.K8s( - context.Background(), - clientSet, - metricsClient, - config, - options..., - ) - } - - scoutCommands = append(scoutCommands, &command{ - flagSet: flagSet, - handler: handler, - usageFunc: usageFunc, - }) - -} diff --git a/internal/scout/advise/advise.go b/internal/scout/advise/advise.go deleted file mode 100644 index c1cefe90c0..0000000000 --- a/internal/scout/advise/advise.go +++ /dev/null @@ -1,97 +0,0 @@ -package advise - -import ( - "context" - "fmt" - "os" - - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/src-cli/internal/scout" -) - -type Option = func(config *scout.Config) - -const ( - UNDER_PROVISIONED = "%s %s: %s is under-provisioned (%.2f%% usage). Add resources." - WELL_PROVISIONED = "%s %s: %s is well-provisioned (%.2f%% usage). No action needed." - OVER_PROVISIONED = "%s %s: %s is over-provisioned (%.2f%% usage). Trim resources." -) - -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 WithOutput(pathToFile string) Option { - return func(config *scout.Config) { - config.Output = pathToFile - } -} - -func WithWarnings(includeWarnings bool) Option { - return func(config *scout.Config) { - config.Warnings = includeWarnings - } -} - -func CheckUsage(usage float64, resourceType string, container string) scout.Advice { - var advice scout.Advice - switch { - case usage >= 80: - advice.Kind = scout.DANGER - advice.Msg = fmt.Sprintf( - UNDER_PROVISIONED, - scout.FlashingLightEmoji, - container, - resourceType, - usage, - ) - case usage >= 20 && usage < 80: - advice.Kind = scout.HEALTHY - advice.Msg = fmt.Sprintf( - WELL_PROVISIONED, - scout.SuccessEmoji, - container, - resourceType, - usage, - ) - default: - advice.Kind = scout.WARNING - advice.Msg = fmt.Sprintf( - OVER_PROVISIONED, - scout.WarningSign, - container, - resourceType, - usage, - ) - } - - return advice -} - -// outputToFile writes resource allocation advice for a Kubernetes pod to a file. -func OutputToFile(ctx context.Context, cfg *scout.Config, name string, advice []scout.Advice) error { - 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", name); err != nil { - return errors.Wrap(err, "failed to write service name to file") - } - - for _, adv := range advice { - if _, err := fmt.Fprintf(file, "%s\n", adv.Msg); err != nil { - return errors.Wrap(err, "failed to write container advice to file") - } - } - return nil -} diff --git a/internal/scout/advise/advise_test.go b/internal/scout/advise/advise_test.go deleted file mode 100644 index c93696c20f..0000000000 --- a/internal/scout/advise/advise_test.go +++ /dev/null @@ -1,147 +0,0 @@ -package advise - -import ( - "bufio" - "context" - "os" - "testing" - "time" - - "github.com/sourcegraph/src-cli/internal/scout" -) - -func TestCheckUsage(t *testing.T) { - cases := []struct { - name string - usage float64 - resourceType string - container string - want scout.Advice - }{ - { - name: "should return correct message for usage over 100", - usage: 110, - resourceType: "cpu", - container: "gitserver-0", - want: scout.Advice{ - Kind: scout.DANGER, - Msg: "🚨 gitserver-0: cpu is under-provisioned (110.00% usage). Add resources.", - }, - }, - { - name: "should return correct message for usage over 80 and under 100", - usage: 87, - resourceType: "memory", - container: "gitserver-0", - want: scout.Advice{ - Kind: scout.DANGER, - Msg: "🚨 gitserver-0: memory is under-provisioned (87.00% usage). Add resources.", - }, - }, - { - name: "should return correct message for usage over 40 and under 80", - usage: 63.4, - resourceType: "memory", - container: "gitserver-0", - want: scout.Advice{ - Kind: scout.HEALTHY, - Msg: "✅ gitserver-0: memory is well-provisioned (63.40% usage). No action needed.", - }, - }, - { - name: "should return correct message for usage under 40", - usage: 12.33, - resourceType: "memory", - container: "gitserver-0", - want: scout.Advice{ - Kind: scout.WARNING, - Msg: "⚠️ gitserver-0: memory is over-provisioned (12.33% usage). Trim resources.", - }, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got := CheckUsage(tc.usage, tc.resourceType, tc.container) - - if got != tc.want { - t.Errorf("got: '%s' want '%s'", got, tc.want) - } - }) - } -} - -func TestOutputToFile(t *testing.T) { - cfg := &scout.Config{ - Output: os.TempDir() + string(os.PathSeparator) + "test.txt", - } - name := "gitserver-0" - advice := []scout.Advice{ - { - Kind: scout.WARNING, - Msg: "Add more CPU", - }, - { - Kind: scout.WARNING, - Msg: "Add more memory", - }, - } - - err := OutputToFile(context.Background(), cfg, name, advice) - if err != nil { - t.Fatal(err) - } - - lines := readOutputFile(t, cfg) - - cases := []struct { - lineNum int - want string - }{ - {1, "- gitserver-0"}, - {2, advice[0].Msg}, - {3, advice[1].Msg}, - } - - for _, tc := range cases { - tc := tc - got := lines[tc.lineNum-1] - if got != tc.want { - t.Errorf("Expected %q, got %q", tc.want, lines[tc.lineNum-1]) - } - } - - if len(lines) > 3 { - t.Error("Expected only 3 lines, got more") - } - - if err != nil { - t.Fatal(err) - } -} - -func readOutputFile(t *testing.T, cfg *scout.Config) []string { - file, err := os.Open(cfg.Output) - if err != nil { - t.Fatal(err) - } - - var lines []string - scanner := bufio.NewScanner(file) - for scanner.Scan() { - lines = append(lines, scanner.Text()) - } - - file.Close() - err = os.Remove(cfg.Output) - if err != nil { - // try again after waiting a bit - time.Sleep(100 * time.Millisecond) - err = os.Remove(cfg.Output) - if err != nil { - t.Fatal(err) - } - } - return lines -} diff --git a/internal/scout/advise/k8s.go b/internal/scout/advise/k8s.go deleted file mode 100644 index 4226762730..0000000000 --- a/internal/scout/advise/k8s.go +++ /dev/null @@ -1,146 +0,0 @@ -package advise - -import ( - "context" - "fmt" - "time" - - "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: "", - Output: "", - Warnings: false, - RestConfig: restConfig, - K8sClient: k8sClient, - 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 - } - - if cfg.Output != "" { - fmt.Printf("writing to %s. This can take a few minutes...", cfg.Output) - } - - 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 []scout.Advice - 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) - if cfg.Warnings { - advice = append(advice, cpuAdvice) - } else if !cfg.Warnings && cpuAdvice.Kind != scout.WARNING { - advice = append(advice, cpuAdvice) - } - - memoryAdvice := CheckUsage(metrics.MemoryUsage, "memory", metrics.ContainerName) - if cfg.Warnings { - advice = append(advice, memoryAdvice) - } else if !cfg.Warnings && memoryAdvice.Kind != scout.WARNING { - advice = append(advice, memoryAdvice) - } - - if metrics.Storage != nil { - storageAdvice := CheckUsage(metrics.StorageUsage, "storage", metrics.ContainerName) - if cfg.Warnings { - advice = append(advice, storageAdvice) - } else if !cfg.Warnings && storageAdvice.Kind != scout.WARNING { - advice = append(advice, storageAdvice) - } - } - - if cfg.Output != "" { - OutputToFile(ctx, cfg, pod.Name, advice) - } else { - fmt.Printf("%s %s: advising...\n", scout.EmojiFingerPointRight, pod.Name) - time.Sleep(time.Millisecond * 300) - for _, adv := range advice { - fmt.Printf("\t%s\n", adv.Msg) - } - } - } - - 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 -} diff --git a/internal/scout/constants.go b/internal/scout/constants.go deleted file mode 100644 index 3549ceabd6..0000000000 --- a/internal/scout/constants.go +++ /dev/null @@ -1,12 +0,0 @@ -package scout - -const ( - ABillion float64 = 1_000_000_000 - EmojiFingerPointRight = "👉" - FlashingLightEmoji = "🚨" - SuccessEmoji = "✅" - WarningSign = "⚠️ " // why does this need an extra space to align?!?! - HEALTHY = "HEALTHY" - WARNING = "WARNING" - DANGER = "DANGER" -) diff --git a/internal/scout/helpers.go b/internal/scout/helpers.go deleted file mode 100644 index 1c917d3b94..0000000000 --- a/internal/scout/helpers.go +++ /dev/null @@ -1,24 +0,0 @@ -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 -} diff --git a/internal/scout/kube/kube.go b/internal/scout/kube/kube.go deleted file mode 100644 index 7dc0a6f0e0..0000000000 --- a/internal/scout/kube/kube.go +++ /dev/null @@ -1,353 +0,0 @@ -package kube - -import ( - "bytes" - "context" - "fmt" - "os" - "regexp" - "strconv" - "strings" - - "github.com/charmbracelet/lipgloss" - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/src-cli/internal/scout" - "gopkg.in/inf.v0" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/client-go/kubernetes/scheme" - "k8s.io/client-go/tools/remotecommand" - metav1beta1 "k8s.io/metrics/pkg/apis/metrics/v1beta1" -) - -// GetPods fetches all pods in a given namespace. -func GetPods(ctx context.Context, cfg *scout.Config) ([]corev1.Pod, error) { - podInterface := cfg.K8sClient.CoreV1().Pods(cfg.Namespace) - podList, err := podInterface.List(ctx, metav1.ListOptions{}) - if err != nil { - return []corev1.Pod{}, errors.Wrap(err, "could not list pods") - } - - if len(podList.Items) == 0 { - msg := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) - fmt.Println(msg.Render(` - No pods exist in this namespace. - Did you mean to use the --namespace flag? - - If you are attempting to check - resources for a docker deployment, you - must use the --docker flag. - See --help for more info. - `)) - os.Exit(1) - } - - return podList.Items, nil -} - -// GetPod returns a pod object with the given name from a list of pods. -func GetPod(podName string, pods []corev1.Pod) (corev1.Pod, error) { - for _, p := range pods { - if p.Name == podName { - return p, nil - } - } - return corev1.Pod{}, errors.New("no pod with this name exists in this namespace") -} - -// GetPodMetrics fetches metrics for a given pod from the Kubernetes Metrics API. -// It accepts: -// - ctx: The context for the request -// - cfg: The scout config containing Kubernetes clientsets -// - pod: The pod specification -// -// It returns: -// - podMetrics: The PodMetrics object containing metrics for the pod -// - Any error that occurred while fetching the metrics -func GetPodMetrics(ctx context.Context, cfg *scout.Config, pod corev1.Pod) (*metav1beta1.PodMetrics, error) { - podMetrics, err := cfg.MetricsClient. - MetricsV1beta1(). - PodMetricses(cfg.Namespace). - Get(ctx, pod.Name, metav1.GetOptions{}) - if err != nil { - return nil, errors.Wrap(err, "failed to get pod metrics") - } - - return podMetrics, nil -} - -// GetLimits generates resource limits for containers in a pod. -// -// It accepts: -// - ctx: The context for the request -// - cfg: The scout config containing Kubernetes clientsets -// - pod: The pod specification -// - containerMetrics: A pointer to a ContainerMetrics struct to populate -// -// It populates the containerMetrics.Limits field with a map of container names -// to resource limits (CPU, memory, storage) for each container in the pod. -// -// It returns: -// - Any error that occurred while fetching resource limits -func AddLimits(ctx context.Context, cfg *scout.Config, pod *corev1.Pod, containerMetrics *scout.ContainerMetrics) error { - for _, container := range pod.Spec.Containers { - containerName := container.Name - capacity, err := GetPvcCapacity(ctx, cfg, container, pod) - if err != nil { - return errors.Wrap(err, "while getting storage capacity of PV") - } - - rsrcs := scout.Resources{ - Cpu: container.Resources.Limits.Cpu().ToDec(), - Memory: container.Resources.Limits.Memory().ToDec(), - Storage: capacity, - } - containerMetrics.Limits[containerName] = rsrcs - } - return nil -} - -// GetUsage generates resource usage statistics for a Kubernetes container. -// -// It accepts: -// - ctx: The context for the request -// - cfg: The scout config containing Kubernetes clientsets -// - metrics: Container resource limits -// - pod: The pod specification -// - container: Container metrics from the Metrics API -// -// It returns: -// - usageStats: A UsageStats struct containing the resource usage info -// - Any error that occurred while generating the usage stats -func GetUsage( - ctx context.Context, - cfg *scout.Config, - metrics scout.ContainerMetrics, - pod corev1.Pod, - container metav1beta1.ContainerMetrics, -) (scout.UsageStats, error) { - var usageStats scout.UsageStats - usageStats.ContainerName = container.Name - - cpuUsage, err := GetRawUsage(container.Usage, "cpu") - if err != nil { - return usageStats, errors.Wrap(err, "failed to get raw CPU usage") - } - - memUsage, err := GetRawUsage(container.Usage, "memory") - if err != nil { - return usageStats, errors.Wrap(err, "failed to get raw memory usage") - } - - limits := metrics.Limits[container.Name] - - var storageCapacity float64 - var storageUsage float64 - if limits.Storage != nil { - storageCapacity, storageUsage, err = GetStorageUsage(ctx, cfg, pod.Name, container.Name) - if err != nil { - return usageStats, errors.Wrap(err, "failed to get storage usage") - } - } - - usageStats.CpuCores = limits.Cpu - usageStats.CpuUsage = scout.GetPercentage( - cpuUsage, - limits.Cpu.AsApproximateFloat64()*scout.ABillion, - ) - - usageStats.Memory = limits.Memory - usageStats.MemoryUsage = scout.GetPercentage( - memUsage, - limits.Memory.AsApproximateFloat64(), - ) - - if limits.Storage == nil { - storageDec := *inf.NewDec(0, 0) - usageStats.Storage = resource.NewDecimalQuantity(storageDec, resource.Format("DecimalSI")) - } else { - usageStats.Storage = limits.Storage - } - - usageStats.StorageUsage = scout.GetPercentage( - storageUsage, - storageCapacity, - ) - - if metrics.Limits[container.Name].Storage == nil { - usageStats.Storage = nil - } - - return usageStats, nil -} - -// GetRawUsage returns the raw usage value for a given resource type from a Kubernetes ResourceList. -// -// It accepts: -// - usages: A Kubernetes ResourceList containing usage values -// - targetKey: The resource type to get the usage for (e.g. "cpu" or "memory") -// -// It returns: -// - The raw usage value for the target resource type -// - Any error that occurred while parsing the usage value -func GetRawUsage(usages corev1.ResourceList, targetKey string) (float64, error) { - var usage *inf.Dec - - for key, val := range usages { - if key.String() == targetKey { - usage = val.AsDec().SetScale(0) - } - } - - toFloat, err := strconv.ParseFloat(usage.String(), 64) - if err != nil { - return 0, errors.Wrap(err, "failed to convert inf.Dec type to float") - } - - return toFloat, nil -} - -// GetPvcCapacity returns the storage capacity of a PersistentVolumeClaim mounted to a container. -// -// It accepts: -// - ctx: The context for the request -// - cfg: The scout config containing Kubernetes clientsets -// - container: The container specification -// - pod: The pod specification -// -// It returns: -// - The storage capacity of the PVC in bytes -// - Any error that occurred while fetching the PVC -// -// If no PVC is mounted to the container, nil is returned for the capacity and no error. -func GetPvcCapacity(ctx context.Context, cfg *scout.Config, container corev1.Container, pod *corev1.Pod) (*resource.Quantity, error) { - for _, vm := range container.VolumeMounts { - for _, v := range pod.Spec.Volumes { - if v.Name == vm.Name && v.PersistentVolumeClaim != nil { - pvc, err := cfg.K8sClient. - CoreV1(). - PersistentVolumeClaims(cfg.Namespace). - Get( - ctx, - v.PersistentVolumeClaim.ClaimName, - metav1.GetOptions{}, - ) - if err != nil { - return nil, errors.Wrapf( - err, - "failed to get PVC %s", - v.PersistentVolumeClaim.ClaimName, - ) - } - return pvc.Status.Capacity.Storage().ToDec(), nil - } - } - } - return nil, nil -} - -// GetStorageUsage returns the storage capacity and usage for a given pod and container. -// -// It accepts: -// - ctx: The context for the request -// - cfg: The scout config containing Kubernetes clientsets -// - podName: The name of the pod -// - containerName: The name of the container -// -// It returns: -// - storageCapacity: The total storage capacity for the container in bytes -// - storageUsage: The used storage for the container in bytes -// - Any error that occurred while fetching the storage usage -func GetStorageUsage( - ctx context.Context, - cfg *scout.Config, - podName string, - containerName string, -) (float64, float64, error) { - var storageCapacity float64 - var storageUsage float64 - - stateless := []string{ - "cadvisor", - "pgsql-exporter", - "executor", - "dind", - "github-proxy", - "jaeger", - "node-exporter", - "otel-agent", - "otel-collector", - "precise-code-intel-worker", - "redis-exporter", - "repo-updater", - "frontend", - "syntect-server", - "worker", - } - - // if pod is stateless, return 0 for capacity and usage - if scout.Contains(stateless, containerName) { - return storageCapacity, storageUsage, nil - } - - req := cfg.K8sClient.CoreV1().RESTClient().Post(). - Resource("pods"). - Name(podName). - Namespace(cfg.Namespace). - SubResource("exec") - - req.VersionedParams(&corev1.PodExecOptions{ - Container: containerName, - Command: []string{"df"}, - Stdin: false, - Stdout: true, - Stderr: true, - TTY: false, - }, scheme.ParameterCodec) - - exec, err := remotecommand.NewSPDYExecutor(cfg.RestConfig, "POST", req.URL()) - if err != nil { - return 0, 0, err - } - - var stdout, stderr bytes.Buffer - err = exec.StreamWithContext(ctx, remotecommand.StreamOptions{ - Stdout: &stdout, - Stderr: &stderr, - }) - if err != nil { - return 0, 0, err - } - - lines := strings.Split(stdout.String(), "\n") - for _, line := range lines[1 : len(lines)-1] { - fields := strings.Fields(line) - - if acceptedFileSystem(fields[0]) { - capacityFloat, err := strconv.ParseFloat(fields[1], 64) - if err != nil { - return 0, 0, errors.Wrap(err, "could not convert string to float64") - } - - usageFloat, err := strconv.ParseFloat(fields[2], 64) - if err != nil { - return 0, 0, errors.Wrap(err, "could not convert string to float64") - } - return capacityFloat, usageFloat, nil - } - } - - return 0, 0, nil -} - -// acceptedFileSystem checks if a given file system, represented -// as a string, is an accepted system. -// -// It returns: -// - True if the file system matches the pattern '/dev/sd[a-z]$' -// - False otherwise -func acceptedFileSystem(fileSystem string) bool { - matched, _ := regexp.MatchString(`/dev/sd[a-z]$`, fileSystem) - return matched -} diff --git a/internal/scout/kube/kube_test.go b/internal/scout/kube/kube_test.go deleted file mode 100644 index c45080dced..0000000000 --- a/internal/scout/kube/kube_test.go +++ /dev/null @@ -1,84 +0,0 @@ -package kube - -import ( - "testing" - - corev1 "k8s.io/api/core/v1" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" -) - -func TestAcceptedFileSystem(t *testing.T) { - cases := []struct { - name string - filesystem string - want bool - }{ - { - name: "should return true if filesystem matches 'matched' regular expression", - filesystem: "/dev/sda", - want: true, - }, - { - name: "should return false if filesystem doesn't match 'matched' regular expression", - filesystem: "/dev/sda1", - want: false, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got := acceptedFileSystem(tc.filesystem) - if got != tc.want { - t.Errorf("got %v want %v", got, tc.want) - } - }) - } -} - -func TestGetPod(t *testing.T) { - cases := []struct { - name string - podList []corev1.Pod - wantPod string - }{ - { - name: "should return correct pod", - podList: []corev1.Pod{ - *testPod("sg", "soucegraph-frontend-0", "sourcegraph-frontend"), - *testPod("sg", "gitserver-0", "gitserver"), - *testPod("sg", "indexed-search-0", "indexed-search"), - }, - wantPod: "gitserver-0", - }, - { - name: "should return empty pod if pod not found", - podList: []corev1.Pod{ - *testPod("sg", "soucegraph-frontend-0", "sourcegraph-frontend"), - *testPod("sg", "indexed-search-0", "indexed-search"), - }, - wantPod: "", - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got, _ := GetPod("gitserver-0", tc.podList) - gotPod := got.Name - - if gotPod != tc.wantPod { - t.Errorf("want pod %s, got pod %s", tc.wantPod, gotPod) - } - }) - } -} - -func testPod(namespace, podName, containerName string) *corev1.Pod { - return &corev1.Pod{ - ObjectMeta: metav1.ObjectMeta{ - Namespace: namespace, - Name: podName, - }, - } -} diff --git a/internal/scout/resource/resource.go b/internal/scout/resource/resource.go deleted file mode 100644 index 6be8a873e0..0000000000 --- a/internal/scout/resource/resource.go +++ /dev/null @@ -1,122 +0,0 @@ -package resource - -import ( - "context" - "fmt" - "os" - - "github.com/charmbracelet/bubbles/table" - "github.com/charmbracelet/lipgloss" - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/src-cli/internal/scout" - "github.com/sourcegraph/src-cli/internal/scout/kube" - "github.com/sourcegraph/src-cli/internal/scout/style" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" -) - -type Option = func(config *scout.Config) - -func WithNamespace(namespace string) Option { - return func(config *scout.Config) { - config.Namespace = namespace - } -} - -// K8s prints the CPU and memory resource limits and requests for all pods in the given namespace. -func K8s(ctx context.Context, clientSet *kubernetes.Clientset, restConfig *rest.Config, opts ...Option) error { - cfg := &scout.Config{ - Namespace: "default", - Pod: "", - RestConfig: restConfig, - K8sClient: clientSet, - MetricsClient: nil, - } - - for _, opt := range opts { - opt(cfg) - } - - return listPodResources(ctx, cfg) -} - -func listPodResources(ctx context.Context, cfg *scout.Config) error { - pods, err := kube.GetPods(ctx, cfg) - if err != nil { - return errors.Wrap(err, "could not get pods") - } - - columns := []table.Column{ - {Title: "CONTAINER", Width: 20}, - {Title: "CPU LIMITS", Width: 10}, - {Title: "CPU REQUESTS", Width: 12}, - {Title: "MEM LIMITS", Width: 10}, - {Title: "MEM REQUESTS", Width: 12}, - {Title: "CAPACITY", Width: 8}, - } - - if len(pods) == 0 { - msg := lipgloss.NewStyle().Foreground(lipgloss.Color("#FFA500")) - fmt.Println(msg.Render(` - No pods exist in this namespace. - Did you mean to use the --namespace flag? - - If you are attemptying to check - resources for a docker deployment, you - must use the --docker flag. - See --help for more info. - `)) - os.Exit(1) - } - - var rows []table.Row - for _, pod := range pods { - if pod.GetNamespace() == cfg.Namespace { - for _, container := range pod.Spec.Containers { - cpuLimits := container.Resources.Limits.Cpu() - cpuRequests := container.Resources.Requests.Cpu() - memLimits := container.Resources.Limits.Memory() - memRequests := container.Resources.Requests.Memory() - - capacity, err := kube.GetPvcCapacity(ctx, cfg, container, &pod) - if err != nil { - return err - } - - row := table.Row{ - container.Name, - cpuLimits.String(), - cpuRequests.String(), - memLimits.String(), - memRequests.String(), - capacity.String(), - } - rows = append(rows, row) - } - } - } - - style.ResourceTable(columns, rows) - return nil -} - -// getMemUnits converts a byte value to the appropriate memory unit. -func getMemUnits(valToConvert int64) (string, int64, error) { - if valToConvert < 0 { - return "", valToConvert, fmt.Errorf("invalid memory value: %d", valToConvert) - } - - var memUnit string - switch { - case valToConvert < 1000000: - memUnit = "KB" - case valToConvert < 1000000000: - memUnit = "MB" - valToConvert = valToConvert / 1000000 - default: - memUnit = "GB" - valToConvert = valToConvert / 1000000000 - } - - return memUnit, valToConvert, nil -} diff --git a/internal/scout/resource/resource_test.go b/internal/scout/resource/resource_test.go deleted file mode 100644 index f0cbd74d36..0000000000 --- a/internal/scout/resource/resource_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package resource - -import ( - "fmt" - "testing" -) - -func TestGetMemUnits(t *testing.T) { - cases := []struct { - name string - param int64 - wantUnit string - wantValue int64 - wantError error - }{ - { - name: "convert bytes below a million to KB", - param: 999999, - wantUnit: "KB", - wantValue: 999999, - wantError: nil, - }, - { - name: "convert bytes below a billion to MB", - param: 999999999, - wantUnit: "MB", - wantValue: 999, - wantError: nil, - }, - { - name: "convert bytes above a billion to GB", - param: 12999999900, - wantUnit: "GB", - wantValue: 12, - wantError: nil, - }, - { - name: "return error for a negative number", - param: -300, - wantUnit: "", - wantValue: -300, - wantError: fmt.Errorf("invalid memory value: %d", -300), - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - gotUnit, gotValue, gotError := getMemUnits(tc.param) - - if gotUnit != tc.wantUnit { - t.Errorf("got %s want %s", gotUnit, tc.wantUnit) - } - - if gotValue != tc.wantValue { - t.Errorf("got %v want %v", gotValue, tc.wantValue) - } - - if gotError == nil && tc.wantError != nil { - t.Error("got nil want error") - } - - if gotError != nil && tc.wantError == nil { - t.Error("got error want nil") - } - }) - } -} diff --git a/internal/scout/style/resource_table.go b/internal/scout/style/resource_table.go deleted file mode 100644 index db5455ffdb..0000000000 --- a/internal/scout/style/resource_table.go +++ /dev/null @@ -1,174 +0,0 @@ -package style - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "text/tabwriter" - - "github.com/atotto/clipboard" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -var resourceTableStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - -type resourceTableModel struct { - table table.Model -} - -func (m resourceTableModel) Init() tea.Cmd { return nil } - -func (m resourceTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc", "q", "ctrl+c": - return m, tea.Quit - case "c": - m.copyRowToClipboard(m.table.SelectedRow()) - copiedMessage := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#32CD32")). - Render(fmt.Sprintf( - "Copied resource allocations for '%s' to clipboard", - m.table.SelectedRow()[0], - )) - return m, tea.Batch( - tea.Printf("%s", copiedMessage), - ) - case "C": - tmpDir := os.TempDir() - filePath := filepath.Join(tmpDir, "resource-dump.txt") - m.dump(m.table.Rows(), filePath) - savedMessage := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#32CD32")). - Render(fmt.Sprintf( - "saved resource allocations to %s", - filePath, - )) - return m, tea.Batch( - tea.Printf("%s", savedMessage), - ) - } - } - m.table, cmd = m.table.Update(msg) - return m, cmd -} - -func (m resourceTableModel) View() string { - s := "\n > Press 'j' and 'k' to go up and down\n" - s += " > Press 'c' to copy highlighted row to clipboard\n" - s += " > Press 'C' to copy all rows to a file\n" - s += " > Press 'q' to quit\n\n" - s += resourceTableStyle.Render(m.table.View()) + "\n" - return s -} - -func (m resourceTableModel) dump(rows []table.Row, filePath string) error { - dumpFile, err := os.Create(filePath) - if err != nil { - return errors.Wrap(err, "error while creating new file") - } - defer dumpFile.Close() - - tw := tabwriter.NewWriter(dumpFile, 0, 0, 3, ' ', 0) - defer tw.Flush() - - // default to docker terms - headers := []string{ - "NAME", - "CPU CORES", - "CPU SHARES", - "MEM LIMITS", - "MEM RESERVATIONS", - } - - // kubernetes rows will always have 6 items - // change column headers to reflect k8s terms - if len(rows[0]) == 6 { - headers = []string{ - "NAME", - "CPU LIMITS", - "CPU REQUESTS", - "MEM LIMITS", - "MEM REQUESTS", - "CAPACITY", - } - } - - fmt.Fprintf(tw, "%s\n", strings.Join(headers, "\t")) - - for _, row := range rows { - values := []string{row[0], row[1], row[2], row[3], row[4]} - if len(row) == 6 { - values = append(values, row[5]) - } - fmt.Fprintf(tw, "%s\n", strings.Join(values, "\t")) - } - return nil -} - -func (m resourceTableModel) copyRowToClipboard(row table.Row) { - var containerInfo string - - // default to docker headers - headers := []string{ - "NAME", - "CPU CORES", - "CPU SHARES", - "MEM LIMITS", - "MEM RESERVATIONS", - } - - // kubernetes rows will always have 6 items - // change column headers to reflect k8s terms - if len(row) == 6 { - headers = []string{ - "NAME", - "CPU LIMITS", - "CPU REQUESTS", - "MEM LIMITS", - "MEM REQUESTS", - "CAPACITY", - } - } - - for i, header := range headers { - containerInfo += fmt.Sprintf("%s: %s\n", header, row[i]) - } - - clipboard.WriteAll(containerInfo) -} - -func ResourceTable(columns []table.Column, rows []table.Row) { - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(14), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) - - m := resourceTableModel{t} - if _, err := tea.NewProgram(m).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/internal/scout/style/usage_table.go b/internal/scout/style/usage_table.go deleted file mode 100644 index f76ba19475..0000000000 --- a/internal/scout/style/usage_table.go +++ /dev/null @@ -1,176 +0,0 @@ -package style - -import ( - "fmt" - "os" - "path/filepath" - "strings" - "text/tabwriter" - - "github.com/atotto/clipboard" - "github.com/charmbracelet/bubbles/table" - tea "github.com/charmbracelet/bubbletea" - "github.com/charmbracelet/lipgloss" - "github.com/sourcegraph/sourcegraph/lib/errors" -) - -var usageTableStyle = lipgloss.NewStyle(). - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")) - -type usageTableModel struct { - table table.Model -} - -func (m usageTableModel) Init() tea.Cmd { return nil } - -func (m usageTableModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) { - var cmd tea.Cmd - switch msg := msg.(type) { - case tea.KeyMsg: - switch msg.String() { - case "esc", "q", "ctrl+c": - return m, tea.Quit - case "c": - m.copyRowToClipboard(m.table.SelectedRow()) - copiedMessage := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#32CD32")). - Render(fmt.Sprintf( - "Copied usage data for '%s' to clipboard", - m.table.SelectedRow()[0], - )) - return m, tea.Batch( - tea.Printf("%s", copiedMessage), - ) - case "C": - tmpDir := os.TempDir() - filePath := filepath.Join(tmpDir, "usage-dump.txt") - m.dump(m.table.Rows(), filePath) - savedMessage := lipgloss.NewStyle(). - Foreground(lipgloss.Color("#32CD32")). - Render(fmt.Sprintf( - "saved usage data to %s", - filePath, - )) - return m, tea.Batch( - tea.Printf("%s", savedMessage), - ) - } - } - m.table, cmd = m.table.Update(msg) - return m, cmd -} - -func (m usageTableModel) View() string { - s := "\n > Press 'j' and 'k' to go up and down\n" - s += " > Press 'c' to copy highlighted row to clipboard\n" - s += " > Press 'C' to copy all rows to a file\n" - s += " > Press 'q' to quit\n\n" - s += usageTableStyle.Render(m.table.View()) + "\n" - return s -} - -func (m usageTableModel) dump(rows []table.Row, filePath string) error { - dumpFile, err := os.Create(filePath) - if err != nil { - return errors.Wrap(err, "error while creating new file") - } - defer dumpFile.Close() - - tw := tabwriter.NewWriter(dumpFile, 5, 0, 3, ' ', 0) - defer tw.Flush() - - // default to docker terms - headers := []string{ - "Name", - "Cores", - "Usage", - "Memory", - "Usage", - } - - // kubernetes rows will always have 6 items - // change column headers to reflect k8s terms - if len(rows[0]) == 7 { - headers = []string{ - "Name", - "Cores", - "Usage", - "Memory", - "Usage", - "Storage", - "Usage", - } - } - - fmt.Fprintf(tw, "%s\n", strings.Join(headers, "\t")) - for _, row := range rows { - values := []string{row[0], row[1], row[2], row[3], row[4]} - if len(row) == 7 { - values = append(values, row[5]) - values = append(values, row[6]) - } - fmt.Fprintf(tw, "%s\n", strings.Join(values, "\t")) - } - return nil -} - -func (m usageTableModel) copyRowToClipboard(row table.Row) { - var containerInfo string - - // default to docker headers - headers := []string{ - "Name", - "Cores", - "Usage", - "Memory", - "Usage", - } - - // kubernetes rows will always have 6 items - // change column headers to reflect k8s terms - if len(row) == 7 { - headers = []string{ - "Name", - "Cores", - "Usage", - "Memory", - "Usage", - "Storage", - "Usage", - } - } - - for i, header := range headers { - containerInfo += fmt.Sprintf("%s: %s\n", header, row[i]) - } - - clipboard.WriteAll(containerInfo) -} - -func UsageTable(columns []table.Column, rows []table.Row) { - t := table.New( - table.WithColumns(columns), - table.WithRows(rows), - table.WithFocused(true), - table.WithHeight(14), - ) - - s := table.DefaultStyles() - s.Header = s.Header. - BorderStyle(lipgloss.NormalBorder()). - BorderForeground(lipgloss.Color("240")). - BorderBottom(true). - Bold(false) - s.Selected = s.Selected. - Foreground(lipgloss.Color("229")). - Background(lipgloss.Color("57")). - Bold(false) - t.SetStyles(s) - - m := usageTableModel{t} - if _, err := tea.NewProgram(m).Run(); err != nil { - fmt.Println("Error running program:", err) - os.Exit(1) - } -} diff --git a/internal/scout/types.go b/internal/scout/types.go deleted file mode 100644 index 09bf11d2c9..0000000000 --- a/internal/scout/types.go +++ /dev/null @@ -1,44 +0,0 @@ -package scout - -import ( - "k8s.io/apimachinery/pkg/api/resource" - "k8s.io/client-go/kubernetes" - "k8s.io/client-go/rest" - metricsv "k8s.io/metrics/pkg/client/clientset/versioned" -) - -type Config struct { - Namespace string - Pod string - Output string - Warnings bool - RestConfig *rest.Config - K8sClient *kubernetes.Clientset - MetricsClient *metricsv.Clientset -} - -type ContainerMetrics struct { - PodName string - Limits map[string]Resources -} - -type Resources struct { - Cpu *resource.Quantity - Memory *resource.Quantity - Storage *resource.Quantity -} - -type UsageStats struct { - ContainerName string - CpuCores *resource.Quantity - Memory *resource.Quantity - Storage *resource.Quantity - CpuUsage float64 - MemoryUsage float64 - StorageUsage float64 -} - -type Advice struct { - Kind string - Msg string -} diff --git a/internal/scout/usage/k8s.go b/internal/scout/usage/k8s.go deleted file mode 100644 index c3805938a0..0000000000 --- a/internal/scout/usage/k8s.go +++ /dev/null @@ -1,164 +0,0 @@ -package usage - -import ( - "context" - "fmt" - - "github.com/charmbracelet/bubbles/table" - "github.com/sourcegraph/sourcegraph/lib/errors" - "github.com/sourcegraph/src-cli/internal/scout" - "github.com/sourcegraph/src-cli/internal/scout/kube" - "github.com/sourcegraph/src-cli/internal/scout/style" - - corev1 "k8s.io/api/core/v1" - "k8s.io/client-go/kubernetes" - restclient "k8s.io/client-go/rest" - metricsv "k8s.io/metrics/pkg/client/clientset/versioned" -) - -func K8s( - ctx context.Context, - clientSet *kubernetes.Clientset, - metricsClient *metricsv.Clientset, - restConfig *restclient.Config, - opts ...Option, -) error { - cfg := &scout.Config{ - Namespace: "default", - Pod: "", - RestConfig: restConfig, - K8sClient: clientSet, - 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 != "" { - return renderSinglePodUsageTable(ctx, cfg, pods) - } - - return renderUsageTable(ctx, cfg, pods) -} - -// renderSinglePodUsageStats prints resource usage statistics for a single pod. -func renderSinglePodUsageTable(ctx context.Context, cfg *scout.Config, pods []corev1.Pod) error { - pod, err := kube.GetPod(cfg.Pod, pods) - if err != nil { - return errors.Wrapf(err, "could not get pod with name %s", cfg.Pod) - } - - containerMetrics := &scout.ContainerMetrics{ - PodName: cfg.Pod, - Limits: map[string]scout.Resources{}, - } - if err = kube.AddLimits(ctx, cfg, &pod, containerMetrics); err != nil { - return errors.Wrap(err, "failed to add limits to container metrics") - } - - columns := []table.Column{ - {Title: "Container", Width: 20}, - {Title: "Cores", Width: 10}, - {Title: "Usage(%)", Width: 10}, - {Title: "Memory", Width: 10}, - {Title: "Usage(%)", Width: 10}, - {Title: "Storage", Width: 10}, - {Title: "Usage(%)", Width: 10}, - } - var rows []table.Row - - podMetrics, err := kube.GetPodMetrics(ctx, cfg, pod) - if err != nil { - return errors.Wrap(err, "while attempting to fetch pod metrics") - } - - for _, container := range podMetrics.Containers { - stats, err := kube.GetUsage(ctx, cfg, *containerMetrics, pod, container) - if err != nil { - return errors.Wrapf(err, "could not compile usage data for row: %s\n", container.Name) - } - - row := makeRow(stats) - rows = append(rows, row) - } - - style.ResourceTable(columns, rows) - return nil -} - -// renderUsageTable renders a table displaying resource usage for pods. -func renderUsageTable(ctx context.Context, cfg *scout.Config, pods []corev1.Pod) error { - columns := []table.Column{ - {Title: "Container", Width: 20}, - {Title: "Cores", Width: 10}, - {Title: "Usage(%)", Width: 10}, - {Title: "Memory", Width: 10}, - {Title: "Usage(%)", Width: 10}, - {Title: "Storage", Width: 10}, - {Title: "Usage(%)", Width: 10}, - } - var rows []table.Row - - for _, pod := range pods { - containerMetrics := &scout.ContainerMetrics{ - PodName: pod.Name, - Limits: map[string]scout.Resources{}, - } - - if err := kube.AddLimits(ctx, cfg, &pod, containerMetrics); err != nil { - return errors.Wrap(err, "failed to get get container metrics") - } - - podMetrics, err := kube.GetPodMetrics(ctx, cfg, pod) - if err != nil { - return errors.Wrap(err, "while attempting to fetch pod metrics") - } - - for _, container := range podMetrics.Containers { - stats, err := kube.GetUsage(ctx, cfg, *containerMetrics, pod, container) - if err != nil { - return errors.Wrapf(err, "could not compile usage data for row %s\n", container.Name) - } - - row := makeRow(stats) - rows = append(rows, row) - } - } - - style.UsageTable(columns, rows) - return nil -} - -// makeRow generates a table row containing resource usage data for a container. -// It returns: -// - A table.Row containing the resource usage information -// - An error if there was an issue generating the row -func makeRow(usageStats scout.UsageStats) table.Row { - if usageStats.Storage == nil { - return table.Row{ - usageStats.ContainerName, - usageStats.CpuCores.String(), - fmt.Sprintf("%.2f%%", usageStats.CpuUsage), - usageStats.Memory.String(), - fmt.Sprintf("%.2f%%", usageStats.MemoryUsage), - "-", - "-", - } - } - - return table.Row{ - usageStats.ContainerName, - usageStats.CpuCores.String(), - fmt.Sprintf("%.2f%%", usageStats.CpuUsage), - usageStats.Memory.String(), - fmt.Sprintf("%.2f%%", usageStats.MemoryUsage), - usageStats.Storage.String(), - fmt.Sprintf("%.2f%%", usageStats.StorageUsage), - } -} diff --git a/internal/scout/usage/usage.go b/internal/scout/usage/usage.go deleted file mode 100644 index df4cb31b7a..0000000000 --- a/internal/scout/usage/usage.go +++ /dev/null @@ -1,19 +0,0 @@ -package usage - -import ( - "github.com/sourcegraph/src-cli/internal/scout" -) - -type Option = func(config *scout.Config) - -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 - } -} diff --git a/internal/scout/usage/usage_test.go b/internal/scout/usage/usage_test.go deleted file mode 100644 index 14cc444b28..0000000000 --- a/internal/scout/usage/usage_test.go +++ /dev/null @@ -1,164 +0,0 @@ -package usage - -import ( - "testing" - - "github.com/sourcegraph/src-cli/internal/scout" - "github.com/sourcegraph/src-cli/internal/scout/kube" - corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/api/resource" -) - -func TestGetPercentage(t *testing.T) { - cases := []struct { - name string - x float64 - y float64 - want float64 - shouldError bool - }{ - { - name: "should return 0 if x is 0", - x: 0, - y: 1, - want: 0, - shouldError: false, - }, - { - name: "should return correct percentage", - x: 36, - y: 72, - want: 50, - shouldError: false, - }, - { - name: "should return correct percentage", - x: 75, - y: 100, - want: 75, - shouldError: false, - }, - { - name: "should return correct percentages over 100%", - x: 3800, - y: 2000, - want: 190, - shouldError: false, - }, - { - name: "should return 0 if y is 0", - x: 75, - y: 0, - want: 0, - shouldError: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got := scout.GetPercentage(tc.x, tc.y) - - if got != tc.want { - t.Errorf("got %.2f want %.2f", tc.want, got) - } - }) - } -} - -func TestGetRawUsage(t *testing.T) { - cases := []struct { - name string - cpu *resource.Quantity - mem *resource.Quantity - targetKey string - want float64 - shouldError bool - }{ - { - name: "return cpu usage in nanocores", - cpu: resource.NewQuantity(2756053, resource.Format("BinarySI")), - mem: resource.NewQuantity(838374, resource.Format("BinarySI")), - targetKey: "cpu", - want: 2756053, - shouldError: false, - }, - { - name: "return memory usage in KiB", - cpu: resource.NewQuantity(8926483, resource.Format("BinarySI")), - mem: resource.NewQuantity(2332343, resource.Format("BinarySI")), - targetKey: "memory", - want: 2332343, - shouldError: false, - }, - { - name: "should error with non-existant targetKey", - cpu: resource.NewQuantity(8, resource.Format("BinarySI")), - mem: resource.NewQuantity(2, resource.Format("BinarySI")), - targetKey: "mem", - want: 0, - shouldError: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - resourceList := resourceListHelper(tc.cpu, tc.mem) - got, err := kube.GetRawUsage(resourceList, tc.targetKey) - if !tc.shouldError && err != nil { - t.Errorf("unexpected error: %v", err) - } - - want := tc.want - if got != want { - t.Errorf("got %v, want %v", got, want) - } - }) - } -} - -func TestContains(t *testing.T) { - cases := []struct { - name string - s []string - val string - want bool - }{ - { - name: "should return true if given slice contains given value", - s: []string{"this", "is", "a", "unit", "test"}, - val: "unit", - want: true, - }, - { - name: "should return false if given slice does not contains given value", - s: []string{"this", "is", "a", "unit", "test"}, - val: "foobar", - want: false, - }, - { - name: "should return true if given slice contains for than one instance of the given value", - s: []string{"this", "is", "a", "unit", "unit", "test", "unit"}, - val: "unit", - want: true, - }, - } - - for _, tc := range cases { - tc := tc - t.Run(tc.name, func(t *testing.T) { - got := scout.Contains(tc.s, tc.val) - if got != tc.want { - t.Errorf("got %v, want %v", got, tc.want) - } - }) - } -} - -func resourceListHelper(cpu *resource.Quantity, mem *resource.Quantity) corev1.ResourceList { - return corev1.ResourceList{ - corev1.ResourceCPU: *cpu, - corev1.ResourceMemory: *mem, - } -}