├── .circleci └── config.yml ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── feature_request.md │ └── other.md ├── dependabot.yml ├── pull_request_template.md └── workflows │ └── stale.yml ├── .gitignore ├── .goreleaser.yml ├── .pre-commit-config.yaml ├── CHANGELOG.md ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── DESIGN.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── ROADMAP.md ├── cmd ├── controller.go ├── create.go ├── dashboard.go ├── delete.go ├── root.go ├── summary.go └── version.go ├── docs ├── .vuepress │ ├── config-extras.js │ ├── config.js │ ├── public │ │ ├── favicon.png │ │ ├── img │ │ │ ├── fairwinds-logo.svg │ │ │ ├── goldilocks.svg │ │ │ ├── insights-banner.png │ │ │ └── screenshot.png │ │ └── scripts │ │ │ ├── marketing.js │ │ │ └── modify.js │ ├── styles │ │ ├── index.styl │ │ └── palette.styl │ └── theme │ │ ├── index.js │ │ └── layouts │ │ └── Layout.vue ├── README.md ├── advanced.md ├── contributing │ ├── code-of-conduct.md │ └── guide.md ├── faq.md ├── installation.md ├── main-metadata.md ├── package-lock.json └── package.json ├── e2e ├── .gitignore ├── pre.sh ├── run.sh └── tests │ ├── 00_setup.yaml │ ├── 10_basic.yaml │ ├── 20_flags.yaml │ └── 99_cleanup.yaml ├── go.mod ├── go.sum ├── hack ├── kind │ ├── course.yml │ ├── demo-values.yaml │ ├── load-generator-values.yaml │ └── setup.sh └── manifests │ ├── controller │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ └── serviceaccount.yaml │ └── dashboard │ ├── clusterrole.yaml │ ├── clusterrolebinding.yaml │ ├── deployment.yaml │ ├── service.yaml │ └── serviceaccount.yaml ├── img ├── goldilocks.svg └── screenshot.png ├── main.go └── pkg ├── controller ├── controller.go └── controller_test.go ├── dashboard ├── assets.go ├── assets │ ├── css │ │ ├── font-muli.css │ │ ├── fontawesome-5.7.2.css │ │ ├── main.css │ │ ├── prism.css │ │ ├── reset.css │ │ └── utopia.css │ ├── images │ │ ├── benchmark.png │ │ ├── caret-bottom.svg │ │ ├── caret-right.svg │ │ ├── email.svg │ │ ├── fairwinds-logo.svg │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon.ico │ │ ├── fw-logo.png │ │ ├── github.svg │ │ ├── goldilocks.svg │ │ ├── icon.svg │ │ ├── insights.png │ │ ├── slack.svg │ │ ├── triangle.svg │ │ └── twitter.svg │ ├── js │ │ ├── api-token.js │ │ ├── cost_settings.js │ │ ├── email.js │ │ ├── filter.js │ │ ├── main.js │ │ ├── prism.js │ │ └── utilities.js │ └── webfonts │ │ ├── Muli-Bold.tff │ │ ├── Muli-Light.tff │ │ ├── Muli-Regular.tff │ │ ├── fa-regular-400.woff │ │ ├── fa-regular-400.woff2 │ │ ├── fa-solid-900.woff │ │ └── fa-solid-900.woff2 ├── dashboard.go ├── health.go ├── helpers │ ├── helpers.go │ └── helpers_test.go ├── namespace-list.go ├── options.go ├── router.go ├── templates.go └── templates │ ├── api_token.gohtml │ ├── container.gohtml │ ├── cost_settings.gohtml │ ├── dashboard.gohtml │ ├── email.gohtml │ ├── filter.gohtml │ ├── footer.gohtml │ ├── head.gohtml │ ├── namespace.gohtml │ ├── namespace_list.gohtml │ └── navigation.gohtml ├── handler ├── handler.go ├── namespace.go └── pod.go ├── kube ├── client.go ├── client_test.go └── test_helpers.go ├── summary ├── constants_test.go ├── options.go ├── summary.go └── summary_test.go ├── utils ├── utils.go └── utils_test.go └── vpa ├── test_constants.go ├── vpa.go └── vpa_test.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | version: 2.1 2 | 3 | orbs: 4 | rok8s: fairwinds/rok8s-scripts@12 5 | oss-docs: fairwinds/oss-docs@0 6 | 7 | references: 8 | install_vault: &install_vault 9 | run: 10 | name: install hashicorp vault 11 | command: | 12 | apk --update add curl yq 13 | cd /tmp 14 | curl -LO https://releases.hashicorp.com/vault/1.13.2/vault_1.13.2_linux_amd64.zip 15 | sha256sum vault_1.13.2_linux_amd64.zip | grep f7930279de8381de7c532164b4a4408895d9606c0d24e2e9d2f9acb5dfe99b3c 16 | unzip vault_1.13.2_linux_amd64.zip 17 | mv vault /usr/bin/vault 18 | e2e_configuration: &e2e_configuration 19 | pre_script: e2e/pre.sh 20 | script: e2e/run.sh 21 | command_runner_image: quay.io/reactiveops/ci-images:v12-buster 22 | enable_docker_layer_caching: true 23 | store-test-results: /tmp/test-results 24 | attach-workspace: true 25 | requires: 26 | - test 27 | - snapshot 28 | filters: 29 | branches: 30 | only: /.*/ 31 | tags: 32 | ignore: /v.*/ 33 | 34 | jobs: 35 | test: 36 | working_directory: /home/circleci/go/src/github.com/fairwindsops/goldilocks 37 | docker: 38 | - image: cimg/go:1.22 39 | environment: 40 | GL_DEBUG: linters_output 41 | GOPACKAGESPRINTGOLISTERRORS: "1" 42 | steps: 43 | - checkout 44 | - run: 45 | name: golangci-lint 46 | environment: 47 | GOLANGCI_LINT_VERSION: 1.61.0 48 | GOLANGCI_LINT_CHECKSUM: 77cb0af99379d9a21d5dc8c38364d060e864a01bd2f3e30b5e8cc550c3a54111 49 | command: | 50 | curl -OL https://github.com/golangci/golangci-lint/releases/download/v${GOLANGCI_LINT_VERSION}/golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz 51 | [[ "$(sha256sum golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz)" == "${GOLANGCI_LINT_CHECKSUM} golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz" ]] 52 | tar xzf golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64.tar.gz && mv golangci-lint-${GOLANGCI_LINT_VERSION}-linux-amd64/golangci-lint $(go env GOPATH)/bin/golangci-lint 53 | go mod tidy 54 | golangci-lint run -v --timeout 2m0s 55 | - run: 56 | name: Run Tests 57 | command: make test 58 | 59 | build_and_release: 60 | working_directory: /go/src/github.com/fairwindsops/goldilocks 61 | resource_class: large 62 | shell: /bin/bash 63 | docker: 64 | - image: goreleaser/goreleaser:v2.3.2 65 | steps: 66 | - checkout 67 | - setup_remote_docker 68 | - *install_vault 69 | - rok8s/get_vault_env: 70 | vault_path: repo/global/env 71 | - run: 72 | name: docker login 73 | command: | 74 | docker login -u _json_key -p "$(echo $GCP_ARTIFACTREADWRITE_JSON_KEY | base64 -d)" us-docker.pkg.dev 75 | - run: echo 'export GORELEASER_CURRENT_TAG="${CIRCLE_TAG}"' >> $BASH_ENV 76 | - run: goreleaser --skip=sign 77 | snapshot: 78 | working_directory: /go/src/github.com/fairwindsops/goldilocks 79 | resource_class: large 80 | docker: 81 | - image: goreleaser/goreleaser:v2.3.2 82 | steps: 83 | - checkout 84 | - setup_remote_docker 85 | - run: goreleaser --snapshot --skip=sign 86 | - run: mkdir -p /tmp/workspace/docker_save/ 87 | - run: docker save us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:${CIRCLE_SHA1}-amd64 > /tmp/workspace/docker_save/goldilocks_${CIRCLE_SHA1}-amd64.tar 88 | - persist_to_workspace: 89 | root: /tmp/workspace/ 90 | paths: 91 | - docker_save 92 | - store_artifacts: 93 | path: dist 94 | destination: snapshot 95 | 96 | workflows: 97 | version: 2 98 | tests: 99 | jobs: 100 | - test 101 | - snapshot: 102 | context: org-global 103 | requires: 104 | - test 105 | - rok8s/kubernetes_e2e_tests: 106 | name: "End-To-End Kubernetes 1.24" 107 | kind_node_image: "kindest/node:v1.24.7@sha256:577c630ce8e509131eab1aea12c022190978dd2f745aac5eb1fe65c0807eb315" 108 | <<: *e2e_configuration 109 | - rok8s/kubernetes_e2e_tests: 110 | name: "End-To-End Kubernetes 1.25" 111 | kind_node_image: "kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1" 112 | <<: *e2e_configuration 113 | - rok8s/kubernetes_e2e_tests: 114 | name: "End-To-End Kubernetes 1.26" 115 | kind_node_image: "kindest/node:v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb" 116 | <<: *e2e_configuration 117 | - rok8s/kubernetes_e2e_tests: 118 | name: "End-To-End Kubernetes 1.27" 119 | kind_node_image: "kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72" 120 | <<: *e2e_configuration 121 | release: 122 | jobs: 123 | - build_and_release: 124 | filters: 125 | branches: 126 | ignore: /.*/ 127 | tags: 128 | only: /v.*/ 129 | - oss-docs/publish-docs: 130 | repository: goldilocks 131 | filters: 132 | branches: 133 | ignore: /.*/ 134 | tags: 135 | only: /^.*/ 136 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.svg whitespace=-trailing-space 2 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: File a bug report 3 | labels: [bug, triage] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to fill out this bug report! Please fill the form below. 9 | - type: textarea 10 | id: what-happened 11 | attributes: 12 | label: What happened? 13 | description: What happened? 14 | validations: 15 | required: true 16 | - type: textarea 17 | id: expected 18 | attributes: 19 | label: What did you expect to happen? 20 | description: What is the expected or desired behavior? 21 | validations: 22 | required: true 23 | - type: textarea 24 | id: reproducible 25 | attributes: 26 | label: How can we reproduce this? 27 | description: Please share the steps that we can take to reproduce this. Also include any relevant configuration. 28 | validations: 29 | required: true 30 | - type: input 31 | id: version 32 | attributes: 33 | label: Version 34 | description: The version of the tool that you are using. If a helm chart, please share the name of the chart. 35 | validations: 36 | required: true 37 | - type: checkboxes 38 | id: search 39 | attributes: 40 | label: Search 41 | options: 42 | - label: I did search for other open and closed issues before opening this. 43 | required: true 44 | - type: checkboxes 45 | id: terms 46 | attributes: 47 | label: Code of Conduct 48 | description: By submitting this issue, you agree to follow the CODE_OF_CONDUCT in this repository. 49 | options: 50 | - label: I agree to follow this project's Code of Conduct 51 | required: true 52 | - type: textarea 53 | id: ctx 54 | attributes: 55 | label: Additional context 56 | description: Anything else you would like to add 57 | validations: 58 | required: false 59 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: [triage, enhancement] 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/other.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Other 3 | about: For misc. tasks like research or continued conversation 4 | title: '' 5 | labels: [triage] 6 | assignees: '' 7 | 8 | --- 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | ## DO NOT EDIT - Managed by Terraform 2 | version: 2 3 | updates: 4 | - package-ecosystem: "docker" 5 | directory: "/" 6 | schedule: 7 | interval: "weekly" 8 | 9 | - package-ecosystem: "npm" 10 | directory: "/docs" 11 | schedule: 12 | interval: "weekly" 13 | open-pull-requests-limit: 0 14 | ignore: 15 | - dependency-name: "*" 16 | 17 | - package-ecosystem: "gomod" 18 | directory: "/" 19 | schedule: 20 | interval: "weekly" 21 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 2 | This PR fixes # 3 | 4 | ## Checklist 5 | * [ ] I have signed the CLA 6 | * [ ] I have updated/added any relevant documentation 7 | 8 | ## Description 9 | ### What's the goal of this PR? 10 | 11 | ### What changes did you make? 12 | 13 | ### What alternative solution should we consider, if any? 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: 'Close stale issues and PRs' 2 | on: 3 | schedule: 4 | - cron: '32 1 * * *' 5 | 6 | permissions: 7 | issues: write 8 | pull-requests: write 9 | 10 | jobs: 11 | stale: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/stale@v4 15 | with: 16 | exempt-issue-labels: pinned 17 | stale-pr-label: stale 18 | stale-issue-label: stale 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | goldilocks 2 | cover* 3 | goreleaser.deb 4 | *-packr.go 5 | dist 6 | packrd 7 | coverage.txt 8 | *.out 9 | debug 10 | 11 | hack/kind/autoscaler 12 | .vscode 13 | __debug_bin 14 | node_modules 15 | /dist 16 | docs/README.md 17 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | - go install github.com/gobuffalo/packr/v2/packr2@latest 5 | - packr2 6 | builds: 7 | - id: goldilocks 8 | ldflags: 9 | - -X main.version={{.Version}} -X main.commit={{.Commit}} -s -w 10 | goarch: 11 | - amd64 12 | - arm 13 | - arm64 14 | env: 15 | - CGO_ENABLED=0 16 | - GO111MODULE=on 17 | goos: 18 | - linux 19 | - darwin 20 | goarm: 21 | - 6 22 | - 7 23 | changelog: 24 | sort: asc 25 | filters: 26 | exclude: 27 | - '^docs:' 28 | - '^test:' 29 | release: 30 | prerelease: auto 31 | footer: | 32 | You can verify the signatures of both the checksums.txt file and the published docker images using [cosign](https://github.com/sigstore/cosign). 33 | 34 | ``` 35 | sha256sum -c goldilocks_v{{ .Major }}.{{ .Minor }}.{{ .Patch }}_checksums.txt --ignore-missing 36 | cosign verify-blob goldilocks_v{{ .Major }}.{{ .Minor }}.{{ .Patch }}_checksums.txt --signature=goldilocks_v{{ .Major }}.{{ .Minor }}.{{ .Patch }}_checksums.txt.sig --key https://artifacts.fairwinds.com/cosign.pub 37 | ``` 38 | 39 | ``` 40 | cosign verify us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v4 --key https://artifacts.fairwinds.com/cosign.pub 41 | ``` 42 | signs: 43 | - cmd: cosign 44 | args: ["sign-blob", "--key=hashivault://cosign", "-output-signature=${signature}", "${artifact}"] 45 | artifacts: checksum 46 | 47 | docker_signs: 48 | - artifacts: all 49 | args: ["sign", "--key=hashivault://cosign", "${artifact}", "-r"] 50 | dockers: 51 | - image_templates: 52 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .FullCommit }}-amd64" 53 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-amd64" 54 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-amd64" 55 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-amd64" 56 | use: buildx 57 | dockerfile: Dockerfile 58 | build_flag_templates: 59 | - "--platform=linux/amd64" 60 | - image_templates: 61 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-arm64v8" 62 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-arm64v8" 63 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-arm64v8" 64 | use: buildx 65 | goarch: arm64 66 | dockerfile: Dockerfile 67 | build_flag_templates: 68 | - "--platform=linux/arm64/v8" 69 | - image_templates: 70 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-armv7" 71 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-armv7" 72 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-armv7" 73 | use: buildx 74 | goarch: arm64 75 | dockerfile: Dockerfile 76 | build_flag_templates: 77 | - "--platform=linux/arm/v7" 78 | docker_manifests: 79 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }} 80 | image_templates: 81 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-amd64" 82 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-arm64v8" 83 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:{{ .Tag }}-armv7" 84 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }} 85 | image_templates: 86 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-amd64" 87 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-arm64v8" 88 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}-armv7" 89 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }} 90 | image_templates: 91 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-amd64" 92 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-arm64v8" 93 | - "us-docker.pkg.dev/fairwinds-ops/oss/goldilocks:v{{ .Major }}.{{ .Minor }}-armv7" 94 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | repos: 3 | - repo: https://github.com/pre-commit/pre-commit-hooks 4 | rev: v2.3.0 5 | hooks: 6 | - id: check-yaml 7 | args: ['--allow-multiple-documents'] 8 | - id: check-json 9 | - id: detect-private-key 10 | - id: trailing-whitespace 11 | - id: check-added-large-files 12 | args: ['--maxkb=500'] 13 | - id: check-byte-order-marker 14 | - id: check-merge-conflict 15 | - id: check-symlinks 16 | - id: end-of-file-fixer 17 | - id: check-executables-have-shebangs 18 | - id: flake8 19 | - id: no-commit-to-branch 20 | args: [--branch, master] 21 | - id: pretty-format-json 22 | args: ['--autofix'] 23 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 24 | rev: 1.11.0 25 | hooks: 26 | - id: forbid-binary 27 | exclude: > 28 | (?x)^( 29 | .+\.png| 30 | .+\.woff| 31 | .+\.woff2| 32 | .+\.tff| 33 | .+\.ico 34 | )$ 35 | 36 | - id: shellcheck 37 | - id: git-check 38 | - repo: https://github.com/dnephin/pre-commit-golang.git 39 | rev: v0.3.4 40 | hooks: 41 | - id: go-fmt 42 | - id: go-vet 43 | - id: go-lint 44 | - id: go-unit-tests 45 | - id: go-build 46 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | See the [Releases](https://github.com/FairwindsOps/goldilocks/releases) page for relevant changes. 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | ## DO NOT EDIT - Managed by Terraform 2 | * @sudermanjr @transient1 3 | -------------------------------------------------------------------------------- /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 opensource@fairwinds.com. 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 | -------------------------------------------------------------------------------- /DESIGN.md: -------------------------------------------------------------------------------- 1 | # goldilocks 2 | 3 | ## People 4 | 5 | Owner: @sudermanjr 6 | Only other main contributor: @davekonopka 7 | 8 | ## Intent 9 | 10 | The intent of goldilocks is to utilize an already somewhat-stable resource recommendation engine and aggregate the results into a nice dashboard. The goal is to allow end-users to easily see a sane recommendation for how to set their resource requests and limits on their containers. 11 | 12 | ## Key Elements 13 | 14 | * [VPA (vertical pod autoscaler)](https://github.com/kubernetes/autoscaler/tree/master/vertical-pod-autoscaler) is the core of the project. It will be utilized to provide the resource recommendations. 15 | * Controller - manage the creation and deletion of VPA objects in specified namespaces. 16 | * Dashboard - Aggregate and display the results 17 | * CLI - Manually manage VPA creation, deletion, and provide a JSON summary. 18 | 19 | 20 | ## Scope 21 | ### In Scope: 22 | 23 | * Aggregating resource recommednations from existing sources. 24 | 25 | ### Out of Scope: 26 | 27 | * Writing our own recommendation engine. 28 | 29 | ## Architecture 30 | 31 | ### Labelling 32 | 33 | Right now, goldilocks labels all VPA objects that are created with two unique labels. These are used to keep track of the objects that it creates. 34 | 35 | In addition, namespace labels are utilized. The label `goldilocks.fairwinds.com/enabled=true` is used to signify that the deployments in a namespace should be given VPAs. 36 | 37 | ### Controller 38 | 39 | The controller watches Kubernetes events for Deployments and Namespaces that have been modified, created, or deleted. When one of these is changed, the namespace that is involved is "reconciled". This means checking to see if the namespace is labelled for goldilocks usage and then making sure there is a VPA object for every deployment in that namespace. All VPA objects are set in recommendation mode only. 40 | 41 | ### CLI 42 | 43 | The cli has three main features: 44 | 45 | 1. It can create VPA objects in a namespace for each deployment in that namespace. 46 | 1. It can delete all the VPA objects that it created in a namespace. 47 | 1. It can provide a JSON summary of all the existing resource limits/requests, all the VPA recommendations, and a list of the namespaces in which there are VPAs owned by goldilocks. This summary package is reused by the dashboard. In the future, this can be used to send the data somewhere else if desired. 48 | 49 | ### Dashboard 50 | 51 | The dashboard is an HTML/CSS/JS webpage that utilizes go templating to present the data generated by the summary. 52 | 53 | ### Summary Package 54 | 55 | This package looks for all VPA objects in the cluster that are owned by goldilocks and summarizes them, along with adding information about the existing deployment such as resource requests and limits. 56 | 57 | ## Related Work 58 | 59 | There are other resource-tuning efforts out there. None of them are as simple, free, or easy to use. 60 | 61 | * [Kubecost](https://kubecost.com/) - Requires prometheus and a bunch of other stuff (vpa has optional prometheus). Not open source entirely. 62 | * [Spotinst](https://spotinst.com/) - Resource tuning is not their main focus. Also not free. 63 | 64 | ## Possible objections 65 | 66 | Vertical pod autoscaler recommendations are not perfect. This could lead to some uncertainty as to the validity of the recommendations. 67 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM alpine:3.21 2 | 3 | LABEL org.opencontainers.image.authors="FairwindsOps, Inc." \ 4 | org.opencontainers.image.vendor="FairwindsOps, Inc." \ 5 | org.opencontainers.image.title="goldilocks" \ 6 | org.opencontainers.image.description="Goldilocks is a utility that can help you identify a starting point for resource requests and limits." \ 7 | org.opencontainers.image.documentation="https://goldilocks.docs.fairwinds.com/" \ 8 | org.opencontainers.image.source="https://github.com/FairwindsOps/goldilocks" \ 9 | org.opencontainers.image.url="https://github.com/FairwindsOps/goldilocks" \ 10 | org.opencontainers.image.licenses="Apache License 2.0" 11 | # 'nobody' user in alpine 12 | USER 65534 13 | COPY goldilocks / 14 | CMD ["/goldilocks"] 15 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | BINARY_NAME=goldilocks 7 | COMMIT := $(shell git rev-parse HEAD) 8 | VERSION := "dev" 9 | 10 | all: test build 11 | build: 12 | $(GOBUILD) -o $(BINARY_NAME) -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -s -w" -v 13 | lint: 14 | golangci-lint run 15 | test: 16 | printf "\n\nTests:\n\n" 17 | $(GOCMD) test -v --bench --benchmem -coverprofile coverage.txt -covermode=atomic ./... 18 | $(GOCMD) vet ./... 2> govet-report.out 19 | $(GOCMD) tool cover -html=coverage.txt -o cover-report.html 20 | printf "\nCoverage report available at cover-report.html\n\n" 21 | tidy: 22 | $(GOCMD) mod tidy 23 | clean: 24 | $(GOCLEAN) 25 | $(GOCMD) fmt ./... 26 | rm -f $(BINARY_NAME) 27 | packr2 clean 28 | rm -rf e2e/results/* 29 | rm *-report* 30 | rm coverage.txt 31 | # Cross compilation 32 | build-linux: 33 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_NAME) -ldflags "-X main.VERSION=$(VERSION)" -v 34 | build-docker: build-linux 35 | docker build -t goldilocks:dev . 36 | e2e-test: 37 | venom run e2e/tests/* --output-dir e2e/results --log info --strict 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Goldilocks 3 |
4 |

Get your resource requests "Just Right"

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 |
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 | Goldilocks Screenshot 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 | Love Fairwinds Open Source? Automate Fairwinds Open Source for free with Fairwinds Insights. Click to learn more 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 | Fairwinds Insights 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 | 4 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 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 | 16 | 17 | 28 | 29 | 46 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 |
2 | Goldilocks 3 |
4 |

Get your resource requests "Just Right"

5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
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 | Goldilocks Screenshot 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 | 6 | 7 | 28 | 30 | 31 | 38 | 39 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 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 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /pkg/dashboard/assets/images/caret-right.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 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 | 6 | 7 | 28 | 30 | 31 | 38 | 39 | 41 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /pkg/dashboard/assets/images/icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 10 | 12 | 17 | 18 | 19 | 20 | 21 | 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 |
35 |
36 | 37 | {{- if opts.EnableCost }} 38 | {{ template "email" . }} 39 | {{ template "api_token" . }} 40 | {{ template "cost_settings" . }} 41 | {{- end }} 42 | 43 |

Namespace Details

44 | 45 | {{ if gt (len .Data.VpaData.Namespaces) 1 }} 46 | {{ template "filter" .Data.VpaData.Namespaces }} 47 | {{ end }} 48 | 49 |
56 | {{ range $nsName, $nsSummary := .Data.VpaData.Namespaces }} 57 | {{ template "namespace" $nsSummary }} 58 | {{end}} 59 |
60 | 61 | 154 |
155 | 156 |
157 | {{ template "footer" . }} 158 |
159 |
160 | 161 | 162 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/email.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "email" }} 2 | 24 | {{ end }} 25 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/filter.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "filter" }} 2 | 5 | 6 | 42 | {{ end }} 43 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/footer.gohtml: -------------------------------------------------------------------------------- 1 | {{define "footer"}} 2 | 6 | 20 | 25 | 30 | {{end}} 31 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/head.gohtml: -------------------------------------------------------------------------------- 1 | {{ define "head" }} 2 | 3 | 4 | Goldilocks by Fairwinds 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | {{- if opts.EnableCost }} 23 | 24 | 25 | {{- end }} 26 | 27 | {{ end }} 28 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/namespace.gohtml: -------------------------------------------------------------------------------- 1 | {{define "namespace"}} 2 | {{ $foundFirstWorkload := false }} 3 | 4 |
5 |

6 | Namespace 7 | {{ $.Namespace }} 8 |

9 | 10 | {{ if not .IsOnlyNamespace }} 11 | Limit results to the {{ $.Namespace }} namespace 15 | {{ end }} 16 | 17 |
18 | Workloads 19 | 20 | {{ if lt (len $.Workloads) 1 }} 21 |

No workloads found in this namespace.

22 | 23 | {{ else }} 24 | {{ range $workload := $.Workloads }} 25 | 26 |
27 |

28 | {{ $workload.ControllerType }} 29 | {{ $workload.ControllerName }} 30 |

31 | 32 |
36 | Containers 37 | 38 | {{ range $cName, $cSummary := $workload.Containers }} 39 |
40 |

41 | Container 42 | {{ $cName }} 43 |

44 | 45 | {{ if opts.EnableCost }} 46 | {{ if gt $cSummary.ContainerCostInt 0 }} 47 | ${{ $cSummary.ContainerCost }}/hour 48 | {{ end }} 49 | {{ end }} 50 | 51 |
52 | Details 53 | 54 |
55 | {{ template "container" $cSummary }} 56 |
57 |
58 |
59 | {{ end }} 60 |
61 |
62 | {{ end }} 63 | {{ end }} 64 |
65 |
66 | {{end}} 67 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/namespace_list.gohtml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ template "head" .Data }} 5 | 6 | {{ if gt (len .Data.Namespaces ) 1 }} 7 | 8 | {{ end }} 9 | 10 | 11 | 12 |
13 | {{ template "navigation" . }} 14 |
15 | 16 |
17 |
18 |

Namespaces

19 | 20 | {{ if lt (len .Data.Namespaces) 1 }} 21 |

No namespaces are labelled for use by Goldilocks. Try labelling one with kubectl label ns NAMESPACE_NAME goldilocks.fairwinds.com/enabled=true

22 | {{ else }} 23 | {{ if gt (len .Data.Namespaces) 1 }} 24 | {{ template "filter" .Data.Namespaces }} 25 | {{ end }} 26 | 27 | 37 | {{end}} 38 |
39 | 40 |
41 | {{ template "footer" . }} 42 |
43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /pkg/dashboard/templates/navigation.gohtml: -------------------------------------------------------------------------------- 1 | {{define "navigation"}} 2 | 3 | 4 | 5 | 6 |

7 | An Open Source Project by 8 | 13 | 18 | (Opens in new window) 19 | 20 |

21 | 22 | 46 | 47 |

48 | Want more?
49 | Automate Goldilocks for free with
50 | Fairwinds Insights 55 | (Opens in new window) 56 | 57 |

58 | 59 | 60 | {{end}} 61 | -------------------------------------------------------------------------------- /pkg/handler/handler.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 handler 16 | 17 | import ( 18 | "strings" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/klog/v2" 22 | 23 | "github.com/fairwindsops/goldilocks/pkg/utils" 24 | ) 25 | 26 | // OnUpdate is a handler that should be called when an object is updated. 27 | // obj is the Kubernetes object that was updated. 28 | // event is the Event metadata representing the update. 29 | func OnUpdate(obj interface{}, event utils.Event) { 30 | klog.V(10).Infof("Handler got an OnUpdate event of type %s", event.EventType) 31 | if event.EventType == "delete" { 32 | onDelete(event) 33 | return 34 | } 35 | switch t := obj.(type) { 36 | case *corev1.Pod: 37 | OnPodChanged(obj.(*corev1.Pod), event) 38 | case *corev1.Namespace: 39 | OnNamespaceChanged(obj.(*corev1.Namespace), event) 40 | default: 41 | klog.Errorf("Object has unknown type of %T", t) 42 | } 43 | } 44 | 45 | func onDelete(event utils.Event) { 46 | klog.V(8).Info("OnDelete()") 47 | switch strings.ToLower(event.ResourceType) { 48 | case "namespace": 49 | OnNamespaceChanged(&corev1.Namespace{}, event) 50 | case "pod": 51 | OnPodChanged(&corev1.Pod{}, event) 52 | default: 53 | klog.Errorf("object has unknown resource type %s", event.ResourceType) 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /pkg/handler/namespace.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 handler 16 | 17 | import ( 18 | "strings" 19 | 20 | corev1 "k8s.io/api/core/v1" 21 | "k8s.io/klog/v2" 22 | 23 | "github.com/fairwindsops/goldilocks/pkg/utils" 24 | "github.com/fairwindsops/goldilocks/pkg/vpa" 25 | ) 26 | 27 | // OnNamespaceChanged is a handler that should be called when a namespace chanages. 28 | func OnNamespaceChanged(namespace *corev1.Namespace, event utils.Event) { 29 | klog.V(7).Infof("Processing namespace: %s", namespace.ObjectMeta.Name) 30 | 31 | switch strings.ToLower(event.EventType) { 32 | case "delete": 33 | klog.Info("Nothing to do on namespace deletion. The VPAs will be deleted as part of the ns.") 34 | case "create", "update": 35 | klog.Infof("Namespace %s updated. Check the labels.", namespace.ObjectMeta.Name) 36 | err := vpa.GetInstance().ReconcileNamespace(namespace) 37 | if err != nil { 38 | klog.Errorf("Error reconciling: %v", err) 39 | } 40 | default: 41 | klog.Infof("Update type %s is not valid, skipping.", event.EventType) 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /pkg/handler/pod.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 handler 16 | 17 | import ( 18 | "strings" 19 | 20 | "github.com/davecgh/go-spew/spew" 21 | "github.com/fairwindsops/goldilocks/pkg/kube" 22 | "github.com/fairwindsops/goldilocks/pkg/utils" 23 | "github.com/fairwindsops/goldilocks/pkg/vpa" 24 | corev1 "k8s.io/api/core/v1" 25 | "k8s.io/klog/v2" 26 | ) 27 | 28 | func OnPodChanged(pod *corev1.Pod, event utils.Event) { 29 | kubeClient := kube.GetInstance() 30 | namespace, err := kube.GetNamespace(kubeClient, event.Namespace) 31 | if err != nil { 32 | klog.Error("handler got error retrieving namespace object. breaking.") 33 | klog.V(5).Info("dumping out event struct") 34 | klog.V(5).Info(spew.Sdump(event)) 35 | return 36 | } 37 | switch strings.ToLower(event.EventType) { 38 | case "delete": 39 | klog.V(3).Infof("Pod %s deleted. Deleting the VPA for it if it had one.", pod.ObjectMeta.Name) 40 | err := vpa.GetInstance().ReconcileNamespace(namespace) 41 | if err != nil { 42 | klog.Errorf("Error reconciling: %v", err) 43 | } 44 | case "create", "update": 45 | klog.V(3).Infof("Pod %s updated. Reconcile", pod.ObjectMeta.Name) 46 | err := vpa.GetInstance().ReconcileNamespace(namespace) 47 | if err != nil { 48 | klog.Errorf("Error reconciling: %v", err) 49 | } 50 | default: 51 | klog.V(3).Infof("Update type %s is not valid, skipping.", event.EventType) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /pkg/kube/client_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 kube 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/stretchr/testify/assert" 22 | 23 | corev1 "k8s.io/api/core/v1" 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | ) 26 | 27 | func TestGetNamespace(t *testing.T) { 28 | kubeClient := GetMockClient() 29 | 30 | namespace := &corev1.Namespace{ 31 | ObjectMeta: metav1.ObjectMeta{ 32 | Name: "test", 33 | }, 34 | } 35 | 36 | _, errNotFound := GetNamespace(kubeClient, "nothere") 37 | assert.EqualError(t, errNotFound, "namespaces \"nothere\" not found") 38 | 39 | _, err := kubeClient.Client.CoreV1().Namespaces().Create(context.TODO(), namespace, metav1.CreateOptions{}) 40 | assert.NoError(t, err) 41 | 42 | got, err := GetNamespace(kubeClient, "test") 43 | assert.NoError(t, err) 44 | assert.EqualValues(t, got, namespace) 45 | } 46 | -------------------------------------------------------------------------------- /pkg/kube/test_helpers.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/fairwindsops/controller-utils/pkg/controller" 7 | "k8s.io/apimachinery/pkg/api/meta" 8 | "k8s.io/apimachinery/pkg/runtime" 9 | "k8s.io/apimachinery/pkg/runtime/schema" 10 | v1beta2fake "k8s.io/autoscaler/vertical-pod-autoscaler/pkg/client/clientset/versioned/fake" 11 | fakedyn "k8s.io/client-go/dynamic/fake" 12 | "k8s.io/client-go/kubernetes/fake" 13 | ) 14 | 15 | // GetMockClient returns a fake client instance for mocking 16 | func GetMockClient() *ClientInstance { 17 | kc := ClientInstance{ 18 | Client: fake.NewSimpleClientset(), 19 | } 20 | SetInstance(kc) 21 | return &kc 22 | } 23 | 24 | // GetMockVPAClient returns fake vpa client instance for mocking. 25 | func GetMockVPAClient() *VPAClientInstance { 26 | kc := VPAClientInstance{ 27 | Client: v1beta2fake.NewSimpleClientset(), 28 | } 29 | SetVPAInstance(kc) 30 | return &kc 31 | } 32 | 33 | // GetMockControllerUtilsClient returns a fake controller client instance for mocking. 34 | func GetMockControllerUtilsClient(dynamicClient *DynamicClientInstance) *ControllerUtilsClientInstance { 35 | kc := ControllerUtilsClientInstance{ 36 | Client: controller.Client{ 37 | Context: context.TODO(), 38 | RESTMapper: dynamicClient.RESTMapper, 39 | Dynamic: dynamicClient.Client, 40 | }, 41 | } 42 | SetControllerUtilsInstance(kc) 43 | return &kc 44 | } 45 | 46 | // GetMockVPAClient returns fake vpa client instance for mocking. 47 | func GetMockDynamicClient() *DynamicClientInstance { 48 | gvapps := schema.GroupVersion{Group: "apps", Version: "v1"} 49 | gvcore := schema.GroupVersion{Group: "", Version: "v1"} 50 | gvbatch := schema.GroupVersion{Group: "batch", Version: "v1"} 51 | restMapper := meta.NewDefaultRESTMapper([]schema.GroupVersion{gvapps, gvcore, gvbatch}) 52 | gvk := gvapps.WithKind("Deployment") 53 | restMapper.Add(gvk, meta.RESTScopeNamespace) 54 | gvk = gvapps.WithKind("DaemonSet") 55 | restMapper.Add(gvk, meta.RESTScopeNamespace) 56 | gvk = gvapps.WithKind("StatefulSet") 57 | restMapper.Add(gvk, meta.RESTScopeNamespace) 58 | gvk = gvapps.WithKind("ReplicaSet") 59 | restMapper.Add(gvk, meta.RESTScopeNamespace) 60 | gvk = gvcore.WithKind("Pod") 61 | restMapper.Add(gvk, meta.RESTScopeNamespace) 62 | gvk = gvcore.WithKind("Namespace") 63 | restMapper.Add(gvk, meta.RESTScopeRoot) 64 | gvk = gvbatch.WithKind("CronJob") 65 | restMapper.Add(gvk, meta.RESTScopeNamespace) 66 | gvk = gvbatch.WithKind("Job") 67 | restMapper.Add(gvk, meta.RESTScopeNamespace) 68 | gvrToListKind := map[schema.GroupVersionResource]string{ 69 | { 70 | Group: "", 71 | Version: "v1", 72 | Resource: "pods", 73 | }: "PodList", 74 | { 75 | Group: "", 76 | Version: "v1", 77 | Resource: "namespaces", 78 | }: "NamespaceList", 79 | { 80 | Group: "apps", 81 | Version: "v1", 82 | Resource: "replicasets", 83 | }: "ReplicaSetList", 84 | { 85 | Group: "apps", 86 | Version: "v1", 87 | Resource: "deployments", 88 | }: "DeploymentList", 89 | { 90 | Group: "apps", 91 | Version: "v1", 92 | Resource: "daemonsets", 93 | }: "DaemonSetList", 94 | { 95 | Group: "apps", 96 | Version: "v1", 97 | Resource: "statefulsets", 98 | }: "StatefulSetList", 99 | { 100 | Group: "batch", 101 | Version: "v1", 102 | Resource: "cronjobs", 103 | }: "CronJobList", 104 | { 105 | Group: "batch", 106 | Version: "v1", 107 | Resource: "jobs", 108 | }: "JobList", 109 | } 110 | fc := fakedyn.NewSimpleDynamicClientWithCustomListKinds(runtime.NewScheme(), gvrToListKind) 111 | kc := DynamicClientInstance{ 112 | Client: fc, 113 | RESTMapper: restMapper, 114 | } 115 | SetDynamicInstance(kc) 116 | return &kc 117 | } 118 | 119 | // SetInstance allows the user to set the kubeClient singleton 120 | func SetInstance(kc ClientInstance) { 121 | kubeClient = &kc 122 | } 123 | 124 | // SetVPAInstance sets the kubeClient for VPA 125 | func SetVPAInstance(kc VPAClientInstance) { 126 | kubeClientVPA = &kc 127 | } 128 | 129 | // SetVPAInstance sets the kubeClient for VPA 130 | func SetDynamicInstance(kc DynamicClientInstance) { 131 | dynamicClient = &kc 132 | } 133 | 134 | // SetControllerUtilsInstance sets a kubeClient for Controller 135 | func SetControllerUtilsInstance(kc ControllerUtilsClientInstance) { 136 | controllerUtilsClient = &kc 137 | } 138 | -------------------------------------------------------------------------------- /pkg/summary/options.go: -------------------------------------------------------------------------------- 1 | package summary 2 | 3 | import ( 4 | "github.com/fairwindsops/goldilocks/pkg/kube" 5 | "github.com/fairwindsops/goldilocks/pkg/utils" 6 | "k8s.io/apimachinery/pkg/util/sets" 7 | ) 8 | 9 | type Option func(*options) 10 | 11 | // options for getting and caching the Summarizer's VPAs 12 | type options struct { 13 | kubeClient *kube.ClientInstance 14 | vpaClient *kube.VPAClientInstance 15 | dynamicClient *kube.DynamicClientInstance 16 | controllerUtilsClient *kube.ControllerUtilsClientInstance 17 | namespace string 18 | vpaLabels map[string]string 19 | excludedContainers sets.Set[string] 20 | } 21 | 22 | // defaultOptions for a Summarizer 23 | func defaultOptions() *options { 24 | return &options{ 25 | kubeClient: kube.GetInstance(), 26 | vpaClient: kube.GetVPAInstance(), 27 | dynamicClient: kube.GetDynamicInstance(), 28 | controllerUtilsClient: kube.GetControllerUtilsInstance(), 29 | namespace: namespaceAllNamespaces, 30 | vpaLabels: utils.VPALabels, 31 | excludedContainers: sets.Set[string]{}, 32 | } 33 | } 34 | 35 | // ForNamespace is an Option for limiting the summary to a single namespace 36 | func ForNamespace(namespace string) Option { 37 | return func(opts *options) { 38 | opts.namespace = namespace 39 | } 40 | } 41 | 42 | // ExcludeContainers is an Option for excluding containers in the summary 43 | func ExcludeContainers(excludedContainers sets.Set[string]) Option { 44 | return func(opts *options) { 45 | opts.excludedContainers = excludedContainers 46 | } 47 | } 48 | 49 | // ForVPAsWithLabels is an Option for limiting the summary to certain VPAs matching the labels 50 | func ForVPAsWithLabels(vpaLabels map[string]string) Option { 51 | return func(opts *options) { 52 | opts.vpaLabels = vpaLabels 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /pkg/summary/summary_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 summary 16 | 17 | import ( 18 | "context" 19 | "testing" 20 | 21 | "github.com/fairwindsops/goldilocks/pkg/kube" 22 | "github.com/stretchr/testify/assert" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/runtime/schema" 25 | ) 26 | 27 | func Test_Summarizer(t *testing.T) { 28 | kubeClientVPA := kube.GetMockVPAClient() 29 | kubeClient := kube.GetMockClient() 30 | dynamicClient := kube.GetMockDynamicClient() 31 | controllerUtilsClient := kube.GetMockControllerUtilsClient(dynamicClient) 32 | 33 | summarizer := NewSummarizer() 34 | summarizer.kubeClient = kubeClient 35 | summarizer.vpaClient = kubeClientVPA 36 | summarizer.dynamicClient = dynamicClient 37 | summarizer.controllerUtilsClient = controllerUtilsClient 38 | 39 | // _, _ = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}).Create(context.TODO(), nsLabeledTrueUnstructured, metav1.CreateOptions{}) 40 | _, err := dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("testing").Create(context.TODO(), testDeploymentBasicUnstructured, metav1.CreateOptions{}) 41 | assert.NoError(t, err) 42 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}).Namespace("testing").Create(context.TODO(), testDeploymentBasicReplicaSetUnstructured, metav1.CreateOptions{}) 43 | assert.NoError(t, err) 44 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}).Namespace("testing").Create(context.TODO(), testDeploymentBasicPodUnstructured, metav1.CreateOptions{}) 45 | assert.NoError(t, err) 46 | _, errOk := kubeClientVPA.Client.AutoscalingV1().VerticalPodAutoscalers("testing").Create(context.TODO(), testVPABasic, metav1.CreateOptions{}) 47 | assert.NoError(t, errOk) 48 | 49 | _, errOk2 := kubeClientVPA.Client.AutoscalingV1().VerticalPodAutoscalers("testing").Create(context.TODO(), testVPANoLabels, metav1.CreateOptions{}) 50 | assert.NoError(t, errOk2) 51 | 52 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "deployments"}).Namespace("testing").Create(context.TODO(), testDeploymentWithRecoUnstructured, metav1.CreateOptions{}) 53 | assert.NoError(t, err) 54 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "replicasets"}).Namespace("testing").Create(context.TODO(), testDeploymentWithRecoReplicaSetUnstructured, metav1.CreateOptions{}) 55 | assert.NoError(t, err) 56 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}).Namespace("testing").Create(context.TODO(), testDeploymentWithRecoPodUnstructured, metav1.CreateOptions{}) 57 | assert.NoError(t, err) 58 | _, errOk3 := kubeClientVPA.Client.AutoscalingV1().VerticalPodAutoscalers("testing").Create(context.TODO(), testVPAWithReco, metav1.CreateOptions{}) 59 | assert.NoError(t, errOk3) 60 | 61 | got, err := summarizer.GetSummary() 62 | assert.NoError(t, err) 63 | 64 | assert.EqualValues(t, testSummary, got) 65 | } 66 | 67 | func Test_Summarizer_Daemonset(t *testing.T) { 68 | kubeClientVPA := kube.GetMockVPAClient() 69 | kubeClient := kube.GetMockClient() 70 | dynamicClient := kube.GetMockDynamicClient() 71 | controllerUtilsClient := kube.GetMockControllerUtilsClient(dynamicClient) 72 | 73 | summarizer := NewSummarizer() 74 | summarizer.kubeClient = kubeClient 75 | summarizer.vpaClient = kubeClientVPA 76 | summarizer.dynamicClient = dynamicClient 77 | summarizer.controllerUtilsClient = controllerUtilsClient 78 | 79 | // _, _ = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "namespaces"}).Create(context.TODO(), nsLabeledTrueUnstructured, metav1.CreateOptions{}) 80 | _, err := dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "apps", Version: "v1", Resource: "daemonsets"}).Namespace("testing-daemonset").Create(context.TODO(), testDaemonSettWithRecoUnstructured, metav1.CreateOptions{}) 81 | assert.NoError(t, err) 82 | _, err = dynamicClient.Client.Resource(schema.GroupVersionResource{Group: "", Version: "v1", Resource: "pods"}).Namespace("testing-daemonset").Create(context.TODO(), testDaemonSetWithRecoPodUnstructured, metav1.CreateOptions{}) 83 | assert.NoError(t, err) 84 | _, errOk := kubeClientVPA.Client.AutoscalingV1().VerticalPodAutoscalers("testing-daemonset").Create(context.TODO(), testDaemonSetVPAWithReco, metav1.CreateOptions{}) 85 | assert.NoError(t, errOk) 86 | 87 | got, err := summarizer.GetSummary() 88 | assert.NoError(t, err) 89 | 90 | assert.EqualValues(t, testSummaryDaemonSet, got) 91 | } 92 | -------------------------------------------------------------------------------- /pkg/utils/utils.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 utils 16 | 17 | import ( 18 | v1 "k8s.io/api/core/v1" 19 | "k8s.io/apimachinery/pkg/api/resource" 20 | ) 21 | 22 | var ( 23 | // LabelBase is the string that will be used for labels on namespaces 24 | LabelOrAnnotationBase = "goldilocks.fairwinds.com" 25 | // VpaEnabledLabel is the label used to indicate that Goldilocks is enabled. 26 | VpaEnabledLabel = LabelOrAnnotationBase + "/" + "enabled" 27 | // VpaUpdateModeKey is the label used to indicate the vpa update mode. 28 | VpaUpdateModeKey = LabelOrAnnotationBase + "/" + "vpa-update-mode" 29 | // VpaMinReplicas is the annotation to use to define minimum replicas for eviction of a VPA 30 | VpaMinReplicasAnnotation = LabelOrAnnotationBase + "/" + "vpa-min-replicas" 31 | // DeploymentExcludeContainersAnnotation is the label used to exclude container names from being reported. 32 | WorkloadExcludeContainersAnnotation = LabelOrAnnotationBase + "/" + "exclude-containers" 33 | // VpaResourcePolicyAnnotation is the annotation use to define the json configuration of PodResourcePolicy section of a vpa 34 | VpaResourcePolicyAnnotation = LabelOrAnnotationBase + "/" + "vpa-resource-policy" 35 | ) 36 | 37 | // VPALabels is a set of default labels that get placed on every VPA. 38 | var VPALabels = map[string]string{ 39 | "creator": "Fairwinds", 40 | "source": "goldilocks", 41 | } 42 | 43 | // An Event represents an update of a Kubernetes object and contains metadata about the update. 44 | type Event struct { 45 | Key string // A key identifying the object. This is in the format / 46 | EventType string // The type of event - update, delete, or create 47 | Namespace string // The namespace of the event's object 48 | ResourceType string // The type of resource that was updated. 49 | } 50 | 51 | // UniqueString returns a unique string from a slice. 52 | func UniqueString(stringSlice []string) []string { 53 | keys := make(map[string]bool) 54 | list := []string{} 55 | for _, entry := range stringSlice { 56 | if _, value := keys[entry]; !value { 57 | keys[entry] = true 58 | list = append(list, entry) 59 | } 60 | } 61 | return list 62 | } 63 | 64 | // Difference returns the difference betwee two string slices. 65 | func Difference(a, b []string) (diff []string) { 66 | m := make(map[string]bool) 67 | 68 | for _, item := range b { 69 | m[item] = true 70 | } 71 | 72 | for _, item := range a { 73 | if _, ok := m[item]; !ok { 74 | diff = append(diff, item) 75 | } 76 | } 77 | return 78 | } 79 | 80 | // FormatResourceList scales the units of a ResourceList so that they are 81 | // human readable 82 | func FormatResourceList(rl v1.ResourceList) v1.ResourceList { 83 | memoryScales := []resource.Scale{ 84 | resource.Kilo, 85 | resource.Mega, 86 | resource.Giga, 87 | resource.Tera, 88 | } 89 | if mem, exists := rl[v1.ResourceMemory]; exists { 90 | i := 0 91 | maxAllowableStringLen := 5 92 | for len(mem.String()) > maxAllowableStringLen && i < len(memoryScales)-1 { 93 | mem.RoundUp(memoryScales[i]) 94 | i++ 95 | } 96 | rl[v1.ResourceMemory] = mem 97 | } 98 | return rl 99 | } 100 | -------------------------------------------------------------------------------- /pkg/utils/utils_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 utils 16 | 17 | import ( 18 | "testing" 19 | 20 | "github.com/stretchr/testify/assert" 21 | "k8s.io/api/core/v1" 22 | "k8s.io/apimachinery/pkg/api/resource" 23 | ) 24 | 25 | func TestUniqueString(t *testing.T) { 26 | for _, tc := range testUniqueStringCases { 27 | res := UniqueString(tc.testData) 28 | assert.Equal(t, res, tc.expected) 29 | } 30 | } 31 | 32 | var testUniqueStringCases = []struct { 33 | description string 34 | testData []string 35 | expected []string 36 | }{ 37 | { 38 | description: "three duplicates, one output", 39 | testData: []string{"test", "test", "test"}, 40 | expected: []string{"test"}, 41 | }, 42 | { 43 | description: "no duplicates", 44 | testData: []string{"one", "two", "three"}, 45 | expected: []string{"one", "two", "three"}, 46 | }, 47 | } 48 | 49 | func TestDifference(t *testing.T) { 50 | for _, tc := range testDifferenceCases { 51 | res := Difference(tc.testData1, tc.testData2) 52 | assert.Equal(t, res, tc.expected) 53 | } 54 | } 55 | 56 | var empty []string 57 | 58 | var testDifferenceCases = []struct { 59 | description string 60 | testData1 []string 61 | testData2 []string 62 | expected []string 63 | }{ 64 | { 65 | description: "empty case", 66 | testData1: []string{"a", "b", "c"}, 67 | testData2: []string{"a", "b", "c"}, 68 | expected: empty, 69 | }, 70 | { 71 | description: "extra item on right", 72 | testData1: []string{"a", "b"}, 73 | testData2: []string{"a", "b", "c"}, 74 | expected: empty, 75 | }, 76 | { 77 | description: "extra item on left", 78 | testData1: []string{"a", "b", "c"}, 79 | testData2: []string{"a", "b"}, 80 | expected: []string{"c"}, 81 | }, 82 | } 83 | 84 | func TestFormatResourceList(t *testing.T) { 85 | for _, tc := range testFormatResourceCases { 86 | res := FormatResourceList(tc.testData) 87 | resource := res[tc.resourceType] 88 | got := resource.String() 89 | assert.Equal(t, tc.expected, got) 90 | } 91 | } 92 | 93 | var testFormatResourceCases = []struct { 94 | description string 95 | testData v1.ResourceList 96 | resourceType v1.ResourceName 97 | expected string 98 | }{ 99 | { 100 | description: "Unmodified cpu", 101 | testData: v1.ResourceList{ 102 | "cpu": resource.MustParse("1"), 103 | }, 104 | resourceType: "cpu", 105 | expected: "1", 106 | }, 107 | { 108 | description: "Unmodified memory", 109 | testData: v1.ResourceList{ 110 | "memory": resource.MustParse("1Mi"), 111 | }, 112 | resourceType: "memory", 113 | expected: "1Mi", 114 | }, 115 | { 116 | description: "Memory in too large of units", 117 | testData: v1.ResourceList{ 118 | "memory": resource.MustParse("123456k"), 119 | }, 120 | resourceType: "memory", 121 | expected: "124M", 122 | }, 123 | } 124 | --------------------------------------------------------------------------------