15 |
16 | Goldilocks is a utility that can help you identify a starting point for resource requests and limits.
17 |
18 | # Documentation
19 | Check out the [documentation at docs.fairwinds.com](https://goldilocks.docs.fairwinds.com/)
20 |
21 | ## How can this help with my resource settings?
22 |
23 | By using the kubernetes [vertical-pod-autoscaler](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) in recommendation mode, we can see a suggestion for resource requests on each of our apps. This tool creates a VPA for each workload in a namespace and then queries them for information.
24 |
25 | Once your VPAs are in place, you'll see recommendations appear in the Goldilocks dashboard:
26 |
27 |
28 |
29 |
30 |
31 |
32 | ## Join the Fairwinds Open Source Community
33 |
34 | The goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap,
35 | and network with fellow Kubernetes users.
36 | [Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-2na8gtwb4-DGQ4qgmQbczQyB2NlFlYQQ)
37 | or
38 | [join the user group](https://www.fairwinds.com/open-source-software-user-group) to get involved!
39 |
40 |
41 |
43 |
44 |
45 | ## Other Projects from Fairwinds
46 |
47 | Enjoying Goldilocks? Check out some of our other projects:
48 | * [Polaris](https://github.com/FairwindsOps/Polaris) - Audit, enforce, and build policies for Kubernetes resources, including over 20 built-in checks for best practices
49 | * [Pluto](https://github.com/FairwindsOps/Pluto) - Detect Kubernetes resources that have been deprecated or removed in future versions
50 | * [Nova](https://github.com/FairwindsOps/Nova) - Check to see if any of your Helm charts have updates available
51 | * [rbac-manager](https://github.com/FairwindsOps/rbac-manager) - Simplify the management of RBAC in your Kubernetes clusters
52 |
53 | Or [check out the full list](https://www.fairwinds.com/open-source-software?utm_source=goldilocks&utm_medium=goldilocks&utm_campaign=goldilocks)
54 | ## Fairwinds Insights
55 | If you're interested in running Goldilocks in multiple clusters,
56 | tracking the results over time, integrating with Slack, Datadog, and Jira,
57 | or unlocking other functionality, check out
58 | [Fairwinds Insights](https://fairwinds.com/pricing),
59 | a platform for auditing and enforcing policy in Kubernetes clusters.
60 |
61 |
62 |
63 |
64 |
--------------------------------------------------------------------------------
/ROADMAP.md:
--------------------------------------------------------------------------------
1 | # goldilocks Roadmap
2 |
3 | Most of the future roadmap of this project will be to improve the quality of the code, as well as the usability of the dashboard. The basic functionality is simple: provide resource recommendation aggregation.
4 |
5 | ## Potential Future Work
6 |
7 | * Much better unit testing. See #5
8 | * Container name exclusion. See #7
9 | * Deployment reconciliation - Allow the labelling of deployments instead of just namespaces
10 | * Deployment - be able to deploy VPA, prometheus, and goldilocks with Helm.
11 |
--------------------------------------------------------------------------------
/cmd/controller.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "os"
19 | "os/signal"
20 | "syscall"
21 |
22 | "github.com/spf13/cobra"
23 | "k8s.io/klog/v2"
24 |
25 | "github.com/fairwindsops/goldilocks/pkg/controller"
26 | "github.com/fairwindsops/goldilocks/pkg/vpa"
27 | )
28 |
29 | var onByDefault bool
30 | var includeNamespaces []string
31 | var ignoreControllerKind []string
32 | var excludeNamespaces []string
33 | var dryRun bool
34 |
35 | func init() {
36 | rootCmd.AddCommand(controllerCmd)
37 | controllerCmd.PersistentFlags().BoolVar(&onByDefault, "on-by-default", false, "Add goldilocks to every namespace that isn't explicitly excluded.")
38 | controllerCmd.PersistentFlags().BoolVar(&dryRun, "dry-run", false, "If true, don't mutate resources, just list what would have been created.")
39 | controllerCmd.PersistentFlags().StringSliceVar(&includeNamespaces, "include-namespaces", []string{}, "Comma delimited list of namespaces to include from recommendations.")
40 | controllerCmd.PersistentFlags().StringSliceVar(&excludeNamespaces, "exclude-namespaces", []string{}, "Comma delimited list of namespaces to exclude from recommendations.")
41 | controllerCmd.PersistentFlags().StringSliceVar(&ignoreControllerKind, "ignore-controller-kind", []string{}, "Comma delimited list of controller kinds to exclude from recommendations.")
42 | }
43 |
44 | var controllerCmd = &cobra.Command{
45 | Use: "controller",
46 | Short: "Run goldilocks as a controller inside a kubernetes cluster.",
47 | Long: `Run goldilocks as a controller.`,
48 | Run: func(cmd *cobra.Command, args []string) {
49 | vpaReconciler := vpa.GetInstance()
50 | vpaReconciler.OnByDefault = onByDefault
51 | vpaReconciler.IncludeNamespaces = includeNamespaces
52 | vpaReconciler.ExcludeNamespaces = excludeNamespaces
53 | vpaReconciler.IgnoreControllerKind = ignoreControllerKind
54 |
55 | klog.V(4).Infof("Starting controller with Reconciler: %+v", vpaReconciler)
56 |
57 | // create a channel for sending a stop to kube watcher threads
58 | stop := make(chan bool, 1)
59 | defer close(stop)
60 | go controller.NewController(stop)
61 |
62 | // create a channel to respond to signals
63 | signals := make(chan os.Signal, 1)
64 | defer close(signals)
65 |
66 | signal.Notify(signals, syscall.SIGTERM)
67 | signal.Notify(signals, syscall.SIGINT)
68 | s := <-signals
69 | stop <- true
70 | klog.Infof("Exiting, got signal: %v", s)
71 | },
72 | }
73 |
--------------------------------------------------------------------------------
/cmd/create.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 | "os"
20 |
21 | "github.com/spf13/cobra"
22 | "k8s.io/klog/v2"
23 |
24 | "github.com/fairwindsops/goldilocks/pkg/kube"
25 | "github.com/fairwindsops/goldilocks/pkg/vpa"
26 | )
27 |
28 | var dryrun bool
29 |
30 | func init() {
31 | rootCmd.AddCommand(createCmd)
32 | createCmd.PersistentFlags().BoolVarP(&dryrun, "dry-run", "", false, "Don't actually create the VPAs, just list which ones would get created.")
33 | createCmd.PersistentFlags().StringVarP(&nsName, "namespace", "n", "default", "Namespace to install the VPA objects in.")
34 | }
35 |
36 | var createCmd = &cobra.Command{
37 | Use: "create-vpas",
38 | Short: "Create VPAs",
39 | Long: `Create a VPA for every workload in the specified namespace.`,
40 | Run: func(cmd *cobra.Command, args []string) {
41 | klog.V(4).Infof("Starting to create the VPA objects in namespace: %s", nsName)
42 | kubeClient := kube.GetInstance()
43 | namespace, err := kube.GetNamespace(kubeClient, nsName)
44 | if err != nil {
45 | fmt.Println("Error getting namespace. Exiting.")
46 | os.Exit(1)
47 | }
48 | reconciler := vpa.GetInstance()
49 | reconciler.DryRun = dryrun
50 | errReconcile := vpa.GetInstance().ReconcileNamespace(namespace)
51 | if errReconcile != nil {
52 | fmt.Println("Errors encountered during reconciliation.")
53 | os.Exit(1)
54 | }
55 | },
56 | }
57 |
--------------------------------------------------------------------------------
/cmd/dashboard.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 | "net/http"
20 | "strings"
21 |
22 | "github.com/spf13/cobra"
23 | "k8s.io/apimachinery/pkg/util/sets"
24 | "k8s.io/klog/v2"
25 |
26 | "github.com/fairwindsops/goldilocks/pkg/dashboard"
27 | )
28 |
29 | var (
30 | serverPort int
31 | showAllVPAs bool
32 | basePath string
33 | insightsHost string
34 | enableCost bool
35 | )
36 |
37 | func init() {
38 | rootCmd.AddCommand(dashboardCmd)
39 | dashboardCmd.PersistentFlags().IntVarP(&serverPort, "port", "p", 8080, "The port to serve the dashboard on.")
40 | dashboardCmd.PersistentFlags().StringVarP(&excludeContainers, "exclude-containers", "e", "", "Comma delimited list of containers to exclude from recommendations.")
41 | dashboardCmd.PersistentFlags().BoolVar(&onByDefault, "on-by-default", false, "Display every namespace that isn't explicitly excluded.")
42 | dashboardCmd.PersistentFlags().BoolVar(&showAllVPAs, "show-all", false, "Display every VPA, even if it isn't managed by Goldilocks")
43 | dashboardCmd.PersistentFlags().StringVar(&basePath, "base-path", "/", "Path on which the dashboard is served.")
44 | dashboardCmd.PersistentFlags().BoolVar(&enableCost, "enable-cost", true, "If set to false, the cost integration will be disabled on the dashboard.")
45 | dashboardCmd.PersistentFlags().StringVar(&insightsHost, "insights-host", "https://insights.fairwinds.com", "Insights host for retrieving optional cost data.")
46 | }
47 |
48 | var dashboardCmd = &cobra.Command{
49 | Use: "dashboard",
50 | Short: "Run the goldilocks dashboard that will show recommendations.",
51 | Long: `Run the goldilocks dashboard that will show recommendations.`,
52 | Run: func(cmd *cobra.Command, args []string) {
53 | var validBasePath = validateBasePath(basePath)
54 | router := dashboard.GetRouter(
55 | dashboard.OnPort(serverPort),
56 | dashboard.BasePath(validBasePath),
57 | dashboard.ExcludeContainers(sets.New[string](strings.Split(excludeContainers, ",")...)),
58 | dashboard.OnByDefault(onByDefault),
59 | dashboard.ShowAllVPAs(showAllVPAs),
60 | dashboard.InsightsHost(insightsHost),
61 | dashboard.EnableCost(enableCost),
62 | )
63 | http.Handle("/", router)
64 | klog.Infof("Starting goldilocks dashboard server on port %d and basePath %v", serverPort, validBasePath)
65 | klog.Fatalf("%v", http.ListenAndServe(fmt.Sprintf(":%d", serverPort), nil))
66 | },
67 | }
68 |
69 | func validateBasePath(path string) string {
70 | if path == "" || path == "/" {
71 | return "/"
72 | }
73 |
74 | if !strings.HasPrefix(path, "/") {
75 | path = "/" + path
76 | }
77 |
78 | if !strings.HasSuffix(path, "/") {
79 | path = path + "/"
80 | }
81 |
82 | return path
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/delete.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 | "os"
20 |
21 | "github.com/spf13/cobra"
22 | "k8s.io/klog/v2"
23 |
24 | "github.com/fairwindsops/goldilocks/pkg/kube"
25 | "github.com/fairwindsops/goldilocks/pkg/vpa"
26 | )
27 |
28 | func init() {
29 | rootCmd.AddCommand(deleteCmd)
30 | deleteCmd.PersistentFlags().BoolVarP(&dryrun, "dry-run", "", false, "Don't actually create the VPAs, just list which ones would get created.")
31 | deleteCmd.PersistentFlags().StringVarP(&nsName, "namespace", "n", "default", "Namespace to install the VPA objects in.")
32 | }
33 |
34 | var deleteCmd = &cobra.Command{
35 | Use: "delete-vpas",
36 | Short: "Delete VPAs",
37 | Long: `Delete VPAs created by this tool in a namespace.`,
38 | Run: func(cmd *cobra.Command, args []string) {
39 | klog.V(4).Infof("Starting to create the VPA objects in namespace: %s", nsName)
40 | kubeClient := kube.GetInstance()
41 | namespace, err := kube.GetNamespace(kubeClient, nsName)
42 | if err != nil {
43 | fmt.Println("Error getting namespace. Exiting.")
44 | os.Exit(1)
45 | }
46 | reconciler := vpa.GetInstance()
47 | reconciler.DryRun = dryrun
48 | errReconcile := reconciler.ReconcileNamespace(namespace)
49 | if errReconcile != nil {
50 | fmt.Println("Errors encountered during reconciliation.")
51 | os.Exit(1)
52 | }
53 | },
54 | }
55 |
--------------------------------------------------------------------------------
/cmd/root.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "flag"
19 | "fmt"
20 | "os"
21 |
22 | "github.com/spf13/cobra"
23 | "github.com/spf13/pflag"
24 | "k8s.io/klog/v2"
25 | )
26 |
27 | var kubeconfig string
28 | var nsName string
29 | var exitCode int
30 |
31 | var (
32 | version string
33 | commit string
34 | )
35 |
36 | func init() {
37 | // Flags
38 | rootCmd.PersistentFlags().StringVarP(&kubeconfig, "kubeconfig", "", "$HOME/.kube/config", "Kubeconfig location.")
39 |
40 | klog.InitFlags(nil)
41 | pflag.CommandLine.AddGoFlag(flag.CommandLine.Lookup("v"))
42 |
43 | environmentVariables := map[string]string{
44 | "KUBECONFIG": "kubeconfig",
45 | }
46 |
47 | for env, flag := range environmentVariables {
48 | flag := rootCmd.PersistentFlags().Lookup(flag)
49 | flag.Usage = fmt.Sprintf("%v [%v]", flag.Usage, env)
50 | if value := os.Getenv(env); value != "" {
51 | err := flag.Value.Set(value)
52 | if err != nil {
53 | klog.Errorf("Error setting flag %v to %s from environment variable %s", flag, value, env)
54 | }
55 | }
56 | }
57 |
58 | }
59 |
60 | var rootCmd = &cobra.Command{
61 | Use: "goldilocks",
62 | Short: "goldilocks",
63 | Long: `A tool for analysis of kubernetes workload resource usage.`,
64 | Run: func(cmd *cobra.Command, args []string) {
65 | klog.Error("You must specify a sub-command.")
66 | err := cmd.Help()
67 | if err != nil {
68 | klog.Error(err)
69 | }
70 | os.Exit(1)
71 | },
72 | PersistentPostRun: func(cmd *cobra.Command, args []string) {
73 | os.Stderr.WriteString("\n\nWant more? Automate Goldilocks for free with Fairwinds Insights!\n🚀 https://fairwinds.com/insights-signup/goldilocks 🚀 \n")
74 | os.Exit(exitCode)
75 | },
76 | }
77 |
78 | // Execute the stuff
79 | func Execute(VERSION string, COMMIT string) {
80 | version = VERSION
81 | commit = COMMIT
82 | if err := rootCmd.Execute(); err != nil {
83 | klog.Error(err)
84 | os.Exit(1)
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/cmd/summary.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "encoding/json"
19 | "fmt"
20 | "os"
21 | "strings"
22 |
23 | "github.com/spf13/cobra"
24 | "k8s.io/apimachinery/pkg/util/sets"
25 | "k8s.io/klog/v2"
26 |
27 | "github.com/fairwindsops/goldilocks/pkg/summary"
28 | )
29 |
30 | var excludeContainers string
31 | var outputFile string
32 | var namespace string
33 |
34 | func init() {
35 | rootCmd.AddCommand(summaryCmd)
36 | summaryCmd.PersistentFlags().StringVarP(&excludeContainers, "exclude-containers", "e", "", "Comma delimited list of containers to exclude from recommendations.")
37 | summaryCmd.PersistentFlags().StringVarP(&outputFile, "output-file", "f", "", "File to write output from audit.")
38 | summaryCmd.PersistentFlags().StringVarP(&namespace, "namespace", "n", "", "Limit the summary to only a single Namespace.")
39 | }
40 |
41 | var summaryCmd = &cobra.Command{
42 | Use: "summary",
43 | Short: "Generate a summary of vpa recommendations.",
44 | Long: `Gather all the vpa data generate a summary of the recommendations.
45 | By default the summary will be about all VPAs in all namespaces.`,
46 | Args: cobra.ArbitraryArgs,
47 | Run: func(cmd *cobra.Command, args []string) {
48 | var opts []summary.Option
49 |
50 | // limit to a single namespace
51 | if namespace != "" {
52 | opts = append(opts, summary.ForNamespace(namespace))
53 | }
54 |
55 | // exclude containers from the summary
56 | if excludeContainers != "" {
57 | opts = append(opts, summary.ExcludeContainers(sets.New[string](strings.Split(excludeContainers, ",")...)))
58 | }
59 |
60 | summarizer := summary.NewSummarizer(opts...)
61 | data, err := summarizer.GetSummary()
62 | if err != nil {
63 | klog.Fatalf("Error getting summary: %v", err)
64 | }
65 |
66 | summaryJSON, err := json.Marshal(data)
67 | if err != nil {
68 | klog.Fatalf("Error marshalling JSON: %v", err)
69 | }
70 |
71 | if outputFile != "" {
72 | err := os.WriteFile(outputFile, summaryJSON, 0644)
73 | if err != nil {
74 | klog.Fatalf("Failed to write summary to file: %v", err)
75 | }
76 |
77 | fmt.Println("Summary has been written to", outputFile)
78 |
79 | } else {
80 | fmt.Println(string(summaryJSON))
81 | }
82 | },
83 | }
84 |
--------------------------------------------------------------------------------
/cmd/version.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package cmd
16 |
17 | import (
18 | "fmt"
19 |
20 | "github.com/spf13/cobra"
21 | )
22 |
23 | func init() {
24 | rootCmd.AddCommand(versionCmd)
25 | }
26 |
27 | var versionCmd = &cobra.Command{
28 | Use: "version",
29 | Short: "Prints the current version of the tool.",
30 | Long: `Prints the current version.`,
31 | Run: func(cmd *cobra.Command, args []string) {
32 | fmt.Println("Version:" + version + " Commit:" + commit)
33 | },
34 | PersistentPostRunE: func(cmd *cobra.Command, args []string) error {
35 | return nil
36 | },
37 | }
38 |
--------------------------------------------------------------------------------
/docs/.vuepress/config-extras.js:
--------------------------------------------------------------------------------
1 | // To see all options:
2 | // https://vuepress.vuejs.org/config/
3 | // https://vuepress.vuejs.org/theme/default-theme-config.html
4 | module.exports = {
5 | title: "goldilocks Documentation",
6 | description: "Documentation for Fairwinds' goldilocks",
7 | themeConfig: {
8 | docsRepo: "FairwindsOps/goldilocks",
9 | sidebar: [
10 | {
11 | title: "Goldilocks",
12 | path: "/",
13 | sidebarDepth: 0,
14 | },
15 | {
16 | title: "Installation",
17 | path: "/installation",
18 | },
19 | {
20 | title: "FAQ",
21 | path: "/faq"
22 | },
23 | {
24 | title: "Advanced Usage",
25 | path: "/advanced",
26 | },
27 | {
28 | title: "Contributing",
29 | children: [
30 | {
31 | title: "Guide",
32 | path: "contributing/guide"
33 | },
34 | {
35 | title: "Code of Conduct",
36 | path: "contributing/code-of-conduct"
37 | }
38 | ]
39 | }
40 | ]
41 | },
42 | }
43 |
--------------------------------------------------------------------------------
/docs/.vuepress/config.js:
--------------------------------------------------------------------------------
1 | // This file is generated from FairwindsOps/documentation-template
2 | // DO NOT EDIT MANUALLY
3 |
4 | const fs = require('fs');
5 | const npath = require('path');
6 |
7 | const CONFIG_FILE = npath.join(__dirname, 'config-extras.js');
8 | const BASE_DIR = npath.join(__dirname, '..');
9 |
10 | const extras = require(CONFIG_FILE);
11 | if (!extras.title || !extras.description || !extras.themeConfig.docsRepo) {
12 | throw new Error("Please specify 'title', 'description', and 'themeConfig.docsRepo' in config-extras.js");
13 | }
14 |
15 | const docFiles = fs.readdirSync(BASE_DIR)
16 | .filter(f => f !== "README.md")
17 | .filter(f => f !== ".vuepress")
18 | .filter(f => f !== "node_modules")
19 | .filter(f => npath.extname(f) === '.md' || npath.extname(f) === '');
20 |
21 | const sidebar = [['/', 'Home']].concat(docFiles.map(f => {
22 | const ext = npath.extname(f);
23 | if (ext === '') {
24 | // this is a directory
25 | const title = f;
26 | const children = fs.readdirSync(npath.join(BASE_DIR, f)).map(subf => {
27 | return '/' + f + '/' + npath.basename(subf);
28 | });
29 | return {title, children};
30 | }
31 | const path = npath.basename(f);
32 | return path;
33 | }));
34 |
35 | const baseConfig = {
36 | title: "",
37 | description: "",
38 | head: [
39 | ['link', { rel: 'icon', href: '/favicon.png' }],
40 | ['script', { src: '/scripts/modify.js' }],
41 | ['script', { src: '/scripts/marketing.js' }],
42 | ],
43 | themeConfig: {
44 | docsRepo: "",
45 | docsDir: 'docs',
46 | editLinks: true,
47 | editLinkText: "Help us improve this page",
48 | logo: '/img/fairwinds-logo.svg',
49 | heroText: "",
50 | sidebar,
51 | nav: [
52 | {text: 'View on GitHub', link: 'https://github.com/' + extras.themeConfig.docsRepo},
53 | ],
54 | },
55 | plugins: {
56 | 'vuepress-plugin-clean-urls': {
57 | normalSuffix: '/',
58 | notFoundPath: '/404.html',
59 | },
60 | 'check-md': {},
61 | },
62 | }
63 |
64 | let config = JSON.parse(JSON.stringify(baseConfig))
65 | if (!fs.existsSync(CONFIG_FILE)) {
66 | throw new Error("Please add config-extras.js to specify your project details");
67 | }
68 | for (let key in extras) {
69 | if (!config[key]) config[key] = extras[key];
70 | else if (key === 'head') config[key] = config[key].concat(extras[key]);
71 | else Object.assign(config[key], extras[key]);
72 | }
73 | module.exports = config;
74 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/docs/.vuepress/public/favicon.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/img/fairwinds-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
26 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/img/goldilocks.svg:
--------------------------------------------------------------------------------
1 | ../../../../pkg/dashboard/assets/images/goldilocks.svg
--------------------------------------------------------------------------------
/docs/.vuepress/public/img/insights-banner.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/docs/.vuepress/public/img/insights-banner.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/img/screenshot.png:
--------------------------------------------------------------------------------
1 | ../../../../img/screenshot.png
--------------------------------------------------------------------------------
/docs/.vuepress/public/scripts/marketing.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is generated from FairwindsOps/documentation-template
3 | * DO NOT EDIT MANUALLY
4 | */
5 |
6 | var llcookieless = true;
7 | var sf14gv = 32793;
8 | (function() {
9 | var sf14g = document.createElement('script');
10 | sf14g.src = 'https://lltrck.com/lt-v2.min.js';
11 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(sf14g, s);
12 | })();
13 |
14 | (function() {
15 | var gtag = document.createElement('script');
16 | gtag.src = "https://www.googletagmanager.com/gtag/js?id=G-ZR5M5SRYKY";
17 | var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(gtag, s);
18 | window.dataLayer = window.dataLayer || [];
19 | function gtag(){dataLayer.push(arguments);}
20 | gtag('js', new Date());
21 | gtag('config', 'G-ZR5M5SRYKY');
22 | })();
23 |
24 | !function(f,b,e,v,n,t,s)
25 | {if(f.fbq)return;n=f.fbq=function(){n.callMethod?
26 | n.callMethod.apply(n,arguments):n.queue.push(arguments)};
27 | if(!f._fbq)f._fbq=n;n.push=n;n.loaded=!0;n.version='2.0';
28 | n.queue=[];t=b.createElement(e);t.async=!0;
29 | t.src=v;s=b.getElementsByTagName(e)[0];
30 | s.parentNode.insertBefore(t,s)}(window,document,'script',
31 | 'https://connect.facebook.net/en_US/fbevents.js');
32 | fbq('init', '521127644762074');
33 | fbq('track', 'PageView');
34 |
--------------------------------------------------------------------------------
/docs/.vuepress/public/scripts/modify.js:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is generated from FairwindsOps/documentation-template
3 | * DO NOT EDIT MANUALLY
4 | */
5 |
6 | document.addEventListener("DOMContentLoaded", function(){
7 | setTimeout(function() {
8 | var link = document.getElementsByClassName('home-link')[0];
9 | linkClone = link.cloneNode(true);
10 | linkClone.href = "https://fairwinds.com";
11 | link.setAttribute('target', '_blank');
12 | link.parentNode.replaceChild(linkClone, link);
13 | }, 1000);
14 | });
15 |
16 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles/index.styl:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is generated from FairwindsOps/documentation-template
3 | * DO NOT EDIT MANUALLY
4 | */
5 |
6 | .github-only {
7 | display: none;
8 | }
9 |
10 | .text-primary {
11 | color: $primaryColor;
12 | }
13 | .text-danger {
14 | color: $dangerColor;
15 | }
16 | .text-warning {
17 | color: $warningColor;
18 | }
19 | .text-info {
20 | color: $infoColor;
21 | }
22 | .text-success {
23 | color: $successColor;
24 | }
25 |
26 | blockquote {
27 | border-left: 0.2rem solid $warningColor;
28 | }
29 | blockquote p {
30 | color: $warningColor;
31 | }
32 |
33 | .theme-default-content:not(.custom),
34 | .page-nav,
35 | .page-edit,
36 | footer {
37 | margin: 0 !important;
38 | }
39 |
40 | .theme-default-content:not(.custom) > h2 {
41 | padding-top: 7rem;
42 | }
43 |
44 | .navbar .site-name {
45 | display: none;
46 | }
47 |
48 | .navbar, .navbar .links {
49 | background-color: $primaryColor !important;
50 | }
51 |
52 | .navbar .links a {
53 | color: #fff;
54 | }
55 | .navbar .links a svg {
56 | display: none;
57 | }
58 |
59 | img {
60 | border: 5px solid #f7f7f7;
61 | }
62 |
63 | .no-border img,
64 | img.no-border,
65 | header img {
66 | border: none;
67 | }
68 |
69 | .mini-img {
70 | text-align: center;
71 | }
72 |
73 | .theme-default-content:not(.custom) .mini-img img {
74 | max-width: 300px;
75 | }
76 |
77 | .page {
78 | padding-bottom: 0 !important;
79 | }
80 |
--------------------------------------------------------------------------------
/docs/.vuepress/styles/palette.styl:
--------------------------------------------------------------------------------
1 | /*
2 | * This file is generated from FairwindsOps/documentation-template
3 | * DO NOT EDIT MANUALLY
4 | */
5 |
6 |
7 | $primaryColor = #23103A
8 | $dangerColor = #A0204C
9 | $warningColor = #FF6C00
10 | $infoColor = #8BD2DC
11 | $successColor = #28a745
12 |
13 | $accentColor = #FF6C00
14 | $textColor = #2c3e50
15 | $borderColor = #eaecef
16 | $codeBgColor = #282c34
17 | $arrowBgColor = #ccc
18 | $badgeTipColor = #42b983
19 | $badgeWarningColor = darken(#ffe564, 35%)
20 | $badgeErrorColor = #DA5961
21 |
22 | // layout
23 | $navbarHeight = 3.6rem
24 | $sidebarWidth = 20rem
25 | $contentWidth = 740px
26 | $homePageWidth = 960px
27 |
28 | // responsive breakpoints
29 | $MQNarrow = 959px
30 | $MQMobile = 719px
31 | $MQMobileNarrow = 419px
32 |
33 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/index.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | extend: '@vuepress/theme-default'
3 | }
4 |
--------------------------------------------------------------------------------
/docs/.vuepress/theme/layouts/Layout.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
28 |
29 |
46 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
18 |
19 | Goldilocks is a utility that can help you identify a starting point for resource requests and limits.
20 |
21 |
22 | ## How can this help with my resource settings?
23 |
24 | By using the kubernetes [vertical-pod-autoscaler](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) in recommendation mode, we can see a suggestion for resource requests on each of our apps. This tool creates a VPA for each workload in a namespace and then queries them for information.
25 |
26 | Once your VPAs are in place, you'll see recommendations appear in the Goldilocks dashboard:
27 |
28 |
29 |
30 |
--------------------------------------------------------------------------------
/docs/contributing/code-of-conduct.md:
--------------------------------------------------------------------------------
1 | # Contributor Covenant Code of Conduct
2 |
3 | ## Our Pledge
4 |
5 | In the interest of fostering an open and welcoming environment, we as
6 | contributors and maintainers pledge to making participation in our project and
7 | our community a harassment-free experience for everyone, regardless of age, body
8 | size, disability, ethnicity, gender identity and expression, level of experience,
9 | nationality, personal appearance, race, religion, or sexual identity and
10 | orientation.
11 |
12 | ## Our Standards
13 |
14 | Examples of behavior that contributes to creating a positive environment
15 | include:
16 |
17 | * Using welcoming and inclusive language
18 | * Being respectful of differing viewpoints and experiences
19 | * Gracefully accepting constructive criticism
20 | * Focusing on what is best for the community
21 | * Showing empathy towards other community members
22 |
23 | Examples of unacceptable behavior by participants include:
24 |
25 | * The use of sexualized language or imagery and unwelcome sexual attention or
26 | advances
27 | * Trolling, insulting/derogatory comments, and personal or political attacks
28 | * Public or private harassment
29 | * Publishing others' private information, such as a physical or electronic
30 | address, without explicit permission
31 | * Other conduct which could reasonably be considered inappropriate in a
32 | professional setting
33 |
34 | ## Our Responsibilities
35 |
36 | Project maintainers are responsible for clarifying the standards of acceptable
37 | behavior and are expected to take appropriate and fair corrective action in
38 | response to any instances of unacceptable behavior.
39 |
40 | Project maintainers have the right and responsibility to remove, edit, or
41 | reject comments, commits, code, wiki edits, issues, and other contributions
42 | that are not aligned to this Code of Conduct, or to ban temporarily or
43 | permanently any contributor for other behaviors that they deem inappropriate,
44 | threatening, offensive, or harmful.
45 |
46 | ## Scope
47 |
48 | This Code of Conduct applies both within project spaces and in public spaces
49 | when an individual is representing the project or its community. Examples of
50 | representing a project or community include using an official project e-mail
51 | address, posting via an official social media account, or acting as an appointed
52 | representative at an online or offline event. Representation of a project may be
53 | further defined and clarified by project maintainers.
54 |
55 | ## Enforcement
56 |
57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be
58 | reported by contacting the project team at [INSERT EMAIL ADDRESS]. All
59 | complaints will be reviewed and investigated and will result in a response that
60 | is deemed necessary and appropriate to the circumstances. The project team is
61 | obligated to maintain confidentiality with regard to the reporter of an incident.
62 | Further details of specific enforcement policies may be posted separately.
63 |
64 | Project maintainers who do not follow or enforce the Code of Conduct in good
65 | faith may face temporary or permanent repercussions as determined by other
66 | members of the project's leadership.
67 |
68 | ## Attribution
69 |
70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71 | available at [http://contributor-covenant.org/version/1/4][version]
72 |
73 | [homepage]: http://contributor-covenant.org
74 | [version]: http://contributor-covenant.org/version/1/4/
75 |
--------------------------------------------------------------------------------
/docs/contributing/guide.md:
--------------------------------------------------------------------------------
1 | ---
2 | meta:
3 | - name: description
4 | content: "Issues are essential for keeping goldilocks great. There are a few guidelines that we need contributors to follow so that we can keep on top of things"
5 | ---
6 | # Contributing
7 |
8 | Issues, whether bugs, tasks, or feature requests are essential for keeping goldilocks great. We believe it should be as easy as possible to contribute changes that get things working in your environment. There are a few guidelines that we need contributors to follow so that we can keep on top of things.
9 |
10 | ## Code of Conduct
11 |
12 | This project adheres to a [code of conduct](/contributing/code-of-conduct). Please review this document before contributing to this project.
13 |
14 | ## Sign the CLA
15 | Before you can contribute, you will need to sign the [Contributor License Agreement](https://cla-assistant.io/fairwindsops/goldilocks).
16 |
17 | ## Project Structure
18 |
19 | Goldilocks can run in 3 modes. There is a CLI that allows the manipulation of VPA objects, a dashboard, and a controller that runs in-cluster and manages VPA objects based on namespace labels. The CLI uses the cobra package and the commands are in the `cmd` folder.
20 |
21 | ## Getting Started
22 |
23 | We label issues with the ["good first issue" tag](https://github.com/FairwindsOps/goldilocks/labels/good%20first%20issue) if we believe they'll be a good starting point for new contributors. If you're interested in working on an issue, please start a conversation on that issue, and we can help answer any questions as they come up.
24 |
25 | ## Pre-commit
26 |
27 | This repo contains a pre-commit file for use with [pre-commit](https://pre-commit.com/). Just run `pre-commit install` and you will have the hooks.
28 |
29 | ## Setting Up Your Development Environment
30 |
31 | ### Using Kind
32 |
33 | Make sure you have the following installed:
34 |
35 | * [kind 0.9.0](https://github.com/kubernetes-sigs/kind/releases) or higher
36 | * [reckoner v1.4.0](https://github.com/FairwindsOps/reckoner/releases) or higher
37 | * [helm 2.13.1](https://github.com/helm/helm/releases) or higher
38 | * git
39 | * kubectl
40 |
41 | Go into the [/hack/kind](https://github.com/FairwindsOps/goldilocks/tree/master/hack/kind) directory and run `./setup.sh`
42 |
43 | This will create a kind cluster, place a demo app, install VPA, and install the latest goldilocks. You can run your local development against this cluster.
44 |
45 | ### Using your own cluster
46 |
47 | Prerequisites:
48 |
49 | * A properly configured Golang environment with Go 1.11 or higher
50 | * If you want to see the local changes you make on a dashboard, you will need access to a Kubernetes cluster defined in `~/.kube/config` or the KUBECONFIG variable.
51 | * The [vertical pod autoscaler](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) will need to be installed in the cluster.
52 |
53 | ### Installation
54 | * Install the project with `go get github.com/fairwindsops/goldilocks`
55 | * Change into the goldilocks directory which is installed at `$GOPATH/src/github.com/fairwindsops/goldilocks`
56 | * Use `make tidy` or `make build` to ensure all dependencies are downloaded.
57 | * See the dashboard with `go run main.go dashboard`, then open http://localhost:8080/. This assumes that you have a working KUBECONFIG in place with access to a cluster.
58 |
59 | ### End-To-End Tests
60 |
61 | The e2e tests run using [Venom](https://github.com/ovh/venom). You can run them yourself by:
62 |
63 | - installing venom
64 | - setting up a kind cluster `kind create cluster`
65 | - running `make e2e-test`.
66 |
67 | The tests are also run automatically by CI
68 |
69 | You can add tests in the [e2e/tests](https://github.com/FairwindsOps/goldilocks/tree/master/e2e/tests) directory. See the Venom README for more info.
70 |
71 | ## Creating a New Issue
72 |
73 | If you've encountered an issue that is not already reported, please create an issue that contains the following:
74 |
75 | - Clear description of the issue
76 | - Steps to reproduce it
77 | - Appropriate labels
78 |
79 | ## Creating a Pull Request
80 |
81 | Each new pull request should:
82 |
83 | - Reference any related issues
84 | - Add tests that show the issues have been solved
85 | - Pass existing tests and linting
86 | - Contain a clear indication of if they're ready for review or a work in progress
87 | - Be up to date and/or rebased on the master branch
88 |
89 | ## Creating a new release
90 |
91 | Push a new annotated tag. This tag should contain a changelog of pertinent changes.
92 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ---
2 | meta:
3 | - name: description
4 | content: "We get a lot of questions about how Goldilocks works and where it gets the recomendations. Hopefully we can answer the most common ones here"
5 | ---
6 | # Frequently Asked Questions
7 |
8 | We get a lot of questions about how Goldilocks works and where it gets the recomendations. Hopefully we can answer the most common ones here.
9 |
10 | ## How does Goldilocks generate recommendations?
11 |
12 | Goldilocks doesn't do any recommending of resource requests/limits by itself. It utilizes a Kubernetes project called [Vertical Pod Autoscaler (VPA)](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler). More specifically, it uses the [Recommender](https://github.com/kubernetes/autoscaler/blob/master/vertical-pod-autoscaler/pkg/recommender/README.md) portion of the VPA.
13 |
14 | According to the VPA documentation:
15 |
16 | ```
17 | After starting the binary, recommender reads the history of running pods and their usage from Prometheus into the model. It then runs in a loop and at each step performs the following actions:
18 |
19 | update model with recent information on resources (using listers based on watch),
20 | update model with fresh usage samples from Metrics API,
21 | compute new recommendation for each VPA,
22 | put any changed recommendations into the VPA resources.
23 | ```
24 |
25 | This means that recommendations are generated based on historical usage of the pod over time.
26 |
27 | ## Which values from the VPA are used?
28 |
29 | There are two types of recommendations that Goldilocks shows in the dashboard. They are based on Kubernetes [QoS Classes](https://kubernetes.io/docs/tasks/configure-pod-container/quality-service-pod/)
30 |
31 | A VPA recommendation section looks like this:
32 |
33 | ```
34 | recommendation:
35 | containerRecommendations:
36 | - containerName: basic-demo
37 | lowerBound:
38 | cpu: 10m
39 | memory: "26214400"
40 | target:
41 | cpu: 11m
42 | memory: "26214400"
43 | uncappedTarget:
44 | cpu: 11m
45 | memory: "26214400"
46 | upperBound:
47 | cpu: 12m
48 | memory: "26214400"
49 | ```
50 |
51 | We generate two different QoS classes of recommendation from this
52 |
53 | * For `Guaranteed`, we take the `target` field from the recommendation and set that as both the request and limit for that container
54 | * For `Burstable`, we set the request as the `lowerBound` and the limit as the `upperBound` from the VPA object
55 |
56 | ## How Accurate is Goldilocks?
57 |
58 | This is entirely based on the underlying VPA project. However, in our experience Goldilocks has usually been a good _starting point_ for setting your resource requests and limits. Every environment will be different, and Goldilocks is not a replacement for tuning your applications for your specific use-case.
59 |
60 | ## I see incoherent recommendations for my limits like 100T for memory or 100G for CPU, what gives?
61 |
62 | This situation can happen if you look at the recommendations very shortly after starting your workload.
63 | Indeed, the statistical model used in the VPA recommender needs 8 days of historic data to produce recommendations and upper/lower boundaries with maximum accuracy. In the time between starting a workload for the first time and these 8 days, the boundaries will become more and more accurate. The lowerBound converges much quicker to maximum accuracy than the upperBound: the idea is that upscaling can be done much more liberally than downscaling.
64 | If you see an upperBound value which is incredibly high, it is the maximum possible value for the VPA recommender's statistical model.
65 | TL;DR: wait a little bit to have more accurate values.
66 |
67 | ## I don't see any VPA objects getting created, what gives?
68 |
69 | There's two main possibilities here:
70 |
71 | * You have not labelled any namespaces for use by goldilocks. Try `kubectl label ns goldilocks.fairwinds.com/enabled=true`
72 | * VPA is not installed. The goldilocks logs will indicate if this is the case.
73 |
74 | ## I am not getting any recommendations, what gives?
75 |
76 | The first thing to do is wait a few minutes. The VPA recommender takes some time to populate data.
77 |
78 | The next most common cause of this is that metrics server is not running, or the metrics api-service isn't working, so VPA cannot provide any recommendations. There are a couple of things you can check.
79 |
80 | ### Check that the metrics apiservice is available:
81 |
82 | This indicates an issue:
83 | ```
84 | ▶ kubectl get apiservice v1beta1.metrics.k8s.io
85 | NAME SERVICE AVAILABLE AGE
86 | v1beta1.metrics.k8s.io metrics-server/metrics-server False (MissingEndpoints) 7s
87 | ```
88 |
89 | This shows a healthy metrics service:
90 | ```
91 | ▶ kubectl get apiservice v1beta1.metrics.k8s.io
92 | NAME SERVICE AVAILABLE AGE
93 | v1beta1.metrics.k8s.io metrics-server/metrics-server True 36s
94 | ```
95 |
--------------------------------------------------------------------------------
/docs/installation.md:
--------------------------------------------------------------------------------
1 | ---
2 | meta:
3 | - name: description
4 | content: "Installation instructions, requirements, and troubleshooting for Goldilocks."
5 | ---
6 | # Installation
7 |
8 | ## Requirements
9 |
10 | * kubectl
11 | * [vertical-pod-autoscaler](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) configured in the cluster
12 | * some workloads with pods (Goldilocks will monitor any workload controller that includes a PodSpec template (`spec.template.spec.containers[]` to be specific). This includes `Deployments`, `DaemonSets`, and `StatefulSets` among others.)
13 | * metrics-server (a requirement of vpa)
14 | * golang 1.17+
15 |
16 | ### Installing Vertical Pod Autoscaler
17 |
18 | There are multiple ways to install VPA for use with Goldilocks:
19 |
20 | * Install using the `hack/vpa-up.sh` script from the [vertical-pod-autoscaler repository](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler)
21 | * Install using the [Fairwinds VPA Helm Chart](https://github.com/FairwindsOps/charts/tree/master/stable/vpa)
22 |
23 | #### Important Note about VPA
24 |
25 | The full VPA install includes the updater and the admission webhook for VPA. Goldilocks only requires the recommender. An admission webhook can introduce unexpected results in a cluster if not planned for properly. If installing VPA using the goldilocks chart and the vpa sub-chart, only the VPA recommender will be installed. See the [vpa chart](https://github.com/FairwindsOps/charts/tree/master/stable/vpa) and the Goldilocks [values.yaml](https://github.com/FairwindsOps/charts/blob/master/stable/goldilocks/values.yaml) for more information.
26 |
27 | ### Prometheus (optional)
28 |
29 | [VPA](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) does not require the use of prometheus, but it is supported. The use of prometheus may provide more accurate results.
30 |
31 | ### GKE Notes
32 |
33 | [VPA](https://cloud.google.com/kubernetes-engine/docs/concepts/verticalpodautoscaler) is enabled by default in Autopilot clusters, but you must [manually enable it in Standard clusters](https://cloud.google.com/kubernetes-engine/docs/how-to/vertical-pod-autoscaling). You can enable it like so:
34 |
35 | ```
36 | gcloud container clusters update [CLUSTER-NAME] --enable-vertical-pod-autoscaling {--region [REGION-NAME] | --zone [ZONE-NAME]}
37 | ```
38 |
39 | NOTE: This does not support using prometheus as a data backend.
40 |
41 | ## Installation
42 |
43 | First, make sure you satisfy the requirements above.
44 |
45 | ### Method 1 - Helm (preferred)
46 |
47 | ```
48 | helm repo add fairwinds-stable https://charts.fairwinds.com/stable
49 | kubectl create namespace goldilocks
50 | Helm v2:
51 | helm install --name goldilocks --namespace goldilocks fairwinds-stable/goldilocks
52 | Helm v3:
53 | helm install goldilocks --namespace goldilocks fairwinds-stable/goldilocks
54 | ```
55 |
56 | ### Method 2 - Manifests
57 |
58 | The [hack/manifests](https://github.com/FairwindsOps/goldilocks/tree/master/hack/manifests) directory contains collections of Kubernetes YAML definitions for installing the controller and dashboard components in cluster.
59 |
60 | ```
61 | git clone https://github.com/FairwindsOps/goldilocks.git
62 | cd goldilocks
63 | kubectl create namespace goldilocks
64 | kubectl -n goldilocks apply -f hack/manifests/controller
65 | kubectl -n goldilocks apply -f hack/manifests/dashboard
66 | ```
67 |
68 | ### Enable Namespace
69 |
70 | Pick an application namespace and label it like so in order to see some data:
71 |
72 | ```
73 | kubectl label ns goldilocks goldilocks.fairwinds.com/enabled=true
74 | ```
75 |
76 | After that you should start to see VPA objects in that namespace.
77 |
78 | ### Viewing the Dashboard
79 |
80 | The default installation creates a ClusterIP service for the dashboard. You can access via port forward:
81 |
82 | ```
83 | kubectl -n goldilocks port-forward svc/goldilocks-dashboard 8080:80
84 | ```
85 |
86 | Then open your browser to [http://localhost:8080](http://localhost:8080)
87 |
--------------------------------------------------------------------------------
/docs/main-metadata.md:
--------------------------------------------------------------------------------
1 | ---
2 | meta:
3 | - name: title
4 | content: "Goldilocks Documentation | Fairwinds"
5 | - name: description
6 | content: "Goldilocks is a utility that can help you identify a starting point for resource requests and limits in Kubernetes."
7 | ---
8 |
--------------------------------------------------------------------------------
/docs/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "author": "",
3 | "bugs": {
4 | "url": "https://github.com/FairwindsOps/insights-docs/issues"
5 | },
6 | "dependencies": {
7 | "vuepress-plugin-check-md": "0.0.2"
8 | },
9 | "description": "A repository with a Vuepress template for Fairwinds projects",
10 | "devDependencies": {
11 | "vuepress": "^1.9.7",
12 | "vuepress-plugin-clean-urls": "^1.1.1",
13 | "vuepress-plugin-redirect": "^1.2.5"
14 | },
15 | "directories": {
16 | "doc": "docs"
17 | },
18 | "homepage": "https://github.com/FairwindsOps/insights-docs#readme",
19 | "license": "MIT",
20 | "main": "index.js",
21 | "name": "fairwinds-docs-template",
22 | "repository": {
23 | "type": "git",
24 | "url": "git+https://github.com/FairwindsOps/insights-docs.git"
25 | },
26 | "scripts": {
27 | "build": "npm run build:readme && npm run build:docs",
28 | "build:docs": "vuepress build -d ../dist/",
29 | "build:metadata": "cat main-metadata.md > README.md || true",
30 | "build:readme": "npm run build:metadata && cat ../README.md | grep -v 'ocumentation' | sed \"s/https:\\/\\/\\w\\+.docs.fairwinds.com//g\" >> README.md",
31 | "check-links": "vuepress check-md",
32 | "serve": "npm run build:readme && vuepress dev --port 3003",
33 | "vuepress": "vuepress"
34 | },
35 | "version": "0.0.1"
36 | }
37 |
--------------------------------------------------------------------------------
/e2e/.gitignore:
--------------------------------------------------------------------------------
1 | test_results.xml
2 |
--------------------------------------------------------------------------------
/e2e/pre.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | curl -LO https://github.com/kubernetes-sigs/kind/releases/download/v0.9.0/kind-linux-amd64
6 | chmod +x kind-linux-amd64
7 | bindir=$(pwd)/bin-kind
8 | mkdir -p "$bindir"
9 | mv kind-linux-amd64 "$bindir/kind"
10 | export PATH="$bindir:$PATH"
11 |
12 | wget -O /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/2.4.0/yq_linux_amd64"
13 | chmod +x /usr/local/bin/yq
14 |
15 | if [ -z "$CI_SHA1" ]; then
16 | echo "CI_SHA1 not set. Something is wrong"
17 | exit 1
18 | else
19 | echo "CI_SHA1: $CI_SHA1"
20 | fi
21 |
22 | printf "\n\n"
23 | echo "********************************************************************"
24 | echo "** LOADING IMAGES TO DOCKER AND KIND **"
25 | echo "********************************************************************"
26 | printf "\n\n"
27 | docker load --input /tmp/workspace/docker_save/goldilocks_${CI_SHA1}-amd64.tar
28 | export PATH=$(pwd)/bin-kind:$PATH
29 | kind load docker-image --name e2e us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:${CI_SHA1}-amd64
30 | printf "\n\n"
31 | echo "********************************************************************"
32 | echo "** END LOADING IMAGE **"
33 | echo "********************************************************************"
34 | printf "\n\n"
35 |
36 | yq w -i hack/manifests/dashboard/deployment.yaml spec.template.spec.containers[0].imagePullPolicy "Never"
37 | yq w -i hack/manifests/controller/deployment.yaml spec.template.spec.containers[0].imagePullPolicy "Never"
38 | yq w -i hack/manifests/dashboard/deployment.yaml spec.template.spec.containers[0].image "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:$CI_SHA1-amd64"
39 | yq w -i hack/manifests/controller/deployment.yaml spec.template.spec.containers[0].image "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:$CI_SHA1-amd64"
40 |
41 | cat hack/manifests/dashboard/deployment.yaml
42 | cat hack/manifests/controller/deployment.yaml
43 |
44 | docker cp . e2e-command-runner:/goldilocks
45 |
--------------------------------------------------------------------------------
/e2e/run.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ## This file is intended for use by CircleCI Only!!!
4 | ## I do not recommend running it yourself. If you want to run the e2e tests,
5 | ## just use the Makefile or read the CONTRIBUTING.md
6 |
7 | printf "\n\n"
8 | echo "**************************"
9 | echo "** Begin E2E Test Setup **"
10 | echo "**************************"
11 | printf "\n\n"
12 |
13 | set -e
14 |
15 | printf "\n\n"
16 | echo "**************************"
17 | echo "** Install Dependencies **"
18 | echo "**************************"
19 | printf "\n\n"
20 |
21 | wget -O /usr/local/bin/yq "https://github.com/mikefarah/yq/releases/download/2.4.0/yq_linux_amd64"
22 | chmod +x /usr/local/bin/yq
23 |
24 | printf "\n\n"
25 | echo "***************************"
26 | echo "** Install and Run Venom **"
27 | echo "***************************"
28 | printf "\n\n"
29 |
30 | curl -LO https://github.com/ovh/venom/releases/download/v0.27.0/venom.linux-amd64
31 | mv venom.linux-amd64 /usr/local/bin/venom
32 | chmod +x /usr/local/bin/venom
33 |
34 | cd /goldilocks/e2e
35 | mkdir -p /tmp/test-results
36 | venom run tests/* --log debug --output-dir=/tmp/test-results --strict
37 | exit $?
38 |
--------------------------------------------------------------------------------
/e2e/tests/00_setup.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | name: Setup
3 | vars:
4 | timeout: 60s
5 | vpa-wait: 20
6 | vpa-ref: e0f63c1caeec518f85c4347b673e4e99e4fb0059
7 | testcases:
8 | - name: Install metrics-server
9 | steps:
10 | - script: |
11 | helm repo add metrics-server https://kubernetes-sigs.github.io/metrics-server
12 | helm repo update
13 | helm upgrade --install metrics-server metrics-server/metrics-server -n metrics-server --create-namespace --set=args={'--kubelet-insecure-tls'}
14 | - name: Install VPA Recommender
15 | steps:
16 | - script: |
17 | helm repo add fairwinds-stable https://charts.fairwinds.com/stable
18 | helm repo update
19 | helm install vpa fairwinds-stable/vpa --namespace vpa --create-namespace
20 | - script: kubectl get crd verticalpodautoscalers.autoscaling.k8s.io -oname
21 | retry: 6
22 | delay: 5
23 | assertions:
24 | - result.code ShouldEqual 0
25 | - result.systemout ShouldEqual "customresourcedefinition.apiextensions.k8s.io/verticalpodautoscalers.autoscaling.k8s.io"
26 | - name: Install Goldilocks
27 | steps:
28 | - script: kubectl create ns goldilocks
29 | - script: |
30 | kubectl -n goldilocks apply -f ../../hack/manifests/dashboard/
31 | kubectl -n goldilocks apply -f ../../hack/manifests/controller/
32 | - script: |
33 | kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=dashboard
34 | kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=controller
35 |
--------------------------------------------------------------------------------
/e2e/tests/10_basic.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | name: "Basic Operation"
3 | vars:
4 | timeout: 60s
5 | vpa-wait: 30
6 | testcases:
7 | - name: Setup demo namespace
8 | steps:
9 | - script: |
10 | helm repo add fairwinds-incubator https://charts.fairwinds.com/incubator
11 | helm install basic-demo fairwinds-incubator/basic-demo --namespace demo --create-namespace
12 | kubectl -n demo wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=basic-demo
13 | - name: No VPA Anywhere Before Labels
14 | steps:
15 | - script: kubectl get vpa --all-namespaces
16 | assertions:
17 | - result.code ShouldEqual 0
18 | - result.systemout ShouldEqual ""
19 | - result.systemerr ShouldContainSubstring "No resources found"
20 | - name: VPA in demo namespace
21 | steps:
22 | - script: kubectl label ns demo goldilocks.fairwinds.com/enabled=true --overwrite
23 | - script: sleep {{.vpa-wait}}
24 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo goldilocks-basic-demo -oname
25 | assertions:
26 | - result.code ShouldEqual 0
27 | - result.systemout ShouldEqual "verticalpodautoscaler.autoscaling.k8s.io/goldilocks-basic-demo"
28 | - name: Setup redis in statefulset-demo namespace
29 | steps:
30 | - script: |
31 | helm repo add bitnami https://charts.bitnami.com/bitnami
32 | helm repo update
33 | kubectl create ns statefulset-demo
34 | helm install redis bitnami/redis --namespace statefulset-demo --set architecture=replication
35 | - name: No VPA in statefulset-demo namespace Before Labels
36 | steps:
37 | - script: kubectl get vpa -n statefulset-demo
38 | assertions:
39 | - result.code ShouldEqual 0
40 | - result.systemout ShouldEqual ""
41 | - result.systemerr ShouldContainSubstring "No resources found"
42 | - name: VPA in statefulset-demo namespace
43 | steps:
44 | - script: kubectl label ns statefulset-demo goldilocks.fairwinds.com/enabled=true --overwrite
45 | - script: sleep {{.vpa-wait}}
46 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n statefulset-demo goldilocks-redis-replicas -oname
47 | assertions:
48 | - result.code ShouldEqual 0
49 | - result.systemout ShouldEqual "verticalpodautoscaler.autoscaling.k8s.io/goldilocks-redis-replicas"
50 | - name: Setup demo-resource-policy namespace
51 | steps:
52 | - script: |
53 | kubectl create ns demo-resource-policy
54 | helm repo add fairwinds-incubator https://charts.fairwinds.com/incubator
55 | helm install basic-demo fairwinds-incubator/basic-demo --namespace demo-resource-policy
56 | kubectl -n demo-resource-policy wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=basic-demo
57 | - name: VPA with Resource Policy in demo-resource-policy namespace
58 | steps:
59 | - script: |
60 | kubectl annotate ns demo-resource-policy goldilocks.fairwinds.com/vpa-resource-policy='{ "containerPolicies": [ { "containerName": "nginx", "minAllowed": { "cpu": "250m", "memory": "100Mi" } } ] }' --overwrite
61 | - script: kubectl label ns demo-resource-policy goldilocks.fairwinds.com/enabled=true --overwrite
62 | - script: sleep {{.vpa-wait}}
63 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo-resource-policy goldilocks-basic-demo -o=jsonpath='{.spec.resourcePolicy.containerPolicies[]}'
64 | assertions:
65 | - result.code ShouldEqual 0
66 | - result.systemout ShouldEqual '{"containerName":"nginx","minAllowed":{"cpu":"250m","memory":"100Mi"}}'
67 |
--------------------------------------------------------------------------------
/e2e/tests/20_flags.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | name: "Flags - include-namespaces/exclude-namespaces/on-by-default"
3 | vars:
4 | timeout: 60s
5 | vpa-wait: 30
6 | testcases:
7 | - name: Setup namespaces demo-no-label/demo-included/demo-excluded
8 | steps:
9 | - script: |
10 | kubectl create ns demo-no-label
11 | kubectl create ns demo-included
12 | kubectl create ns demo-excluded
13 |
14 | helm repo add fairwinds-incubator https://charts.fairwinds.com/incubator
15 | helm install basic-demo-no-label fairwinds-incubator/basic-demo --namespace demo-no-label
16 | helm install basic-demo-included fairwinds-incubator/basic-demo --namespace demo-included
17 | helm install basic-demo-excluded fairwinds-incubator/basic-demo --namespace demo-excluded
18 |
19 | kubectl -n demo-no-label wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=basic-demo
20 | kubectl -n demo-included wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=basic-demo
21 | kubectl -n demo-excluded wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=basic-demo
22 | - name: On By Default
23 | steps:
24 | - script: yq w ../../hack/manifests/controller/deployment.yaml -- spec.template.spec.containers[0].command[2] '--on-by-default' | kubectl -n goldilocks apply -f -
25 | - script: kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=controller
26 | - script: sleep {{.vpa-wait}}
27 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo-no-label goldilocks-basic-demo-no-label -oname
28 | assertions:
29 | - result.code ShouldEqual 0
30 | - result.systemout ShouldEqual "verticalpodautoscaler.autoscaling.k8s.io/goldilocks-basic-demo-no-label"
31 | - name: Include Namespaces
32 | steps:
33 | - script: yq w ../../hack/manifests/controller/deployment.yaml -- spec.template.spec.containers[0].command[2] '--include-namespaces=demo-included' | kubectl -n goldilocks apply -f -
34 | - script: kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=controller
35 | - script: sleep {{.vpa-wait}}
36 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo-included goldilocks-basic-demo-included -oname
37 | assertions:
38 | - result.code ShouldEqual 0
39 | - result.systemout ShouldEqual "verticalpodautoscaler.autoscaling.k8s.io/goldilocks-basic-demo-included"
40 | - name: Exclude Namespaces
41 | steps:
42 | - script: |
43 | yq w ../../hack/manifests/controller/deployment.yaml -- spec.template.spec.containers[0].command[2] '--on-by-default' | \
44 | yq w - -- spec.template.spec.containers[0].command[3] '--exclude-namespaces=demo-excluded' | kubectl -n goldilocks apply -f -
45 | - script: kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=controller
46 | - script: sleep {{.vpa-wait}}
47 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo-excluded -oname
48 | assertions:
49 | - result.code ShouldEqual 0
50 | - result.systemout ShouldEqual ""
51 | - name: Ignore Controller Kind
52 | steps:
53 | - script: |
54 | yq w ../../hack/manifests/controller/deployment.yaml -- spec.template.spec.containers[0].command[2] '--ignore-controller-kind=Deployment' | kubectl -n goldilocks apply -f -
55 | - script: kubectl -n goldilocks wait deployment --timeout={{.timeout}} --for condition=available -l app.kubernetes.io/name=goldilocks,app.kubernetes.io/component=controller
56 | - script: sleep {{.vpa-wait}}
57 | - script: kubectl get verticalpodautoscalers.autoscaling.k8s.io -n demo-excluded -oname
58 | assertions:
59 | - result.code ShouldEqual 0
60 | - result.systemout ShouldEqual ""
61 |
--------------------------------------------------------------------------------
/e2e/tests/99_cleanup.yaml:
--------------------------------------------------------------------------------
1 | version: "2"
2 | name: Cleanup
3 | testcases:
4 | - name: Cleanup
5 | steps:
6 | - script: |
7 | kubectl delete ns demo demo-no-label demo-included demo-excluded goldilocks
8 | - script: |
9 | helm -n metrics-server delete metrics-server
10 | kubectl delete ns metrics-server
11 | helm -n vpa delete vpa
12 | kubectl delete ns vpa
13 | kubectl delete ns statefulset-demo
14 |
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/fairwindsops/goldilocks
2 |
3 | go 1.22.7
4 |
5 | require (
6 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc
7 | github.com/fairwindsops/controller-utils v0.3.4
8 | github.com/gobuffalo/packr/v2 v2.8.3
9 | github.com/google/uuid v1.6.0
10 | github.com/gorilla/mux v1.8.1
11 | github.com/samber/lo v1.47.0
12 | github.com/spf13/cobra v1.8.1
13 | github.com/spf13/pflag v1.0.5
14 | github.com/stretchr/testify v1.9.0
15 | k8s.io/api v0.31.1
16 | k8s.io/apimachinery v0.31.1
17 | k8s.io/autoscaler/vertical-pod-autoscaler v1.2.1
18 | k8s.io/client-go v0.31.1
19 | k8s.io/klog/v2 v2.130.1
20 | sigs.k8s.io/controller-runtime v0.19.0
21 | )
22 |
23 | require (
24 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect
25 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect
26 | github.com/go-logr/logr v1.4.2 // indirect
27 | github.com/go-logr/stdr v1.2.2 // indirect
28 | github.com/go-openapi/jsonpointer v0.19.6 // indirect
29 | github.com/go-openapi/jsonreference v0.20.2 // indirect
30 | github.com/go-openapi/swag v0.22.4 // indirect
31 | github.com/gobuffalo/logger v1.0.6 // indirect
32 | github.com/gobuffalo/packd v1.0.1 // indirect
33 | github.com/gogo/protobuf v1.3.2 // indirect
34 | github.com/golang/protobuf v1.5.4 // indirect
35 | github.com/google/gnostic-models v0.6.8 // indirect
36 | github.com/google/go-cmp v0.6.0 // indirect
37 | github.com/google/gofuzz v1.2.0 // indirect
38 | github.com/imdario/mergo v0.3.6 // indirect
39 | github.com/inconshreveable/mousetrap v1.1.0 // indirect
40 | github.com/josharian/intern v1.0.0 // indirect
41 | github.com/json-iterator/go v1.1.12 // indirect
42 | github.com/karrick/godirwalk v1.16.1 // indirect
43 | github.com/mailru/easyjson v0.7.7 // indirect
44 | github.com/markbates/errx v1.1.0 // indirect
45 | github.com/markbates/oncer v1.0.0 // indirect
46 | github.com/markbates/safe v1.0.1 // indirect
47 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
48 | github.com/modern-go/reflect2 v1.0.2 // indirect
49 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
50 | github.com/pkg/errors v0.9.1 // indirect
51 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
52 | github.com/sirupsen/logrus v1.8.1 // indirect
53 | github.com/x448/float16 v0.8.4 // indirect
54 | golang.org/x/crypto v0.31.0 // indirect
55 | golang.org/x/net v0.33.0 // indirect
56 | golang.org/x/oauth2 v0.21.0 // indirect
57 | golang.org/x/sys v0.28.0 // indirect
58 | golang.org/x/term v0.27.0 // indirect
59 | golang.org/x/text v0.21.0 // indirect
60 | golang.org/x/time v0.4.0 // indirect
61 | google.golang.org/protobuf v1.34.2 // indirect
62 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
63 | gopkg.in/inf.v0 v0.9.1 // indirect
64 | gopkg.in/yaml.v2 v2.4.0 // indirect
65 | gopkg.in/yaml.v3 v3.0.1 // indirect
66 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect
67 | k8s.io/utils v0.0.0-20240711033017-18e509b52bc8 // indirect
68 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
69 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
70 | sigs.k8s.io/yaml v1.4.0 // indirect
71 | )
72 |
--------------------------------------------------------------------------------
/hack/kind/course.yml:
--------------------------------------------------------------------------------
1 | repository: stable
2 | namespace: demo
3 | minimum_versions:
4 | helm: 2.13.1
5 | reckoner: 1.4.0
6 | repositories:
7 | incubator:
8 | url: https://kubernetes-charts-incubator.storage.googleapis.com
9 | stable:
10 | url: https://kubernetes-charts.storage.googleapis.com
11 | fairwinds-stable:
12 | url: https://charts.fairwinds.com/stable
13 | fairwinds-incubator:
14 | url: https://charts.fairwinds.com/incubator
15 | bitnami:
16 | url: https://charts.bitnami.com/bitnami
17 | charts:
18 | metrics-server:
19 | namespace: metrics-server
20 | repository: bitnami
21 | version: "4.3.1"
22 | values:
23 | apiService:
24 | create: true
25 | extraArgs:
26 | kubelet-insecure-tls: 1
27 | kubelet-preferred-address-types: InternalIP
28 | vpa:
29 | namespace: vpa
30 | version: "0.1.0"
31 | repository: fairwinds-stable
32 | load-generator:
33 | namespace: demo
34 | version: 0.1.1
35 | repository: fairwinds-incubator
36 | files:
37 | - load-generator-values.yaml
38 | basic-demo:
39 | namespace: demo
40 | version: 0.4.0
41 | hooks:
42 | post_install:
43 | - kubectl get ns demo || kubectl create ns demo
44 | - kubectl label ns demo --overwrite goldilocks.fairwinds.com/enabled=true
45 | repository: fairwinds-incubator
46 | files:
47 | - demo-values.yaml
48 |
--------------------------------------------------------------------------------
/hack/kind/demo-values.yaml:
--------------------------------------------------------------------------------
1 | linkerd:
2 | serviceProfile: false
3 | enableRetry: false
4 | demo:
5 | title: "Testing Demo"
6 | metadata: "Brought to you by Fairwinds"
7 | refreshInterval: 400
8 | hpa:
9 | cpuTarget: 60
10 | customMetric:
11 | enabled: false
12 | max: 30
13 | min: 3
14 | vpa:
15 | enabled: false
16 | ingress:
17 | enabled: false
18 | resources:
19 | limits:
20 | cpu: 6m
21 | memory: 131072k
22 | requests:
23 | cpu: 6m
24 | memory: 131072k
25 |
--------------------------------------------------------------------------------
/hack/kind/load-generator-values.yaml:
--------------------------------------------------------------------------------
1 | replicaCount: 2
2 |
3 | influx:
4 | enabled: false
5 |
6 | loadScript: |
7 | import http from "k6/http";
8 | import { check, fail } from "k6";
9 | import { Rate } from "k6/metrics";
10 |
11 | export let options = {
12 | noConnectionReuse: true,
13 | thresholds: {
14 | "errors": ["rate<0.05"]
15 | }
16 | };
17 |
18 | export let url = "http://basic-demo.demo"
19 | export let errorRate = new Rate("errors");
20 |
21 | export default function() {
22 |
23 | let params = {
24 | headers: {
25 | "User-Agent": "k6"
26 | },
27 | redirects: 5,
28 | tags: { "k6test": "yes" }
29 | };
30 | let jar = http.cookieJar();
31 | jar.set(url, "testing", "always");
32 |
33 | let res = http.get(url, params);
34 |
35 | errorRate.add(res.status != 200);
36 | check(res, {
37 | "Status 200": (r) => r.status === 200
38 | });
39 | };
40 |
--------------------------------------------------------------------------------
/hack/kind/setup.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | set -e
4 |
5 | kind_required_version=0.9.0
6 | kind_node_image="kindest/node:v1.18.8@sha256:f4bcc97a0ad6e7abaf3f643d890add7efe6ee4ab90baeb374b4f41a4c95567eb"
7 | install_goldilocks=${2:-true}
8 |
9 | ## Test Infra Setup
10 | ## This will use Kind, Reckoner, and Helm to setup a test infrastructure locally for goldilocks
11 |
12 | function version_gt() {
13 | test "$(printf '%s\n' "$@" | sort -V | head -n 1)" != "$1";
14 | }
15 |
16 | cd "$( cd "$(dirname "$0")" ; pwd -P )"
17 |
18 | required_clis="reckoner helm kind"
19 | for cli in $required_clis; do
20 | command -v "$cli" >/dev/null 2>&1 || { echo >&2 "I require $cli but it's not installed. Aborting."; exit 1; }
21 | done
22 |
23 | kind_version=$(kind version | cut -c2-)
24 |
25 | if version_gt "$kind_required_version" "$kind_version"; then
26 | echo "This script requires kind version greater than or equal to $kind_required_version!"
27 | exit 1
28 | fi
29 |
30 | ## Create the kind cluster
31 |
32 | kind create cluster \
33 | --name test-infra \
34 | --image="$kind_node_image" || true
35 |
36 | # shellcheck disable=SC2034
37 | until kubectl cluster-info; do
38 | echo "Waiting for cluster to become available...."
39 | sleep 3
40 | done
41 |
42 | ## Reckoner
43 | ## Installs all dependencies such as metrics-server and vpa
44 |
45 | reckoner plot course.yml -a
46 |
47 | if $install_goldilocks; then
48 | ## Install Goldilocks
49 | kubectl get ns goldilocks || kubectl create ns goldilocks
50 | kubectl -n goldilocks apply -f ../manifests/controller
51 | kubectl -n goldilocks apply -f ../manifests/dashboard
52 | fi
53 |
54 | echo "Your test environment should now be running."
55 |
--------------------------------------------------------------------------------
/hack/manifests/controller/clusterrole.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: goldilocks-controller
6 | labels:
7 | app: goldilocks
8 | rules:
9 | - apiGroups:
10 | - 'apps'
11 | resources:
12 | - '*'
13 | verbs:
14 | - 'get'
15 | - 'list'
16 | - 'watch'
17 | - apiGroups:
18 | - 'batch'
19 | resources:
20 | - 'cronjobs'
21 | - 'jobs'
22 | verbs:
23 | - 'get'
24 | - 'list'
25 | - 'watch'
26 | - apiGroups:
27 | - ''
28 | resources:
29 | - 'namespaces'
30 | - 'pods'
31 | verbs:
32 | - 'get'
33 | - 'list'
34 | - 'watch'
35 | - apiGroups:
36 | - 'autoscaling.k8s.io'
37 | resources:
38 | - 'verticalpodautoscalers'
39 | verbs:
40 | - 'get'
41 | - 'list'
42 | - 'create'
43 | - 'delete'
44 | - 'update'
45 | - apiGroups:
46 | - 'argoproj.io'
47 | resources:
48 | - 'rollouts'
49 | verbs:
50 | - 'get'
51 | - 'list'
52 | - 'watch'
53 |
--------------------------------------------------------------------------------
/hack/manifests/controller/clusterrolebinding.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRoleBinding
4 | metadata:
5 | name: goldilocks-controller
6 | labels:
7 | app: goldilocks
8 | roleRef:
9 | apiGroup: rbac.authorization.k8s.io
10 | kind: ClusterRole
11 | name: goldilocks-controller
12 | subjects:
13 | - kind: ServiceAccount
14 | name: goldilocks-controller
15 | namespace: goldilocks
16 |
--------------------------------------------------------------------------------
/hack/manifests/controller/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps/v1
3 | kind: Deployment
4 | metadata:
5 | name: goldilocks-controller
6 | labels:
7 | app.kubernetes.io/name: goldilocks
8 | app.kubernetes.io/component: controller
9 | spec:
10 | replicas: 1
11 | selector:
12 | matchLabels:
13 | app.kubernetes.io/name: goldilocks
14 | app.kubernetes.io/component: controller
15 | template:
16 | metadata:
17 | labels:
18 | app.kubernetes.io/name: goldilocks
19 | app.kubernetes.io/component: controller
20 | spec:
21 | serviceAccountName: goldilocks-controller
22 | containers:
23 | - name: goldilocks
24 | image: "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v4"
25 | imagePullPolicy: Always
26 | command:
27 | - /goldilocks
28 | - controller
29 | securityContext:
30 | readOnlyRootFilesystem: true
31 | allowPrivilegeEscalation: false
32 | runAsNonRoot: true
33 | runAsUser: 10324
34 | capabilities:
35 | drop:
36 | - ALL
37 | ports:
38 | - name: http
39 | containerPort: 8080
40 | protocol: TCP
41 | resources:
42 | requests:
43 | cpu: 25m
44 | memory: 32Mi
45 | limits:
46 | cpu: 25m
47 | memory: 32Mi
48 |
--------------------------------------------------------------------------------
/hack/manifests/controller/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: goldilocks-controller
5 | labels:
6 | app: goldilocks
7 |
--------------------------------------------------------------------------------
/hack/manifests/dashboard/clusterrole.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRole
4 | metadata:
5 | name: goldilocks-dashboard
6 | labels:
7 | app: goldilocks
8 | rules:
9 | - apiGroups:
10 | - 'apps'
11 | resources:
12 | - '*'
13 | verbs:
14 | - 'get'
15 | - 'list'
16 | - 'watch'
17 | - apiGroups:
18 | - ''
19 | resources:
20 | - 'namespaces'
21 | - 'pods'
22 | verbs:
23 | - 'get'
24 | - 'list'
25 | - 'watch'
26 | - apiGroups:
27 | - 'autoscaling.k8s.io'
28 | resources:
29 | - 'verticalpodautoscalers'
30 | verbs:
31 | - 'get'
32 | - 'list'
33 | - apiGroups:
34 | - 'argoproj.io'
35 | resources:
36 | - 'rollouts'
37 | verbs:
38 | - 'get'
39 | - 'list'
40 | - 'watch'
41 |
--------------------------------------------------------------------------------
/hack/manifests/dashboard/clusterrolebinding.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: rbac.authorization.k8s.io/v1
3 | kind: ClusterRoleBinding
4 | metadata:
5 | name: goldilocks-dashboard
6 | labels:
7 | app: goldilocks
8 | roleRef:
9 | apiGroup: rbac.authorization.k8s.io
10 | kind: ClusterRole
11 | name: goldilocks-dashboard
12 | subjects:
13 | - kind: ServiceAccount
14 | name: goldilocks-dashboard
15 | namespace: goldilocks
16 |
--------------------------------------------------------------------------------
/hack/manifests/dashboard/deployment.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: apps/v1
3 | kind: Deployment
4 | metadata:
5 | name: goldilocks-dashboard
6 | labels:
7 | app.kubernetes.io/name: goldilocks
8 | app.kubernetes.io/component: dashboard
9 | spec:
10 | replicas: 2
11 | selector:
12 | matchLabels:
13 | app.kubernetes.io/name: goldilocks
14 | app.kubernetes.io/component: dashboard
15 | template:
16 | metadata:
17 | labels:
18 | app.kubernetes.io/name: goldilocks
19 | app.kubernetes.io/component: dashboard
20 | spec:
21 | serviceAccountName: goldilocks-dashboard
22 | containers:
23 | - name: goldilocks
24 | image: "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v4"
25 | imagePullPolicy: Always
26 | command:
27 | - /goldilocks
28 | - dashboard
29 | - --exclude-containers=linkerd-proxy,istio-proxy
30 | - -v3
31 | securityContext:
32 | readOnlyRootFilesystem: true
33 | allowPrivilegeEscalation: false
34 | runAsNonRoot: true
35 | runAsUser: 10324
36 | capabilities:
37 | drop:
38 | - ALL
39 | ports:
40 | - name: http
41 | containerPort: 8080
42 | protocol: TCP
43 | resources:
44 | requests:
45 | cpu: 25m
46 | memory: 32Mi
47 | limits:
48 | cpu: 25m
49 | memory: 32Mi
50 | livenessProbe:
51 | httpGet:
52 | path: /health
53 | port: http
54 | readinessProbe:
55 | httpGet:
56 | path: /health
57 | port: http
58 |
--------------------------------------------------------------------------------
/hack/manifests/dashboard/service.yaml:
--------------------------------------------------------------------------------
1 | ---
2 | apiVersion: v1
3 | kind: Service
4 | metadata:
5 | name: goldilocks-dashboard
6 | labels:
7 | app.kubernetes.io/name: goldilocks
8 | app.kubernetes.io/component: dashboard
9 | spec:
10 | type: ClusterIP
11 | ports:
12 | - port: 80
13 | targetPort: http
14 | protocol: TCP
15 | name: http
16 | selector:
17 | app.kubernetes.io/name: goldilocks
18 | app.kubernetes.io/component: dashboard
19 |
--------------------------------------------------------------------------------
/hack/manifests/dashboard/serviceaccount.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v1
2 | kind: ServiceAccount
3 | metadata:
4 | name: goldilocks-dashboard
5 | labels:
6 | app: goldilocks
7 |
--------------------------------------------------------------------------------
/img/goldilocks.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
57 |
--------------------------------------------------------------------------------
/img/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/img/screenshot.png
--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 Fairwinds
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License..
14 |
15 | package main
16 |
17 | import (
18 | "github.com/fairwindsops/goldilocks/cmd"
19 | )
20 |
21 | var (
22 | // version is set during build
23 | version = "development"
24 | // commit is set during build
25 | commit = "n/a"
26 | )
27 |
28 | func main() {
29 | cmd.Execute(version, commit)
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/controller/controller_test.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package controller
16 |
17 | import (
18 | "testing"
19 |
20 | "github.com/stretchr/testify/assert"
21 | corev1 "k8s.io/api/core/v1"
22 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
23 | )
24 |
25 | func Test_objectMeta(t *testing.T) {
26 | tests := []struct {
27 | name string
28 | obj interface{}
29 | want metav1.ObjectMeta
30 | }{
31 | {
32 | name: "Namespace with Labels",
33 | obj: &corev1.Namespace{
34 | ObjectMeta: metav1.ObjectMeta{
35 | Name: "ns",
36 | Namespace: "test",
37 | Labels: map[string]string{
38 | "goldilocks.fairwinds.com/enabled": "True",
39 | },
40 | },
41 | },
42 | want: metav1.ObjectMeta{
43 | Name: "ns",
44 | Labels: map[string]string{
45 | "goldilocks.fairwinds.com/enabled": "True",
46 | },
47 | Namespace: "test",
48 | },
49 | },
50 | {
51 | name: "Pod",
52 | obj: &corev1.Pod{
53 | ObjectMeta: metav1.ObjectMeta{
54 | Name: "pod",
55 | Namespace: "test",
56 | },
57 | },
58 | want: metav1.ObjectMeta{
59 | Namespace: "test",
60 | Name: "pod",
61 | },
62 | },
63 | }
64 | for _, tt := range tests {
65 | t.Run(tt.name, func(t *testing.T) {
66 | assert.EqualValues(t, objectMeta(tt.obj), tt.want)
67 | })
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "net/http"
5 |
6 | "github.com/gobuffalo/packr/v2"
7 | "k8s.io/klog/v2"
8 | )
9 |
10 | var assetBox = (*packr.Box)(nil)
11 |
12 | // getAssetBox returns a binary-friendly set of assets packaged from disk
13 | func getAssetBox() *packr.Box {
14 | if assetBox == (*packr.Box)(nil) {
15 | assetBox = packr.New("Assets", "assets")
16 | }
17 | return assetBox
18 | }
19 |
20 | // Asset replies with the contents of the loaded asset from disk
21 | func Asset(assetPath string) http.Handler {
22 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
23 | asset, err := getAssetBox().Find(assetPath)
24 | if err != nil {
25 | klog.Errorf("Error getting asset: %v", err)
26 | http.Error(w, "Error getting asset", http.StatusInternalServerError)
27 | return
28 | }
29 | _, err = w.Write(asset)
30 | if err != nil {
31 | klog.Errorf("Error writing asset: %v", err)
32 | }
33 | })
34 | }
35 |
36 | // StaticAssets replies with a FileServer for all assets, the prefix is used to strip the URL path
37 | func StaticAssets(prefix string) http.Handler {
38 | klog.V(3).Infof("stripping prefix: %s", prefix)
39 | return http.StripPrefix(prefix, http.FileServer(getAssetBox()))
40 | }
41 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/css/font-muli.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: "Muli";
3 | font-style: normal;
4 | font-weight: 300;
5 | src: local("Muli Light"), local("Muli-Light"), url(../webfonts/Muli-Light.tff) format("truetype");
6 | }
7 |
8 | @font-face {
9 | font-family: "Muli";
10 | font-style: normal;
11 | font-weight: 400;
12 | src: local("Muli Regular"), local("Muli-Regular"), url(../webfonts/Muli-Regular.tff) format("truetype");
13 | }
14 |
15 | @font-face {
16 | font-family: "Muli";
17 | font-style: normal;
18 | font-weight: 700;
19 | src: local("Muli Bold"), local("Muli-Bold"), url(../webfonts/Muli-Bold.tff) format("truetype");
20 | }
21 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/css/prism.css:
--------------------------------------------------------------------------------
1 | /* PrismJS 1.17.1
2 | https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+yaml&plugins=toolbar+copy-to-clipboard */
3 | /**
4 | * prism.js default theme for JavaScript, CSS and HTML
5 | * Based on dabblet (http://dabblet.com)
6 | * @author Lea Verou
7 | */
8 |
9 | code[class*="language-"],
10 | pre[class*="language-"] {
11 | color: black;
12 | background: none;
13 | text-shadow: 0 1px white;
14 | font-family: Consolas, Monaco, 'Andale Mono', 'Ubuntu Mono', monospace;
15 | font-size: 1em;
16 | text-align: left;
17 | white-space: pre;
18 | word-spacing: normal;
19 | word-break: normal;
20 | word-wrap: normal;
21 | line-height: 1.5;
22 |
23 | -moz-tab-size: 4;
24 | -o-tab-size: 4;
25 | tab-size: 4;
26 |
27 | -webkit-hyphens: none;
28 | -moz-hyphens: none;
29 | -ms-hyphens: none;
30 | hyphens: none;
31 | }
32 |
33 | pre[class*="language-"]::-moz-selection, pre[class*="language-"] ::-moz-selection,
34 | code[class*="language-"]::-moz-selection, code[class*="language-"] ::-moz-selection {
35 | text-shadow: none;
36 | background: #b3d4fc;
37 | }
38 |
39 | pre[class*="language-"]::selection, pre[class*="language-"] ::selection,
40 | code[class*="language-"]::selection, code[class*="language-"] ::selection {
41 | text-shadow: none;
42 | background: #b3d4fc;
43 | }
44 |
45 | @media print {
46 | code[class*="language-"],
47 | pre[class*="language-"] {
48 | text-shadow: none;
49 | }
50 | }
51 |
52 | /* Code blocks */
53 | pre[class*="language-"] {
54 | padding: 1em;
55 | margin: .5em 0;
56 | overflow: auto;
57 | }
58 |
59 | :not(pre) > code[class*="language-"],
60 | pre[class*="language-"] {
61 | background: #f5f2f0;
62 | }
63 |
64 | /* Inline code */
65 | :not(pre) > code[class*="language-"] {
66 | padding: .1em;
67 | border-radius: .3em;
68 | white-space: normal;
69 | }
70 |
71 | .token.comment,
72 | .token.prolog,
73 | .token.doctype,
74 | .token.cdata {
75 | color: slategray;
76 | }
77 |
78 | .token.punctuation {
79 | color: #999;
80 | }
81 |
82 | .namespace {
83 | opacity: .7;
84 | }
85 |
86 | .token.property,
87 | .token.tag,
88 | .token.boolean,
89 | .token.number,
90 | .token.constant,
91 | .token.symbol,
92 | .token.deleted {
93 | color: #905;
94 | }
95 |
96 | .token.selector,
97 | .token.attr-name,
98 | .token.string,
99 | .token.char,
100 | .token.builtin,
101 | .token.inserted {
102 | color: #690;
103 | }
104 |
105 | .token.operator,
106 | .token.entity,
107 | .token.url,
108 | .language-css .token.string,
109 | .style .token.string {
110 | color: #9a6e3a;
111 | background: hsla(0, 0%, 100%, .5);
112 | }
113 |
114 | .token.atrule,
115 | .token.attr-value,
116 | .token.keyword {
117 | color: #07a;
118 | }
119 |
120 | .token.function,
121 | .token.class-name {
122 | color: #DD4A68;
123 | }
124 |
125 | .token.regex,
126 | .token.important,
127 | .token.variable {
128 | color: #e90;
129 | }
130 |
131 | .token.important,
132 | .token.bold {
133 | font-weight: bold;
134 | }
135 | .token.italic {
136 | font-style: italic;
137 | }
138 |
139 | .token.entity {
140 | cursor: help;
141 | }
142 |
143 | div.code-toolbar {
144 | position: relative;
145 | }
146 |
147 | div.code-toolbar > .toolbar {
148 | position: absolute;
149 | top: .3em;
150 | right: .2em;
151 | transition: opacity 0.3s ease-in-out;
152 | opacity: 0;
153 | }
154 |
155 | div.code-toolbar:hover > .toolbar {
156 | opacity: 1;
157 | }
158 |
159 | /* Separate line b/c rules are thrown out if selector is invalid.
160 | IE11 and old Edge versions don't support :focus-within. */
161 | div.code-toolbar:focus-within > .toolbar {
162 | opacity: 1;
163 | }
164 |
165 | div.code-toolbar > .toolbar .toolbar-item {
166 | display: inline-block;
167 | }
168 |
169 | div.code-toolbar > .toolbar a {
170 | cursor: pointer;
171 | }
172 |
173 | div.code-toolbar > .toolbar button {
174 | background: none;
175 | border: 0;
176 | color: inherit;
177 | font: inherit;
178 | line-height: normal;
179 | overflow: visible;
180 | padding: 0;
181 | -webkit-user-select: none; /* for button */
182 | -moz-user-select: none;
183 | -ms-user-select: none;
184 | }
185 |
186 | div.code-toolbar > .toolbar a,
187 | div.code-toolbar > .toolbar button,
188 | div.code-toolbar > .toolbar span {
189 | color: #bbb;
190 | font-size: .8em;
191 | padding: 0 .5em;
192 | background: #f5f2f0;
193 | background: rgba(224, 224, 224, 0.2);
194 | box-shadow: 0 2px 0 0 rgba(0,0,0,0.2);
195 | border-radius: .5em;
196 | }
197 |
198 | div.code-toolbar > .toolbar a:hover,
199 | div.code-toolbar > .toolbar a:focus,
200 | div.code-toolbar > .toolbar button:hover,
201 | div.code-toolbar > .toolbar button:focus,
202 | div.code-toolbar > .toolbar span:hover,
203 | div.code-toolbar > .toolbar span:focus {
204 | color: inherit;
205 | text-decoration: none;
206 | }
207 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/css/reset.css:
--------------------------------------------------------------------------------
1 | /*
2 | Modern CSS Reset
3 | https://github.com/hankchizljaw/modern-css-reset
4 | MIT License
5 |
6 | Copyright (c) 2019 Andy Bell and other contributors
7 |
8 | Permission is hereby granted, free of charge, to any person obtaining a copy
9 | of this software and associated documentation files (the "Software"), to deal
10 | in the Software without restriction, including without limitation the rights
11 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
12 | copies of the Software, and to permit persons to whom the Software is
13 | furnished to do so, subject to the following conditions:
14 |
15 | The above copyright notice and this permission notice shall be included in all
16 | copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24 | SOFTWARE.
25 | */
26 |
27 | /* Box sizing rules */
28 | *,
29 | *::before,
30 | *::after {
31 | box-sizing: border-box;
32 | }
33 |
34 | /* Remove default padding */
35 | ul[class],
36 | ol[class] {
37 | padding: 0;
38 | }
39 |
40 | /* Remove default margin */
41 | blockquote,
42 | body,
43 | dd,
44 | dl,
45 | h1,
46 | h2,
47 | h3,
48 | h4,
49 | h5,
50 | h6,
51 | figure,
52 | p {
53 | margin: 0;
54 | }
55 |
56 | /* Set core root defaults */
57 | html {
58 | scroll-behavior: smooth;
59 | }
60 |
61 | /* Set core body defaults */
62 | body {
63 | line-height: 1.5;
64 | min-height: 100vh;
65 | text-rendering: optimizeSpeed;
66 | }
67 |
68 | /* A elements that don't have a class get default styles */
69 | a:not([class]) {
70 | text-decoration-skip-ink: auto;
71 | }
72 |
73 | /* Make images easier to work with */
74 | img,
75 | picture {
76 | display: block;
77 | max-width: 100%;
78 | }
79 |
80 | /* Inherit fonts for inputs and buttons */
81 | input,
82 | button,
83 | textarea,
84 | select {
85 | font: inherit;
86 | letter-spacing: inherit;
87 | word-spacing: inherit;
88 | }
89 |
90 | /* Make images stand out when they have no alt attribute */
91 | img:not([alt]) {
92 | border: 5px solid red;
93 | }
94 |
95 | /* Remove all animations and transitions for people that prefer not to see them */
96 | @media (prefers-reduced-motion: reduce) {
97 | * {
98 | animation-duration: 0.01ms !important;
99 | animation-iteration-count: 1 !important;
100 | scroll-behavior: auto !important;
101 | transition-duration: 0.01ms !important;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/css/utopia.css:
--------------------------------------------------------------------------------
1 | /*
2 | -----
3 | UTOPIA
4 | Courtesy of https://utopia.fyi
5 | Some modifications made
6 | -----
7 | */
8 |
9 | /* @link https://utopia.fyi/generator?c=320,16,1.2,1500,20,1.333,5,1, */
10 |
11 | :root {
12 | --px-per-rem: 16;
13 | --fluid-min-width: (320 / var(--px-per-rem));
14 | --fluid-max-width: (
15 | 1500 / var(--px-per-rem)
16 | ); /* if changed, don't forget media query */
17 | --fluid-min-size: 1; /* in rem */
18 | --fluid-max-size: 1; /* in rem */
19 | --fluid-min-ratio: 1.2;
20 | --fluid-max-ratio: 1.25;
21 |
22 | --fluid-screen: 100vw;
23 | --fluid-bp: calc(
24 | (var(--fluid-screen) - (var(--fluid-min-width) * 1rem)) /
25 | (var(--fluid-max-width) - var(--fluid-min-width))
26 | );
27 |
28 | --fluid-max-negative: (1 / var(--fluid-max-ratio) / var(--fluid-max-ratio));
29 | --fluid-min-negative: (1 / var(--fluid-min-ratio) / var(--fluid-min-ratio));
30 |
31 | --fluid-min-scale--1: var(--fluid-min-ratio) * var(--fluid-min-negative);
32 | --fluid-max-scale--1: var(--fluid-max-ratio) * var(--fluid-max-negative);
33 | --fluid-min-size--1: var(--fluid-min-size) * var(--fluid-min-scale--1);
34 | --fluid-max-size--1: var(--fluid-max-size) * var(--fluid-max-scale--1);
35 | --step--1: calc(
36 | (var(--fluid-min-size--1) * 1rem) +
37 | (var(--fluid-max-size--1) - var(--fluid-min-size--1)) *
38 | var(--fluid-bp)
39 | );
40 |
41 | --fluid-min-scale-0: var(--fluid-min-ratio);
42 | --fluid-max-scale-0: var(--fluid-max-ratio);
43 | --fluid-min-size-0: var(--fluid-min-size);
44 | --fluid-max-size-0: var(--fluid-max-size);
45 | --step-0: calc(
46 | (var(--fluid-min-size-0) * 1rem) +
47 | (var(--fluid-max-size-0) - var(--fluid-min-size-0)) *
48 | var(--fluid-bp)
49 | );
50 |
51 | --fluid-min-scale-1: var(--fluid-min-scale-0) * var(--fluid-min-ratio);
52 | --fluid-max-scale-1: var(--fluid-max-scale-0) * var(--fluid-max-ratio);
53 | --fluid-min-size-1: var(--fluid-min-size) * var(--fluid-min-scale-0);
54 | --fluid-max-size-1: var(--fluid-max-size) * var(--fluid-max-scale-0);
55 | --step-1: calc(
56 | (var(--fluid-min-size-1) * 1rem) +
57 | (var(--fluid-max-size-1) - var(--fluid-min-size-1)) *
58 | var(--fluid-bp)
59 | );
60 |
61 | --fluid-min-scale-2: var(--fluid-min-scale-1) * var(--fluid-min-ratio);
62 | --fluid-max-scale-2: var(--fluid-max-scale-1) * var(--fluid-max-ratio);
63 | --fluid-min-size-2: var(--fluid-min-size) * var(--fluid-min-scale-1);
64 | --fluid-max-size-2: var(--fluid-max-size) * var(--fluid-max-scale-1);
65 | --step-2: calc(
66 | (var(--fluid-min-size-2) * 1rem) +
67 | (var(--fluid-max-size-2) - var(--fluid-min-size-2)) *
68 | var(--fluid-bp)
69 | );
70 |
71 | --fluid-min-scale-3: var(--fluid-min-scale-2) * var(--fluid-min-ratio);
72 | --fluid-max-scale-3: var(--fluid-max-scale-2) * var(--fluid-max-ratio);
73 | --fluid-min-size-3: var(--fluid-min-size) * var(--fluid-min-scale-2);
74 | --fluid-max-size-3: var(--fluid-max-size) * var(--fluid-max-scale-2);
75 | --step-3: calc(
76 | (var(--fluid-min-size-3) * 1rem) +
77 | (var(--fluid-max-size-3) - var(--fluid-min-size-3)) *
78 | var(--fluid-bp)
79 | );
80 |
81 | --fluid-min-scale-4: var(--fluid-min-scale-3) * var(--fluid-min-ratio);
82 | --fluid-max-scale-4: var(--fluid-max-scale-3) * var(--fluid-max-ratio);
83 | --fluid-min-size-4: var(--fluid-min-size) * var(--fluid-min-scale-3);
84 | --fluid-max-size-4: var(--fluid-max-size) * var(--fluid-max-scale-3);
85 | --step-4: calc(
86 | (var(--fluid-min-size-4) * 1rem) +
87 | (var(--fluid-max-size-4) - var(--fluid-min-size-4)) *
88 | var(--fluid-bp)
89 | );
90 |
91 | --fluid-min-scale-5: var(--fluid-min-scale-4) * var(--fluid-min-ratio);
92 | --fluid-max-scale-5: var(--fluid-max-scale-4) * var(--fluid-max-ratio);
93 | --fluid-min-size-5: var(--fluid-min-size) * var(--fluid-min-scale-4);
94 | --fluid-max-size-5: var(--fluid-max-size) * var(--fluid-max-scale-4);
95 | --step-5: calc(
96 | (var(--fluid-min-size-5) * 1rem) +
97 | (var(--fluid-max-size-5) - var(--fluid-min-size-5)) *
98 | var(--fluid-bp)
99 | );
100 |
101 | /*
102 | to deal with Safari fluid scaling on window resize bug:
103 | https://codepen.io/martinwolf/pen/yKgagE
104 | */
105 | min-height: 0vw;
106 | }
107 |
108 | @media screen and (min-width: 1500px) {
109 | :root {
110 | --fluid-screen: calc(var(--fluid-max-width) * 1rem);
111 | }
112 | }
113 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/benchmark.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/benchmark.png
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/caret-bottom.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/caret-right.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/email.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/fairwinds-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/favicon-16x16.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/favicon-16x16.png
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/favicon-32x32.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/favicon-32x32.png
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/favicon.ico
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/fw-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/fw-logo.png
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/goldilocks.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
57 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/icon.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
22 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/insights.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/images/insights.png
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/slack.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/triangle.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/images/twitter.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/js/api-token.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const apiTokenBoxId = "api-token-box";
3 | const disableCostSettingsBtnId = "api-token__disable-cost-settings";
4 | const apiTokenlLabelContentId = "api-token-box__api-token-label-content";
5 | const apiTokenInputId = "api-token-box__api-token-input";
6 | const apiTokenInputErrorId = "api-token-box__input-error";
7 | const submitBtnId = "api-token-box__submit-btn";
8 |
9 | const apiTokenBox = document.getElementById(apiTokenBoxId);
10 | const disableCostSettingsBtn = document.getElementById(
11 | disableCostSettingsBtnId
12 | );
13 | const apiTokenLabelContent = document.getElementById(apiTokenlLabelContentId);
14 | const apiTokenInput = document.getElementById(apiTokenInputId);
15 | const apiTokenInputError = document.getElementById(apiTokenInputErrorId);
16 | const submitBtn = document.getElementById(submitBtnId);
17 |
18 | const apiKey = localStorage.getItem("apiKey");
19 | const isEmailEntered = localStorage.getItem("emailEntered");
20 |
21 | setTimeout(() => {
22 | initUIState();
23 | }, 500);
24 |
25 | function initUIState() {
26 | if (!apiKey && isEmailEntered) {
27 | apiTokenBox.style.display = "block";
28 | }
29 | }
30 |
31 | apiTokenInput.addEventListener("input", function () {
32 | apiTokenInputError.style.display = "none";
33 | toggleLabelContent(this.value);
34 | });
35 |
36 | function toggleLabelContent(inputApiToken) {
37 | apiTokenLabelContent.style.display = inputApiToken
38 | ? "none"
39 | : "inline-block";
40 | }
41 |
42 | submitBtn.addEventListener("click", function (e) {
43 | e.preventDefault();
44 |
45 | if (apiTokenInput.validity.valid) {
46 | const inputApiToken = apiTokenInput.value.trim();
47 | fetch(
48 | `${window.INSIGHTS_HOST}/v0/oss/instance-types?ossToken=${inputApiToken}`
49 | ).then((response) => {
50 | if (response && ![400, 401].includes(response.status)) {
51 | window.location.reload();
52 | localStorage.setItem("apiKey", apiTokenInput.value.trim());
53 | } else {
54 | apiTokenInputError.style.display = "block";
55 | }
56 | });
57 | }
58 | });
59 |
60 | disableCostSettingsBtn.addEventListener("click", function () {
61 | localStorage.removeItem("emailEntered");
62 | localStorage.removeItem("apiKey");
63 |
64 | window.location.href = window.location.href.split("?")[0];
65 | });
66 | })();
67 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/js/email.js:
--------------------------------------------------------------------------------
1 | (function () {
2 | const emailBoxId = "email-box";
3 | const emailLabelContentId = "email-box__email-label-content";
4 | const emailInputId = "email-box__email-input";
5 | const emailInputErrorId = "email-box__input-error";
6 | const emailCheckboxId = "email-box__checkbox";
7 | const submitBtnId = "email-box__submit-btn";
8 |
9 | const emailBox = document.getElementById(emailBoxId);
10 | const emailLabelContent = document.getElementById(emailLabelContentId);
11 | const emailInput = document.getElementById(emailInputId);
12 | const emailInputError = document.getElementById(emailInputErrorId);
13 | const emailCheckbox = document.getElementById(emailCheckboxId);
14 | const submitBtn = document.getElementById(submitBtnId);
15 |
16 | const urlParams = new URLSearchParams(window.location.search);
17 |
18 | setTimeout(() => {
19 | initUIState();
20 | }, 500);
21 |
22 | function initUIState() {
23 | if (!urlParams.get("emailEntered")) {
24 | emailBox.style.display = "block";
25 | }
26 | }
27 |
28 | emailInput.addEventListener("input", function (evt) {
29 | emailInputError.style.display = "none";
30 | toggleLabelContent(this.value);
31 | });
32 |
33 | function toggleLabelContent(inputEmail) {
34 | emailLabelContent.style.display = inputEmail ? "none" : "inline-block";
35 | }
36 |
37 | submitBtn.addEventListener("click", function (e) {
38 | e.preventDefault();
39 | if (emailCheckbox.checked && emailInput.validity.valid) {
40 | fetch(`${window.INSIGHTS_HOST}/v0/oss/users`, {
41 | method: "POST",
42 | headers: {
43 | Accept: "application/json",
44 | "Content-Type": "application/json",
45 | },
46 | body: JSON.stringify({
47 | email: emailInput.value,
48 | project: "goldilocks",
49 | }),
50 | }).then((response) => {
51 | if (response && response.status !== 400) {
52 | response.json().then((data) => {
53 | if (data?.email) {
54 | window.location.reload();
55 | localStorage.setItem("emailEntered", true);
56 | }
57 | });
58 | } else {
59 | emailInputError.style.display = "block";
60 | }
61 | });
62 | }
63 | });
64 | })();
65 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/js/filter.js:
--------------------------------------------------------------------------------
1 | import {
2 | showElement,
3 | hideElement
4 | } from "./utilities.js";
5 |
6 | const form = document.getElementById("js-filter-form");
7 | const container = document.getElementById("js-filter-container");
8 |
9 | /*
10 | These lookups simultaneously test that certain elements and attributes
11 | required for accessibility are present
12 | */
13 | const filterInput = form?.querySelector("input[type='text']");
14 | const potentialResults = container?.querySelectorAll("[data-filter]");
15 |
16 | const outputVisual = form?.querySelector("output[aria-hidden]");
17 | const outputPolite = form?.querySelector("output[aria-live='polite']");
18 | const outputAlert = form?.querySelector("output[role='alert']");
19 |
20 | let statusDelay = null;
21 |
22 | // Test that all expected HTML is present
23 | if (!form) {
24 | console.error("Could not find filter form");
25 | } else if (!filterInput) {
26 | hideElement(form);
27 | console.error("Could not find filter input element, removed filter form");
28 | } else if (!container) {
29 | hideElement(form);
30 | console.error("Could not find filter results container, removed filter form");
31 | } else if (!outputVisual || !outputPolite || !outputAlert) {
32 | hideElement(form);
33 | console.error("Could not find all filter output elements, removed filter form");
34 | } else if (potentialResults.length === 0) {
35 | hideElement(form);
36 | console.error("No filterable entries found, removed filter form");
37 | } else {
38 | // HTML was successfully set up, wire in JS
39 | filterInput.addEventListener("input", runFilter);
40 |
41 | // Handle case where input value doesn't start empty (such as on page refresh)
42 | runFilter();
43 | }
44 |
45 | function runFilter() {
46 | updateResults();
47 | updateStatus();
48 | }
49 |
50 | function updateResults() {
51 | let filterTerm = filterInput.value;
52 |
53 | if (filterTerm) {
54 | let regex = new RegExp(`${ filterTerm.trim().replace(/\s/g, "|") }`, "i");
55 |
56 | for (const result of potentialResults) {
57 | if (regex.test(result.dataset.filter)) {
58 | showElement(result);
59 | } else {
60 | hideElement(result);
61 | }
62 | }
63 | } else {
64 | clearFilter();
65 | }
66 | }
67 |
68 | function clearFilter() {
69 | for (const result of potentialResults) {
70 | showElement(result);
71 | }
72 | }
73 |
74 | function updateStatus() {
75 | const numResults = container?.querySelectorAll("[data-filter]:not([hidden])").length;
76 |
77 | let message, type;
78 |
79 | if (!filterInput.value) {
80 | message = `${potentialResults.length} namespaces found`;
81 | type = "polite";
82 | } else if (numResults === 0) {
83 | message = "No namespaces match filter";
84 | type = "alert";
85 | } else {
86 | message = `Showing ${numResults} out of ${potentialResults.length} namespaces`;
87 | type = "polite";
88 | }
89 |
90 | changeStatusMessage(message, type);
91 | }
92 |
93 | function changeStatusMessage(message, type = "polite") {
94 | if (statusDelay) {
95 | window.clearTimeout(statusDelay);
96 | }
97 |
98 | outputVisual.textContent = message;
99 | outputPolite.textContent = "";
100 | outputAlert.textContent = "";
101 |
102 | /*
103 | If you don't clear the content, then repeats of the same message aren't announced.
104 | There must be a time gap between clearing and injecting new content for this to work.
105 | Delay also:
106 | - Helps make spoken announcements less disruptive by generating fewer of them
107 | - Gives the screen reader a chance to finish announcing what's been typed, which will otherwise talk over these announcements (in MacOS/VoiceOver at least)
108 | */
109 | statusDelay = window.setTimeout(() => {
110 | switch (type) {
111 | case "polite":
112 | outputPolite.textContent = message;
113 | outputAlert.textContent = "";
114 | break;
115 | case "alert":
116 | outputPolite.textContent = "";
117 | outputAlert.textContent = message;
118 | break;
119 | default:
120 | outputPolite.textContent = "Error: There was a problem with the filter.";
121 | outputAlert.textContent = "";
122 | }
123 | }, 1000);
124 | }
125 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/js/main.js:
--------------------------------------------------------------------------------
1 | // For scripts that should always be run on every page
2 |
3 | import { setJavascriptAvailable } from "./utilities.js";
4 |
5 | setJavascriptAvailable();
6 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/js/utilities.js:
--------------------------------------------------------------------------------
1 | function setJavascriptAvailable() {
2 | document.body.dataset.javascriptAvailable = true;
3 | }
4 |
5 | function showElement(element) {
6 | element.removeAttribute("hidden");
7 | }
8 |
9 | function hideElement(element) {
10 | element.setAttribute("hidden", "");
11 | }
12 |
13 | export {
14 | setJavascriptAvailable,
15 | showElement,
16 | hideElement
17 | };
18 |
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/Muli-Bold.tff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/Muli-Bold.tff
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/Muli-Light.tff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/Muli-Light.tff
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/Muli-Regular.tff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/Muli-Regular.tff
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/fa-regular-400.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/fa-regular-400.woff
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/fa-regular-400.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/fa-regular-400.woff2
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/fa-solid-900.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/fa-solid-900.woff
--------------------------------------------------------------------------------
/pkg/dashboard/assets/webfonts/fa-solid-900.woff2:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/FairwindsOps/goldilocks/8b07f7513afaffa1e83972b6fcd9ea1062eb02e4/pkg/dashboard/assets/webfonts/fa-solid-900.woff2
--------------------------------------------------------------------------------
/pkg/dashboard/health.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "net/http"
5 |
6 | "k8s.io/klog/v2"
7 | )
8 |
9 | // Health replies with the status messages given for healthy
10 | func Health(healthyMessage string) http.Handler {
11 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
12 | _, err := w.Write([]byte(healthyMessage))
13 | if err != nil {
14 | klog.Errorf("Error writing healthcheck: %v", err)
15 | }
16 | })
17 | }
18 |
19 | // Healthz replies with a zero byte 200 response
20 | func Healthz() http.Handler {
21 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {})
22 | }
23 |
--------------------------------------------------------------------------------
/pkg/dashboard/helpers/helpers.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package helpers
16 |
17 | import (
18 | "reflect"
19 |
20 | "github.com/google/uuid"
21 | corev1 "k8s.io/api/core/v1"
22 | "k8s.io/apimachinery/pkg/api/resource"
23 | )
24 |
25 | func PrintResource(quant resource.Quantity) string {
26 | if quant.IsZero() {
27 | return "Not Set"
28 | }
29 | return quant.String()
30 | }
31 |
32 | func GetStatus(existing resource.Quantity, recommendation resource.Quantity, style string) string {
33 | if existing.IsZero() {
34 | switch style {
35 | case "text":
36 | return "error - not set"
37 | case "icon":
38 | return "fa-exclamation error"
39 | default:
40 | return ""
41 | }
42 | }
43 |
44 | comparison := existing.Cmp(recommendation)
45 | if comparison == 0 {
46 | switch style {
47 | case "text":
48 | return "equal"
49 | case "icon":
50 | return "fa-equals success"
51 | default:
52 | return ""
53 | }
54 | }
55 | if comparison < 0 {
56 | switch style {
57 | case "text":
58 | return "less than"
59 | case "icon":
60 | return "fa-less-than warning"
61 | default:
62 | return ""
63 | }
64 | }
65 | if comparison > 0 {
66 | switch style {
67 | case "text":
68 | return "greater than"
69 | case "icon":
70 | return "fa-greater-than warning"
71 | default:
72 | return ""
73 | }
74 | }
75 | return ""
76 | }
77 |
78 | func GetStatusRange(existing, lower, upper resource.Quantity, style string, resourceType string) string {
79 | if existing.IsZero() {
80 | switch style {
81 | case "text":
82 | return "error - not set"
83 | case "icon":
84 | return "fa-exclamation error"
85 | default:
86 | return ""
87 | }
88 | }
89 |
90 | comparisonLower := existing.Cmp(lower)
91 | comparisonUpper := existing.Cmp(upper)
92 |
93 | if comparisonLower < 0 {
94 | switch style {
95 | case "text":
96 | return "less than"
97 | case "icon":
98 | return "fa-less-than warning"
99 | }
100 | }
101 |
102 | if comparisonUpper > 0 {
103 | switch style {
104 | case "text":
105 | return "greater than"
106 | case "icon":
107 | return "fa-greater-than warning"
108 | }
109 | }
110 |
111 | switch resourceType {
112 | case "request":
113 | if comparisonLower == 0 {
114 | switch style {
115 | case "text":
116 | return "equal"
117 | case "icon":
118 | return "fa-equals success"
119 | }
120 | }
121 | case "limit":
122 | if comparisonUpper == 0 {
123 | switch style {
124 | case "text":
125 | return "equal"
126 | case "icon":
127 | return "fa-equals success"
128 | }
129 | }
130 | }
131 |
132 | switch style {
133 | case "text":
134 | return "not equal"
135 | case "icon":
136 | return "fa-exclamation error"
137 | }
138 |
139 | return ""
140 | }
141 |
142 | func ResourceName(name string) corev1.ResourceName {
143 | return corev1.ResourceName(name)
144 | }
145 |
146 | func GetUUID() string {
147 | return uuid.New().String()
148 | }
149 |
150 | func HasField(v interface{}, name string) bool {
151 | rv := reflect.ValueOf(v)
152 | if rv.Kind() == reflect.Ptr {
153 | rv = rv.Elem()
154 | }
155 | if rv.Kind() != reflect.Struct {
156 | return false
157 | }
158 | return rv.FieldByName(name).IsValid()
159 | }
160 |
--------------------------------------------------------------------------------
/pkg/dashboard/namespace-list.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "context"
5 | "fmt"
6 | "net/http"
7 |
8 | "github.com/fairwindsops/goldilocks/pkg/kube"
9 | "github.com/fairwindsops/goldilocks/pkg/utils"
10 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
11 | "k8s.io/apimachinery/pkg/labels"
12 | "k8s.io/klog/v2"
13 | )
14 |
15 | // NamespaceList replies with the rendered namespace list of all goldilocks enabled namespaces
16 | func NamespaceList(opts Options) http.Handler {
17 | return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
18 | var listOptions v1.ListOptions
19 | if opts.OnByDefault || opts.ShowAllVPAs {
20 | listOptions = v1.ListOptions{
21 | LabelSelector: fmt.Sprintf("%s!=false", utils.VpaEnabledLabel),
22 | }
23 | } else {
24 | listOptions = v1.ListOptions{
25 | LabelSelector: labels.Set(map[string]string{
26 | utils.VpaEnabledLabel: "true",
27 | }).String(),
28 | }
29 | }
30 | namespacesList, err := kube.GetInstance().Client.CoreV1().Namespaces().List(context.TODO(), listOptions)
31 | if err != nil {
32 | klog.Errorf("Error getting namespace list: %v", err)
33 | http.Error(w, "Error getting namespace list", http.StatusInternalServerError)
34 | return
35 | }
36 |
37 | tmpl, err := getTemplate("namespace_list", opts,
38 | "filter",
39 | "namespace_list",
40 | )
41 | if err != nil {
42 | klog.Errorf("Error getting template data: %v", err)
43 | http.Error(w, "Error getting template data", http.StatusInternalServerError)
44 | return
45 | }
46 |
47 | // only expose the needed data from Namespace
48 | // this helps to not leak additional information like
49 | // annotations, labels, metadata about the Namespace to the
50 | // client UI source code or javascript console
51 |
52 | data := struct {
53 | Namespaces []struct {
54 | Name string
55 | }
56 | }{}
57 |
58 | for _, ns := range namespacesList.Items {
59 | item := struct {
60 | Name string
61 | }{
62 | Name: ns.Name,
63 | }
64 | data.Namespaces = append(data.Namespaces, item)
65 | }
66 |
67 | writeTemplate(tmpl, opts, &data, w)
68 | })
69 | }
70 |
--------------------------------------------------------------------------------
/pkg/dashboard/options.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "github.com/fairwindsops/goldilocks/pkg/utils"
5 | "k8s.io/apimachinery/pkg/util/sets"
6 | )
7 |
8 | // Option is a Functional options
9 | type Option func(*Options)
10 |
11 | // Options are options for getting and caching the Summarizer's VPAs
12 | type Options struct {
13 | Port int
14 | BasePath string
15 | VpaLabels map[string]string
16 | ExcludedContainers sets.Set[string]
17 | OnByDefault bool
18 | ShowAllVPAs bool
19 | InsightsHost string
20 | EnableCost bool
21 | }
22 |
23 | // default options for the dashboard
24 | func defaultOptions() *Options {
25 | return &Options{
26 | Port: 8080,
27 | BasePath: "/",
28 | VpaLabels: utils.VPALabels,
29 | ExcludedContainers: sets.Set[string]{},
30 | OnByDefault: false,
31 | ShowAllVPAs: false,
32 | EnableCost: true,
33 | }
34 | }
35 |
36 | // OnPort is an Option for running the dashboard on a different port
37 | func OnPort(port int) Option {
38 | return func(opts *Options) {
39 | opts.Port = port
40 | }
41 | }
42 |
43 | // ExcludeContainers is an Option for excluding containers in the dashboard summary
44 | func ExcludeContainers(excludedContainers sets.Set[string]) Option {
45 | return func(opts *Options) {
46 | opts.ExcludedContainers = excludedContainers
47 | }
48 | }
49 |
50 | // ForVPAsWithLabels Option for limiting the dashboard to certain VPAs matching the labels
51 | func ForVPAsWithLabels(vpaLabels map[string]string) Option {
52 | return func(opts *Options) {
53 | opts.VpaLabels = vpaLabels
54 | }
55 | }
56 |
57 | // OnByDefault is an option for listing all namespaces in the dashboard unless explicitly excluded
58 | func OnByDefault(onByDefault bool) Option {
59 | return func(opts *Options) {
60 | opts.OnByDefault = onByDefault
61 | }
62 | }
63 |
64 | func ShowAllVPAs(showAllVPAs bool) Option {
65 | return func(opts *Options) {
66 | opts.ShowAllVPAs = showAllVPAs
67 | }
68 | }
69 |
70 | func BasePath(basePath string) Option {
71 | return func(opts *Options) {
72 | opts.BasePath = basePath
73 | }
74 | }
75 |
76 | func InsightsHost(insightsHost string) Option {
77 | return func(opts *Options) {
78 | opts.InsightsHost = insightsHost
79 | }
80 | }
81 |
82 | func EnableCost(enableCost bool) Option {
83 | return func(opts *Options) {
84 | opts.EnableCost = enableCost
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/pkg/dashboard/router.go:
--------------------------------------------------------------------------------
1 | // Copyright 2019 FairwindsOps Inc
2 | //
3 | // Licensed under the Apache License, Version 2.0 (the "License");
4 | // you may not use this file except in compliance with the License.
5 | // You may obtain a copy of the License at
6 | //
7 | // http://www.apache.org/licenses/LICENSE-2.0
8 | //
9 | // Unless required by applicable law or agreed to in writing, software
10 | // distributed under the License is distributed on an "AS IS" BASIS,
11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 | // See the License for the specific language governing permissions and
13 | // limitations under the License.
14 |
15 | package dashboard
16 |
17 | import (
18 | "net/http"
19 | "path"
20 | "strings"
21 |
22 | "k8s.io/klog/v2"
23 |
24 | packr "github.com/gobuffalo/packr/v2"
25 | "github.com/gorilla/mux"
26 | )
27 |
28 | var (
29 | markdownBox = (*packr.Box)(nil)
30 | )
31 |
32 | // GetMarkdownBox returns a binary-friendly set of markdown files with error details
33 | func GetMarkdownBox() *packr.Box {
34 | if markdownBox == (*packr.Box)(nil) {
35 | markdownBox = packr.New("Markdown", "../../docs")
36 | }
37 | return markdownBox
38 | }
39 |
40 | func GetAssetBox() *packr.Box {
41 | if assetBox == (*packr.Box)(nil) {
42 | assetBox = packr.New("Assets", "assets")
43 | }
44 | return assetBox
45 | }
46 |
47 | // GetRouter returns a mux router serving all routes necessary for the dashboard
48 | func GetRouter(setters ...Option) *mux.Router {
49 | opts := defaultOptions()
50 | for _, setter := range setters {
51 | setter(opts)
52 | }
53 |
54 | router := mux.NewRouter().PathPrefix(strings.TrimSuffix(opts.BasePath, "/")).Subrouter().StrictSlash(true)
55 |
56 | // health
57 | router.Handle("/health", Health("OK"))
58 | router.Handle("/healthz", Healthz())
59 |
60 | // assets
61 | router.Handle("/favicon.ico", Asset("/images/favicon-32x32.png"))
62 | fileServer := http.FileServer(GetAssetBox())
63 | router.PathPrefix("/static/").Handler(http.StripPrefix(path.Join(opts.BasePath, "/static/"), fileServer))
64 |
65 | // dashboard
66 | router.Handle("/dashboard", Dashboard(*opts))
67 | router.Handle("/dashboard/{namespace:[a-zA-Z0-9-]+}", Dashboard(*opts))
68 |
69 | // namespace list
70 | router.Handle("/namespaces", NamespaceList(*opts))
71 |
72 | // root
73 | router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
74 | // catch all other paths that weren't matched
75 | if r.URL.Path != "/" && r.URL.Path != opts.BasePath && r.URL.Path != opts.BasePath+"/" {
76 | klog.Infof("404: %s", r.URL.Path)
77 | http.NotFound(w, r)
78 | return
79 | }
80 |
81 | klog.Infof("redirecting to %v", path.Join(opts.BasePath, "/namespaces"))
82 | // default redirect on root path
83 | http.Redirect(w, r, path.Join(opts.BasePath, "/namespaces"), http.StatusMovedPermanently)
84 | })
85 |
86 | // api
87 | router.Handle("/api/{namespace:[a-zA-Z0-9-]+}", API(*opts))
88 | return router
89 | }
90 |
--------------------------------------------------------------------------------
/pkg/dashboard/templates.go:
--------------------------------------------------------------------------------
1 | package dashboard
2 |
3 | import (
4 | "bytes"
5 | "encoding/json"
6 | "fmt"
7 | "html/template"
8 | "net/http"
9 | "strings"
10 |
11 | "github.com/fairwindsops/goldilocks/pkg/dashboard/helpers"
12 | "github.com/gobuffalo/packr/v2"
13 | "k8s.io/klog/v2"
14 | )
15 |
16 | var templateBox = (*packr.Box)(nil)
17 |
18 | // templates
19 | const (
20 | ContainerTemplateName = "container.gohtml"
21 | DashboardTemplateName = "dashboard.gohtml"
22 | FilterTemplateName = "filter.gohtml"
23 | FooterTemplateName = "footer.gohtml"
24 | HeadTemplateName = "head.gohtml"
25 | NamespaceTemplateName = "namespace.gohtml"
26 | NavigationTemplateName = "navigation.gohtml"
27 | EmailTemplateName = "email.gohtml"
28 | ApiTokenTemplateName = "api_token.gohtml"
29 | CostSettingTemplateName = "cost_settings.gohtml"
30 | )
31 |
32 | var (
33 | // templates with these names are included by default in getTemplate()
34 | defaultIncludedTemplates = []string{
35 | "head",
36 | "navigation",
37 | "footer",
38 | }
39 | )
40 |
41 | // to be included in data structs fo
42 | type baseTemplateData struct {
43 | // BasePath is the base URL that goldilocks is being served on, used in templates for html base
44 | BasePath string
45 |
46 | // Data is the data struct passed to writeTemplate()
47 | Data interface{}
48 |
49 | // JSON is the json version of Data
50 | JSON template.JS
51 | }
52 |
53 | // getTemplateBox returns a binary-friendly set of templates for rendering the dash
54 | func getTemplateBox() *packr.Box {
55 | if templateBox == (*packr.Box)(nil) {
56 | templateBox = packr.New("Templates", "templates")
57 | }
58 | return templateBox
59 | }
60 |
61 | // getTemplate puts together a template. Individual pieces can be overridden before rendering.
62 | func getTemplate(name string, opts Options, includedTemplates ...string) (*template.Template, error) {
63 | tmpl := template.New(name).Funcs(template.FuncMap{
64 | "printResource": helpers.PrintResource,
65 | "getStatus": helpers.GetStatus,
66 | "getStatusRange": helpers.GetStatusRange,
67 | "resourceName": helpers.ResourceName,
68 | "getUUID": helpers.GetUUID,
69 | "hasField": helpers.HasField,
70 |
71 | "opts": func() Options {
72 | return opts
73 | },
74 | })
75 |
76 | // join the default templates and included templates
77 | templatesToParse := make([]string, 0, len(includedTemplates)+len(defaultIncludedTemplates))
78 | templatesToParse = append(templatesToParse, defaultIncludedTemplates...)
79 | templatesToParse = append(templatesToParse, includedTemplates...)
80 |
81 | return parseTemplateFiles(tmpl, templatesToParse)
82 | }
83 |
84 | // parseTemplateFiles combines the template with the included templates into one parsed template
85 | func parseTemplateFiles(tmpl *template.Template, includedTemplates []string) (*template.Template, error) {
86 | templateBox := getTemplateBox()
87 | for _, fname := range includedTemplates {
88 | templateFile, err := templateBox.Find(fmt.Sprintf("%s.gohtml", fname))
89 | if err != nil {
90 | return nil, err
91 | }
92 |
93 | tmpl, err = tmpl.Parse(string(templateFile))
94 | if err != nil {
95 | return nil, err
96 | }
97 | }
98 |
99 | return tmpl, nil
100 | }
101 |
102 | // writeTemplate executes the given template with the data and writes to the writer.
103 | func writeTemplate(tmpl *template.Template, opts Options, data interface{}, w http.ResponseWriter) {
104 | buf := &bytes.Buffer{}
105 | jsonData, err := json.Marshal(data)
106 | if err != nil {
107 | http.Error(w, "Error serializing template jsonData", http.StatusInternalServerError)
108 | return
109 | }
110 | err = tmpl.Execute(buf, baseTemplateData{
111 | BasePath: validateBasePath(opts.BasePath),
112 | Data: data,
113 | JSON: template.JS(jsonData),
114 | })
115 | if err != nil {
116 | klog.Errorf("Error executing template: %v", err)
117 | http.Error(w, err.Error(), http.StatusInternalServerError)
118 | return
119 | }
120 | _, err = buf.WriteTo(w)
121 | if err != nil {
122 | klog.Errorf("Error writing template: %v", err)
123 | }
124 | }
125 |
126 | func validateBasePath(path string) string {
127 | if path == "/" {
128 | return path
129 | }
130 |
131 | if !strings.HasSuffix(path, "/") {
132 | path = path + "/"
133 | }
134 |
135 | return path
136 | }
137 |
--------------------------------------------------------------------------------
/pkg/dashboard/templates/api_token.gohtml:
--------------------------------------------------------------------------------
1 | {{ define "api_token" }}
2 |
20 | {{ end }}
21 |
--------------------------------------------------------------------------------
/pkg/dashboard/templates/cost_settings.gohtml:
--------------------------------------------------------------------------------
1 | {{ define "cost_settings" }}
2 |
43 | {{ end }}
44 |
--------------------------------------------------------------------------------
/pkg/dashboard/templates/dashboard.gohtml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | {{ template "head" .Data }}
5 |
6 | {{ if gt (len .Data.VpaData.Namespaces) 1 }}
7 |
8 | {{ end }}
9 |
10 | {{- if opts.EnableCost }}
11 |
18 |
19 |
22 |
23 |
24 |
25 |
26 | {{- end }}
27 |
28 |
29 |
30 |
31 | {{ template "navigation" . }}
32 |
33 |
34 |