├── .circleci └── config.yml ├── .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 ├── cmd ├── root.go ├── root_test.go ├── testdata │ ├── course.yaml │ └── coursev2.yaml ├── update.go ├── validation.go ├── validation_test.go └── version.go ├── docs ├── .vuepress │ ├── config-extras.js │ ├── config.js │ ├── public │ │ ├── favicon.png │ │ ├── img │ │ │ ├── fairwinds-logo.svg │ │ │ ├── reckoner-icon.svg │ │ │ └── reckoner-logo.svg │ │ └── scripts │ │ │ ├── marketing.js │ │ │ └── modify.js │ ├── styles │ │ ├── index.styl │ │ └── palette.styl │ └── theme │ │ ├── index.js │ │ └── layouts │ │ └── Layout.vue ├── README.md ├── contributing │ ├── code-of-conduct.md │ └── guide.md ├── main-metadata.md ├── package-lock.json ├── package.json └── usage.md ├── end_to_end_testing ├── course_files │ ├── 01_test_basic.yaml │ ├── 02_test_create_namespace.yaml │ ├── 03_test_env_var.yaml │ ├── 04_test_failed_chart.yaml │ ├── 05_test_exit_on_post_install_hook.yaml │ ├── 06_test_exit_on_pre_install_hook.yaml │ ├── 07_test_good_hooks.yaml │ ├── 08_test_multi_chart.yaml │ ├── 09_test_multi_chart.yaml │ ├── 10_test_git_chart.yaml │ ├── 11_test_after_first_failure.yaml │ ├── 12_test_after_first_failure.yaml │ ├── 13_test_bad_schema_repository.yaml │ ├── 13_test_lint_bad_secret.yaml │ ├── 13_test_lint_good_secret.yaml │ ├── 13_test_required_schema.yaml │ ├── 15_test_default_namespace_annotation_and_labels.yaml │ ├── 16_test_overwrite_namespace_annotation_and_labels.yaml │ ├── 17_test_dont_overwrite_ns_meta.yaml │ ├── 18_strong_typing.yaml │ ├── 20_test_strong_ordering.yaml │ ├── 21_test_diff.yaml │ ├── 23_test_environment_variable_namespace_on_chart.yaml │ ├── 24_test_no_context.yaml │ ├── 25_test_import.yaml │ ├── 26_test_shell_executor_secret.yaml │ ├── 27_gitops.yaml │ ├── test_does_not_overwrite_namespace_annotation_and_labels.yaml │ └── testing_in_folder │ │ ├── subfolder │ │ ├── relative_hook_namespace.yaml │ │ └── yaml_values.yaml │ │ ├── test_files_in_folders.yaml │ │ └── testing_in_subfolder │ │ └── 22_testing_relative_hook_location.yaml ├── pre_go.sh ├── run_go.sh └── tests │ ├── 00_setup.yaml │ ├── 01_basic.yaml │ ├── 02_namespace_creation.yaml │ ├── 03_test_env_var.yaml │ ├── 04_bad_chart.yaml │ ├── 05_exit_post_install.yaml │ ├── 06_exit_pre_install.yaml │ ├── 07_good_hooks.yaml │ ├── 08_multi_chart.yaml │ ├── 09_one_of_multi_chart.yaml │ ├── 10_git_charts.yaml │ ├── 11_stop_after_first_failure.yaml │ ├── 12_continue_after_first_failure.yaml │ ├── 13_schema.yaml │ ├── 14_folders.yaml │ ├── 15_namespace_management.yaml │ ├── 16_namespace_management_overwrite.yaml │ ├── 17_namespace_managment_dont_overwrite.yaml │ ├── 18_strong_typing.yaml │ ├── 20_strong_ordering.yaml │ ├── 21_diff.yaml │ ├── 22_test_relative_hooks.yaml │ ├── 23_chart_namespace_environment_variable.yaml │ ├── 24_no_context.yaml │ ├── 25_import.yaml │ ├── 26_shell_executor_secret.yaml │ └── 27_gitops.yaml ├── go.mod ├── go.sum ├── img ├── reckoner-icon.svg └── reckoner-logo.svg ├── main.go └── pkg ├── course ├── argocd.go ├── course.go ├── course_test.go ├── coursev2.schema.json ├── namespace.go ├── namespace_test.go ├── shellSecrets.go ├── shellSecrets_test.go └── testdata │ ├── convert1.yaml │ ├── unmarshalerror.yaml │ ├── v2_env.yaml │ └── v2_namespace.yaml ├── helm └── helm.go ├── reckoner ├── argocd.go ├── argocd_test.go ├── client.go ├── diff.go ├── diff_test.go ├── git.go ├── hook.go ├── import.go ├── import_test.go ├── manifest_test.go ├── manifests.go ├── namespace.go ├── namespace_test.go ├── plot.go ├── plot_test.go ├── split.go ├── split_test.go └── update.go └── secrets └── secrets.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | #Copyright 2017 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 | version: 2.1 16 | 17 | orbs: 18 | rok8s: fairwinds/rok8s-scripts@12 19 | oss-docs: fairwinds/oss-docs@0 20 | 21 | references: 22 | e2e_configuration: &e2e_configuration 23 | attach-workspace: true 24 | workspace-location: / 25 | pre_script: end_to_end_testing/pre_go.sh 26 | script: end_to_end_testing/run_go.sh 27 | command_runner_image: quay.io/reactiveops/ci-images:v12-buster 28 | enable_docker_layer_caching: false 29 | store-test-results: /tmp/test-results/ 30 | requires: 31 | - test 32 | - snapshot 33 | filters: 34 | branches: 35 | only: /.*/ 36 | tags: 37 | ignore: /.*/ 38 | install_vault_alpine: &install_vault_alpine 39 | run: 40 | name: install hashicorp vault 41 | command: | 42 | apk --update add curl yq 43 | cd /tmp 44 | curl -LO https://releases.hashicorp.com/vault/1.14.2/vault_1.14.2_linux_amd64.zip 45 | unzip vault_1.14.2_linux_amd64.zip 46 | mv vault /usr/bin/vault 47 | jobs: 48 | test: 49 | working_directory: /home/circleci/go/src/github.com/fairwindsops/reckoner 50 | docker: 51 | - image: cimg/go:1.20 52 | steps: 53 | - checkout 54 | - run: make test 55 | snapshot: 56 | working_directory: /go/src/github.com/fairwindsops/reckoner 57 | docker: 58 | - image: goreleaser/goreleaser:v1.20.0 59 | steps: 60 | - checkout 61 | - setup_remote_docker 62 | - run: 63 | name: Goreleaser Snapshot 64 | command: goreleaser --snapshot --skip-sign -p 1 65 | - store_artifacts: 66 | path: dist 67 | destination: snapshot 68 | - persist_to_workspace: 69 | root: /go/src/github.com/fairwindsops/reckoner 70 | paths: 71 | - dist 72 | release: 73 | working_directory: /home/circleci/go/src/github.com/fairwindsops/reckoner 74 | resource_class: large 75 | shell: /bin/bash 76 | docker: 77 | - image: goreleaser/goreleaser:v1.20.0 78 | steps: 79 | - checkout 80 | - setup_remote_docker 81 | - *install_vault_alpine 82 | - rok8s/get_vault_env: 83 | vault_path: repo/global/env 84 | - rok8s/get_vault_env: 85 | vault_path: repo/reckoner/env 86 | - run: 87 | name: docker login 88 | command: | 89 | docker login -u _json_key -p "$(echo $GCP_ARTIFACTREADWRITE_JSON_KEY | base64 -d)" us-docker.pkg.dev 90 | - run: echo 'export GORELEASER_CURRENT_TAG="${CIRCLE_TAG}"' >> $BASH_ENV 91 | - run: goreleaser -p 1 92 | workflows: 93 | version: 2 94 | build_and_test: 95 | jobs: 96 | - test: 97 | filters: 98 | tags: 99 | ignore: /.*/ 100 | branches: 101 | only: /.*/ 102 | - snapshot: 103 | requires: 104 | - test 105 | filters: 106 | tags: 107 | ignore: /.*/ 108 | branches: 109 | only: /.*/ 110 | - rok8s/kubernetes_e2e_tests: 111 | name: "End-To-End Kubernetes 1.25" 112 | kind_node_image: "kindest/node:v1.25.3@sha256:f52781bc0d7a19fb6c405c2af83abfeb311f130707a0e219175677e366cc45d1" 113 | <<: *e2e_configuration 114 | - rok8s/kubernetes_e2e_tests: 115 | name: "End-To-End Kubernetes 1.26" 116 | kind_node_image: "kindest/node:v1.26.6@sha256:6e2d8b28a5b601defe327b98bd1c2d1930b49e5d8c512e1895099e4504007adb" 117 | <<: *e2e_configuration 118 | - rok8s/kubernetes_e2e_tests: 119 | name: "End-To-End Kubernetes 1.27" 120 | kind_node_image: "kindest/node:v1.27.3@sha256:3966ac761ae0136263ffdb6cfd4db23ef8a83cba8a463690e98317add2c9ba72" 121 | <<: *e2e_configuration 122 | release: 123 | jobs: 124 | - oss-docs/publish-docs: 125 | repository: reckoner 126 | filters: 127 | branches: 128 | ignore: /.*/ 129 | tags: 130 | only: /^v[0-9]+\.[0-9]+\.[0-9]+$/ 131 | - release: 132 | filters: 133 | branches: 134 | ignore: /.*/ 135 | tags: 136 | only: /^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$/ 137 | -------------------------------------------------------------------------------- /.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 | htmlcov/ 2 | reckoner/version.txt 3 | installer/reckoner.spec 4 | node_modules 5 | coverage.txt 6 | cover-report.html 7 | govet-report.out 8 | 9 | /bin 10 | /build 11 | /dist 12 | /reckoner 13 | docs/README.md 14 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | builds: 2 | - ldflags: 3 | - -X main.version={{.Version}} -X main.commit={{.Commit}} -s -w 4 | goarch: 5 | - amd64 6 | - arm 7 | - arm64 8 | env: 9 | - CGO_ENABLED=0 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | goarm: 15 | - 6 16 | - 7 17 | checksum: 18 | name_template: "checksums.txt" 19 | release: 20 | prerelease: auto 21 | footer: | 22 | You can verify the signatures of both the checksums.txt file and the published docker images using [cosign](https://github.com/sigstore/cosign). 23 | 24 | ``` 25 | cosign verify-blob checksums.txt --signature=checksums.txt.sig --key https://artifacts.fairwinds.com/cosign.pub 26 | ``` 27 | 28 | ``` 29 | cosign verify us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v6 --key https://artifacts.fairwinds.com/cosign.pub 30 | ``` 31 | signs: 32 | - cmd: cosign 33 | args: ["sign-blob", "--key=hashivault://cosign", "-output-signature=${signature}", "${artifact}"] 34 | artifacts: checksum 35 | 36 | docker_signs: 37 | - artifacts: all 38 | args: ["sign", "--key=hashivault://cosign", "${artifact}", "-r"] 39 | 40 | dockers: 41 | - image_templates: 42 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-amd64" 43 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-amd64" 44 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-amd64" 45 | use: buildx 46 | dockerfile: Dockerfile 47 | build_flag_templates: 48 | - "--platform=linux/amd64" 49 | - image_templates: 50 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-arm64v8" 51 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-arm64v8" 52 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-arm64v8" 53 | use: buildx 54 | goarch: arm64 55 | dockerfile: Dockerfile 56 | build_flag_templates: 57 | - "--platform=linux/arm64/v8" 58 | - image_templates: 59 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-armv7" 60 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-armv7" 61 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-armv7" 62 | use: buildx 63 | goarch: arm64 64 | dockerfile: Dockerfile 65 | build_flag_templates: 66 | - "--platform=linux/arm/v7" 67 | docker_manifests: 68 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }} 69 | image_templates: 70 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-amd64" 71 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-arm64v8" 72 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:{{ .Tag }}-armv7" 73 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }} 74 | image_templates: 75 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-amd64" 76 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-arm64v8" 77 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}-armv7" 78 | - name_template: us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }} 79 | image_templates: 80 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-amd64" 81 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-arm64v8" 82 | - "us-docker.pkg.dev/fairwinds-ops/oss/reckoner:v{{ .Major }}.{{ .Minor }}-armv7" 83 | -------------------------------------------------------------------------------- /.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-json 7 | - id: detect-private-key 8 | - id: trailing-whitespace 9 | exclude: > 10 | (?x)^( 11 | docs/.+ 12 | )$ 13 | - id: check-added-large-files 14 | args: ['--maxkb=500'] 15 | - id: check-byte-order-marker 16 | - id: check-merge-conflict 17 | - id: check-symlinks 18 | - id: end-of-file-fixer 19 | exclude: > 20 | (?x)^( 21 | docs/.+ 22 | )$ 23 | - id: check-executables-have-shebangs 24 | - id: flake8 25 | - id: no-commit-to-branch 26 | args: [--branch, master] 27 | - id: pretty-format-json 28 | args: ['--autofix'] 29 | - repo: https://github.com/jumanjihouse/pre-commit-hooks 30 | rev: 2.1.5 31 | hooks: 32 | - id: forbid-binary 33 | exclude: > 34 | (?x)^( 35 | .+\.png| 36 | .+\.woff| 37 | .+\.woff2| 38 | .+\.tff| 39 | .+\.ico 40 | )$ 41 | - id: shellcheck 42 | - repo: https://github.com/dnephin/pre-commit-golang.git 43 | rev: v0.3.5 44 | hooks: 45 | - id: go-fmt 46 | - id: golangci-lint 47 | - id: go-vet 48 | - id: go-unit-tests 49 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | ## DO NOT EDIT - Managed by Terraform 2 | * @sudermanjr @AbdulAhadAkhter 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 | # Reckoner 2 | 3 | ## Intent 4 | Reckoner is intended to provide a declarative syntax to install and manage multiple Helm releases. A primary additional feature is the ability to pull from a git repository instead of a local chart or chart repository. 5 | 6 | ## Key Design Features 7 | 8 | * A stable declarative syntax for Helm releases and their configuration values 9 | * The ability to pull a chart from a git repository 10 | * Pre and Post hooks to provide extra steps not included in the Helm chart installation 11 | 12 | ## Scope 13 | 14 | ### In Scope: 15 | * Interaction with the Helm Client 16 | * Translation of configuration to a helm command 17 | * Staying up to date with Helm features and commands 18 | 19 | 20 | ### Out of Scope: 21 | * Direct interaction with the Tiller api 22 | * Replacing Tiller 23 | * Creation or modification of Helm charts 24 | * Overwriting portions of a chart's output (eg Kustomize or Ship) 25 | * Managing installation versions of Helm and Tiller 26 | 27 | ## Golang Re-Write and Schema Changes 28 | 29 | We will be rewriting this project in Go in order to provide a nicer UX via a pre-compiled binary. At the same time, the course file schema will be changed in order to facilitate this rewrite. 30 | 31 | The re-write will occur in [This Pull Request](https://github.com/FairwindsOps/reckoner/pull/293). Any new branches related to the rewrite should come from here in the short term. 32 | 33 | ### Guiding principles of the re-write 34 | 35 | - No new features will be added that are not directly related to the schema change 36 | - Feature parity should be maintained 37 | - The old end-to-end test suite must run with the new binary with minimal changes (pre-converting the schema may be necessary) 38 | - A conversion path from the old schema to the new will be provided 39 | - Unit Test coverage should be relatively high. I'm thinking >60% 40 | 41 | ### Un-answered Questions 42 | 43 | - Should we automatically detect the old schema and convert it for the end-user? 44 | - Should we instead just error and point them at the conversion utility? 45 | - Do we care about ordering? If so, this may get much uglier. In the past we have maintained strict ordering of the releases. 46 | - Can we import the helm packages instead of relying on exec to the installed helm binary? 47 | 48 | ### New Schema Changes 49 | 50 | The new schema will align better with Go structs, rather than constantly using `map[string]interface`. 51 | 52 | [See the new schema here, along with the conversion function](https://github.com/FairwindsOps/reckoner/blob/golang/pkg/course/course.go) 53 | 54 | Some major changes to the functionality: 55 | 56 | #### Repositories will _only_ be defined in the header 57 | 58 | Each release will reference the name of the repository from the header section. 59 | 60 | This includes git repositories. This should prevent confusion in the future, as well as allow us to not need a dynamic field in the release struct (or in the repositories section of the header) 61 | 62 | #### Charts will be renamed Releases 63 | 64 | This makes a lot more sense. It's what they are. 65 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | 3 | USER nobody 4 | COPY reckoner / 5 | WORKDIR / 6 | ENTRYPOINT ["/reckoner"] 7 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Go parameters 2 | GOCMD=GO111MODULE=on go 3 | GOBUILD=$(GOCMD) build 4 | GOCLEAN=$(GOCMD) clean 5 | GOTEST=$(GOCMD) test 6 | BINARY_NAME=reckoner 7 | COMMIT := $(shell git rev-parse HEAD) 8 | VERSION := "0.0.0" 9 | 10 | all: lint test 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 | reportcard: 16 | goreportcard-cli -t 100 -v 17 | test: 18 | GO111MODULE=on $(GOCMD) test -v --bench --benchmem -coverprofile coverage.txt -covermode=atomic ./... 19 | GO111MODULE=on $(GOCMD) vet ./... 2> govet-report.out 20 | GO111MODULE=on $(GOCMD) tool cover -html=coverage.txt -o cover-report.html 21 | printf "\nCoverage report available at cover-report.html\n\n" 22 | tidy: 23 | $(GOCMD) mod tidy 24 | clean: 25 | $(GOCLEAN) 26 | $(GOCMD) fmt ./... 27 | rm -f $(BINARY_NAME) 28 | # Cross compilation 29 | build-linux: 30 | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 $(GOBUILD) -o $(BINARY_NAME) -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -s -w" -v 31 | build-apple-silicon: 32 | CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 $(GOBUILD) -o $(BINARY_NAME) -ldflags "-X main.version=$(VERSION) -X main.commit=$(COMMIT) -s -w" -v 33 | build-docker: build-linux 34 | docker build -t quay.io/fairwinds/reckoner:go-dev -f Dockerfile . 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | Reckoner 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | Command line helper for helm. 13 | 14 | This utility adds to the functionality of [Helm](https://github.com/kubernetes/helm) in multiple ways: 15 | 16 | * Creates a declarative syntax to manage multiple releases in one place 17 | * Allows installation of charts from a git commit/branch/release 18 | 19 | # Documentation 20 | Check out the [documentation at docs.fairwinds.com](https://reckoner.docs.fairwinds.com/) 21 | 22 | ## Requirements 23 | 24 | * helm (>= 3.0.0), installed and initialized 25 | 26 | > Helm2 is untested as of v4.3.0. The maintainers of helm have [deprecated helm2](https://helm.sh/blog/helm-v2-deprecation-timeline/). 27 | 28 | ## Quickstart 29 | 30 | In course.yml, write: 31 | 32 | ```yaml 33 | namespace: default 34 | charts: 35 | grafana: 36 | namespace: grafana 37 | values: 38 | image: 39 | tag: "6.2.5" 40 | polaris-dashboard: 41 | namespace: polaris-dashboard 42 | repository: 43 | git: https://github.com/FairwindsOps/charts 44 | path: stable 45 | chart: polaris 46 | ``` 47 | 48 | Then run: 49 | 50 | ```shell 51 | reckoner plot course.yml --run-all 52 | ``` 53 | 54 | Grafana and Polaris should now be installed on your cluster! 55 | 56 | ## Importing Existing Releases 57 | 58 | > Importing existing releases is experimental and the result should be reviewed. 59 | 60 | If you're already using Helm but want to start using `reckoner`, you can use `reckoner import` to facilitate your migration. 61 | 62 | We recommend carefully examining the output of a `reckoner diff` before relying on any imported course.yml definitions. 63 | 64 | 65 | ## Join the Fairwinds Open Source Community 66 | 67 | The goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap, 68 | and network with fellow Kubernetes users. 69 | [Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-2na8gtwb4-DGQ4qgmQbczQyB2NlFlYQQ) 70 | or 71 | [join the user group](https://www.fairwinds.com/open-source-software-user-group) to get involved! 72 | 73 | 74 | Love Fairwinds Open Source? Automate Fairwinds Open Source for free with Fairwinds Insights. Click to learn more 76 | 77 | 78 | ## Other Projects from Fairwinds 79 | 80 | Enjoying Reckoner? Check out some of our other projects: 81 | * [Polaris](https://github.com/FairwindsOps/Polaris) - Audit, enforce, and build policies for Kubernetes resources, including over 20 built-in checks for best practices 82 | * [Goldilocks](https://github.com/FairwindsOps/Goldilocks) - Right-size your Kubernetes Deployments by compare your memory and CPU settings against actual usage 83 | * [Pluto](https://github.com/FairwindsOps/Pluto) - Detect Kubernetes resources that have been deprecated or removed in future versions 84 | * [Nova](https://github.com/FairwindsOps/Nova) - Check to see if any of your Helm charts have updates available 85 | * [rbac-manager](https://github.com/FairwindsOps/rbac-manager) - Simplify the management of RBAC in your Kubernetes clusters 86 | 87 | Or [check out the full list](https://www.fairwinds.com/open-source-software?utm_source=reckoner&utm_medium=reckoner&utm_campaign=reckoner) 88 | -------------------------------------------------------------------------------- /cmd/root_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package cmd 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func TestGetCourseFilePath(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | path []string 25 | want string 26 | }{ 27 | { 28 | name: "empty path, expect default", 29 | path: []string{}, 30 | want: "course.yaml", 31 | }, 32 | { 33 | name: "single course.yaml specified, expect mirror", 34 | path: []string{"testdata/course.yaml"}, 35 | want: "testdata/course.yaml", 36 | }, 37 | { 38 | name: "multiple course.yaml specified, expect first", 39 | path: []string{"testdata/course.yaml", "second_course.yaml"}, 40 | want: "testdata/course.yaml", 41 | }, 42 | } 43 | for _, tt := range tests { 44 | t.Run(tt.name, func(t *testing.T) { 45 | courseFilePath := getCourseFilePath(tt.path) // get course file path 46 | assert.Equal(t, tt.want, courseFilePath) // compare wanted vs result 47 | }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /cmd/testdata/course.yaml: -------------------------------------------------------------------------------- 1 | --- -------------------------------------------------------------------------------- /cmd/testdata/coursev2.yaml: -------------------------------------------------------------------------------- 1 | schema: v2 2 | namespace: namespace 3 | repository: stable 4 | context: farglebargle 5 | repositories: 6 | git-repo-test: 7 | git: https://github.com/FairwindsOps/charts 8 | path: stable 9 | gitrelease-git-repository: 10 | git: giturl 11 | path: gitpath 12 | helm-repo: 13 | url: https://ahelmrepo.example.com 14 | releases: 15 | basic: 16 | chart: somechart 17 | version: 2.0.0 18 | repository: helm-repo 19 | values: 20 | dummyvalue: false 21 | gitrelease: 22 | chart: gitchart 23 | version: main 24 | repository: gitrelease-git-repository 25 | standard: 26 | chart: basic 27 | repository: helm-repo 28 | -------------------------------------------------------------------------------- /cmd/update.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package cmd 14 | 15 | import ( 16 | "os" 17 | 18 | "github.com/blang/semver" 19 | "github.com/gookit/color" 20 | "github.com/rhysd/go-github-selfupdate/selfupdate" 21 | "github.com/spf13/cobra" 22 | "k8s.io/klog/v2" 23 | ) 24 | 25 | var updateReckonerCmd = &cobra.Command{ 26 | Use: "update-cli", 27 | Short: "Update reckoner.", 28 | Long: "Updates the reckoner binary to the latest tagged release.", 29 | Run: func(cmd *cobra.Command, args []string) { 30 | klog.V(4).Infof("current version: %s", version) 31 | v, err := semver.Parse(version) 32 | if err != nil { 33 | color.Red.Printf("Could not parse version: %s\n", err.Error()) 34 | os.Exit(1) 35 | } 36 | color.Green.Printf("Checking for update. Current version: %s\n", version) 37 | 38 | up, err := selfupdate.NewUpdater(selfupdate.Config{}) 39 | if err != nil { 40 | color.Red.Println(err) 41 | os.Exit(1) 42 | } 43 | latest, err := up.UpdateSelf(v, "fairwindsops/reckoner") 44 | if err != nil { 45 | color.Red.Println("Update failed:", err) 46 | os.Exit(1) 47 | } 48 | if latest.Version.Equals(v) { 49 | color.Green.Println("Current binary is the latest version", version) 50 | } else { 51 | color.Green.Println("Successfully updated to version", latest.Version) 52 | color.Gray.Println("Release note:\n", latest.ReleaseNotes) 53 | } 54 | }, 55 | } 56 | -------------------------------------------------------------------------------- /cmd/validation.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package cmd 14 | 15 | import ( 16 | "fmt" 17 | "os" 18 | 19 | "github.com/fatih/color" 20 | "github.com/spf13/cobra" 21 | "k8s.io/klog/v2" 22 | ) 23 | 24 | // validateCobraArgs ensures that the only argument passed is a course 25 | // file that exists 26 | func validateCobraArgs(cmd *cobra.Command, args []string) (err error) { 27 | courseFile = getCourseFilePath(args) // guaranteed to return a path 28 | 29 | // at this point we should have a courseFile value. the following 30 | // makes sure the course file is accessible... 31 | err = validateCourseFilePath(courseFile) 32 | if err != nil { 33 | return err 34 | } 35 | 36 | // ...and checks that we passed the appropriate set of arguments 37 | err = validateArgs(runAll, onlyRun, args) 38 | if err != nil { 39 | return err 40 | } 41 | 42 | color.NoColor = noColor 43 | klog.V(3).Infof("colorize output: %v", !noColor) 44 | 45 | return err 46 | } 47 | 48 | // validateArgs validates that the correct arguments were passed and returns the name of the course file 49 | func validateArgs(runAll bool, onlyRun, args []string) error { 50 | if runAll && len(onlyRun) != 0 { 51 | return fmt.Errorf("you must either use run-all or only") 52 | } 53 | 54 | if !runAll && len(onlyRun) == 0 { 55 | return fmt.Errorf("you must use at least one of run-all or only") 56 | } 57 | 58 | if len(args) > 1 { 59 | return fmt.Errorf("you may only pass one course YAML file at the same time") 60 | } 61 | 62 | return nil 63 | } 64 | 65 | func validateCourseFilePath(courseFile string) error { 66 | _, err := os.Stat(courseFile) 67 | if os.IsNotExist(err) { 68 | return fmt.Errorf("course file %s does not exist", courseFile) 69 | } 70 | return err 71 | } 72 | -------------------------------------------------------------------------------- /cmd/validation_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package cmd 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func TestValidateArgs(t *testing.T) { 22 | type args struct { 23 | runAll bool 24 | onlyRun []string 25 | args []string 26 | } 27 | tests := []struct { 28 | name string 29 | args args 30 | want string 31 | wantErr bool 32 | }{ 33 | { 34 | name: "empty args", 35 | args: args{}, 36 | want: "", 37 | wantErr: true, 38 | }, 39 | { 40 | name: "only one of runAll or onlyRun can be set", 41 | args: args{ 42 | args: []string{}, 43 | runAll: true, 44 | onlyRun: []string{"rbac-manager"}, 45 | }, 46 | wantErr: true, 47 | }, 48 | { 49 | name: "pass onlyrun with success", 50 | args: args{ 51 | args: []string{}, 52 | runAll: false, 53 | onlyRun: []string{"rbac-manager"}, 54 | }, 55 | want: "testdata/course.yaml", 56 | wantErr: false, 57 | }, 58 | { 59 | name: "pass runall with success", 60 | args: args{ 61 | args: []string{}, 62 | runAll: true, 63 | }, 64 | want: "testdata/course.yaml", 65 | wantErr: false, 66 | }, 67 | { 68 | name: "length of args = 2", 69 | args: args{ 70 | args: []string{"testdata/course.yaml", "course.yaml"}, 71 | runAll: false, 72 | onlyRun: []string{"rbac-manager"}, 73 | }, 74 | wantErr: true, 75 | }, 76 | } 77 | for _, tt := range tests { 78 | t.Run(tt.name, func(t *testing.T) { 79 | err := validateArgs(tt.args.runAll, tt.args.onlyRun, tt.args.args) 80 | if tt.wantErr { 81 | assert.Error(t, err) 82 | } else { 83 | assert.NoError(t, err) 84 | } 85 | }) 86 | } 87 | } 88 | 89 | func TestValidateCourseFilePath(t *testing.T) { 90 | type args struct { 91 | runAll bool 92 | onlyRun []string 93 | args []string 94 | } 95 | tests := []struct { 96 | name string 97 | args args 98 | want string 99 | wantErr bool 100 | }{ 101 | { 102 | name: "empty args", 103 | args: args{}, 104 | want: "", 105 | wantErr: true, 106 | }, 107 | { 108 | name: "course.yaml does not exist", 109 | args: args{ 110 | args: []string{"course.yaml"}, 111 | runAll: true, 112 | }, 113 | want: "", 114 | wantErr: true, 115 | }, 116 | { 117 | name: "course.yaml exists, pass onlyrun with success", 118 | args: args{ 119 | args: []string{"testdata/course.yaml"}, 120 | runAll: false, 121 | onlyRun: []string{"rbac-manager"}, 122 | }, 123 | want: "testdata/course.yaml", 124 | wantErr: false, 125 | }, 126 | } 127 | for _, tt := range tests { 128 | t.Run(tt.name, func(t *testing.T) { 129 | courseFilePath := getCourseFilePath(tt.args.args) 130 | 131 | // we now have a path to a course YAML file to test, 132 | // whether from command-line argument or from default value. 133 | // getting a value from environment variable is not directly tested, 134 | // but should always fall under the same tests as from any other 135 | // input source. 136 | 137 | // validate course file existence scenarios 138 | err := validateCourseFilePath(courseFilePath) 139 | if tt.wantErr { 140 | assert.Error(t, err) 141 | } else { 142 | assert.NoError(t, err) 143 | } 144 | }) 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /cmd/version.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package cmd 14 | 15 | import ( 16 | "fmt" 17 | 18 | "github.com/spf13/cobra" 19 | ) 20 | 21 | var ( 22 | shortVersion bool 23 | ) 24 | 25 | func init() { 26 | versionCmd.PersistentFlags().BoolVar(&shortVersion, "short", false, "Display only the version. Useful for automation") 27 | } 28 | 29 | var versionCmd = &cobra.Command{ 30 | Use: "version", 31 | Short: "Prints the current version of the tool.", 32 | Long: `Prints the current version.`, 33 | Run: func(cmd *cobra.Command, args []string) { 34 | if shortVersion { 35 | fmt.Println(version) 36 | } else { 37 | fmt.Println("Version:" + version + " Commit:" + versionCommit) 38 | } 39 | }, 40 | } 41 | -------------------------------------------------------------------------------- /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: "Reckoner Documentation", 6 | description: "Documentation for Fairwinds' Reckoner", 7 | themeConfig: { 8 | docsRepo: "FairwindsOps/reckoner", 9 | sidebar: [ 10 | { 11 | title: "Reckoner", 12 | path: "/", 13 | sidebarDepth: 0, 14 | }, 15 | { 16 | title: "Usage", 17 | path: "/usage", 18 | }, 19 | { 20 | title: "Contributing", 21 | children: [ 22 | { 23 | title: "Guide", 24 | path: "contributing/guide" 25 | }, 26 | { 27 | title: "Code of Conduct", 28 | path: "contributing/code-of-conduct" 29 | } 30 | ] 31 | } 32 | ] 33 | }, 34 | } 35 | 36 | -------------------------------------------------------------------------------- /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/reckoner/a36d8b6be6cb9ba86670d89325e7adf124bbf226/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/reckoner-icon.svg: -------------------------------------------------------------------------------- 1 | ../../../../img/reckoner-icon.svg -------------------------------------------------------------------------------- /docs/.vuepress/public/img/reckoner-logo.svg: -------------------------------------------------------------------------------- 1 | ../../../../img/reckoner-logo.svg -------------------------------------------------------------------------------- /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 | Reckoner 3 |
4 | 5 | 6 | 7 | 8 | 9 | 10 |
11 | 12 | Command line helper for helm. 13 | 14 | This utility adds to the functionality of [Helm](https://github.com/kubernetes/helm) in multiple ways: 15 | 16 | * Creates a declarative syntax to manage multiple releases in one place 17 | * Allows installation of charts from a git commit/branch/release 18 | 19 | # Documentation 20 | Check out the [documentation at docs.fairwinds.com](https://reckoner.docs.fairwinds.com/) 21 | 22 | ## Requirements 23 | 24 | * helm (>= 3.0.0), installed and initialized 25 | 26 | > Helm2 is untested as of v4.3.0. The maintainers of helm have [deprecated helm2](https://helm.sh/blog/helm-v2-deprecation-timeline/). 27 | 28 | ## Quickstart 29 | 30 | In course.yaml, write: 31 | 32 | ```yaml 33 | namespace: default 34 | charts: 35 | grafana: 36 | namespace: grafana 37 | values: 38 | image: 39 | tag: "6.2.5" 40 | polaris-dashboard: 41 | namespace: polaris-dashboard 42 | repository: 43 | git: https://github.com/FairwindsOps/charts 44 | path: stable 45 | chart: polaris 46 | ``` 47 | 48 | Then run: 49 | 50 | ```shell 51 | reckoner plot course.yaml --run-all 52 | ``` 53 | 54 | Grafana and Polaris should now be installed on your cluster! 55 | 56 | ## Importing Existing Releases 57 | 58 | > Importing existing releases is experimental and the result should be reviewed. 59 | 60 | If you're already using Helm but want to start using `reckoner`, you can use `reckoner import` to facilitate your migration. 61 | 62 | We recommend carefully examining the output of a `reckoner diff` before relying on any imported course.yaml definitions. 63 | 64 | 65 | ## Join the Fairwinds Open Source Community 66 | 67 | The goal of the Fairwinds Community is to exchange ideas, influence the open source roadmap, 68 | and network with fellow Kubernetes users. 69 | [Chat with us on Slack](https://join.slack.com/t/fairwindscommunity/shared_invite/zt-e3c6vj4l-3lIH6dvKqzWII5fSSFDi1g) 70 | or 71 | [join the user group](https://www.fairwinds.com/open-source-software-user-group) to get involved! 72 | 73 | 74 | Love Fairwinds Open Source? Share your business email and job title and we'll send you a free Fairwinds t-shirt! 75 | 76 | 77 | ## Other Projects from Fairwinds 78 | 79 | Enjoying Reckoner? Check out some of our other projects: 80 | * [Polaris](https://github.com/FairwindsOps/Polaris) - Audit, enforce, and build policies for Kubernetes resources, including over 20 built-in checks for best practices 81 | * [Goldilocks](https://github.com/FairwindsOps/Goldilocks) - Right-size your Kubernetes Deployments by compare your memory and CPU settings against actual usage 82 | * [Pluto](https://github.com/FairwindsOps/Pluto) - Detect Kubernetes resources that have been deprecated or removed in future versions 83 | * [Nova](https://github.com/FairwindsOps/Nova) - Check to see if any of your Helm charts have updates available 84 | * [rbac-manager](https://github.com/FairwindsOps/rbac-manager) - Simplify the management of RBAC in your Kubernetes clusters 85 | 86 | Or [check out the full list](https://www.fairwinds.com/open-source-software?utm_source=reckoner&utm_medium=reckoner&utm_campaign=reckoner) 87 | -------------------------------------------------------------------------------- /docs/contributing/code-of-conduct.md: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | - name: description 4 | content: "Fairwinds Reckoner | Contribution Code of Conduct" 5 | --- 6 | # Contributor Covenant Code of Conduct 7 | 8 | ## Our Pledge 9 | 10 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment include: 15 | 16 | * Using welcoming and inclusive language 17 | * Being respectful of differing viewpoints and experiences 18 | * Gracefully accepting constructive criticism 19 | * Focusing on what is best for the community 20 | * Showing empathy towards other community members 21 | 22 | Examples of unacceptable behavior by participants include: 23 | 24 | * The use of sexualized language or imagery and unwelcome sexual attention or advances 25 | * Trolling, insulting/derogatory comments, and personal or political attacks 26 | * Public or private harassment 27 | * Publishing others' private information, such as a physical or electronic address, without explicit permission 28 | * Other conduct which could reasonably be considered inappropriate in a professional setting 29 | 30 | ## Our Responsibilities 31 | 32 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 33 | 34 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at info@fairwinds.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 43 | 44 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 45 | 46 | ## Attribution 47 | 48 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 49 | 50 | [homepage]: http://contributor-covenant.org 51 | [version]: http://contributor-covenant.org/version/1/4/ 52 | -------------------------------------------------------------------------------- /docs/contributing/guide.md: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | - name: description 4 | content: "Fairwinds Reckoner | Contribution Guidelines" 5 | --- 6 | # Contributing 7 | 8 | 9 | ## Installation for Local Development 10 | 11 | Requirements: 12 | * [Go](https://go.dev) 13 | 14 | ```sh 15 | $ go --version # Check your version of golang 16 | $ go mod tidy # get dependencies 17 | $ go run . --help # compile & run the project 18 | ``` 19 | 20 | ## Requirements for Pull Requests 21 | * Update the changelog 22 | * Run tests 23 | * Suggest version bump type 24 | 25 | ## How to run tests and test coverage 26 | ```sh 27 | $ go test ./... 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/main-metadata.md: -------------------------------------------------------------------------------- 1 | --- 2 | meta: 3 | - name: description 4 | content: "Fairwinds Reckoner | Documentation" 5 | --- 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/01_test_basic.yaml: -------------------------------------------------------------------------------- 1 | namespace: 01-test 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | first-chart: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | namespace: 01-infra 13 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/02_test_create_namespace.yaml: -------------------------------------------------------------------------------- 1 | namespace: 02-farglebargle 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | namespace-test: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/03_test_env_var.yaml: -------------------------------------------------------------------------------- 1 | namespace: 03-test 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | check-values: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | namespace: 03-infra 13 | values: 14 | non-used: "${myvar}" 15 | comment-problem-detector: 16 | # This is a comment "${this_var_should_not_be_parsed}" 17 | - test-value 18 | escape-values: 19 | repository: fairwinds-incubator 20 | chart: basic-demo 21 | namespace: 03-infra 22 | values: 23 | non-used: "A string that needs a $$ in it" 24 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/04_test_failed_chart.yaml: -------------------------------------------------------------------------------- 1 | namespace: 04-test 2 | repositories: 3 | runatlantis: 4 | url: https://runatlantis.github.io/helm-charts 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | bad-chart: 10 | repository: runatlantis 11 | chart: atlantis 12 | version: 3.12.4 13 | values: 14 | github: 15 | broken: yep # This value should cause an execution error 16 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/05_test_exit_on_post_install_hook.yaml: -------------------------------------------------------------------------------- 1 | namespace: 05-infra 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | basic-demo: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | hooks: 13 | pre_install: 14 | - whoami 15 | post_install: 16 | - exit 1 17 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/06_test_exit_on_pre_install_hook.yaml: -------------------------------------------------------------------------------- 1 | namespace: 06-infra 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | basic-demo: 10 | repository: stable 11 | chart: basic-demo 12 | hooks: 13 | pre_install: 14 | - nonexistent command here 15 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/07_test_good_hooks.yaml: -------------------------------------------------------------------------------- 1 | namespace: 07-redis-test-namespace 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | should-install: 10 | chart: basic-demo 11 | repository: fairwinds-incubator 12 | hooks: 13 | pre_install: 14 | - whoami 15 | post_install: 16 | - whoami 17 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/08_test_multi_chart.yaml: -------------------------------------------------------------------------------- 1 | namespace: 08-second 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | first-chart: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | namespace: 08-first 13 | second-chart: 14 | repository: fairwinds-incubator 15 | chart: basic-demo 16 | namespace: 08-second 17 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/09_test_multi_chart.yaml: -------------------------------------------------------------------------------- 1 | namespace: 09-second 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | first-chart-09: 10 | chart: basic-demo 11 | repository: fairwinds-incubator 12 | namespace: 09-first 13 | second-chart-09: 14 | chart: basic-demo 15 | repository: fairwinds-incubator 16 | namespace: 09-second 17 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/10_test_git_chart.yaml: -------------------------------------------------------------------------------- 1 | namespace: 10-test #namespace to install the chart in, defaults to 'kube-system' 2 | repositories: 3 | polaris-repo-test: 4 | git: https://github.com/FairwindsOps/charts 5 | path: stable 6 | minimum_versions: #set minimum version requirements here 7 | helm: 0.0.0 8 | reckoner: 0.0.0 9 | charts: 10 | polaris-release: 11 | namespace: 10-polaris 12 | chart: polaris 13 | repository: polaris-repo-test 14 | polaris: 15 | namespace: 10-another-polaris 16 | repository: 17 | git: https://github.com/FairwindsOps/charts.git 18 | path: stable 19 | goldilocks-10: 20 | chart: goldilocks 21 | version: 8d2e55aeaedfd3de3839babf3fb5f747ff17cbd9 22 | repository: 23 | git: https://github.com/FairwindsOps/charts 24 | path: stable 25 | values: 26 | vpa: 27 | enabled: false 28 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/11_test_after_first_failure.yaml: -------------------------------------------------------------------------------- 1 | namespace: 11-test 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | runatlantis: 6 | url: https://runatlantis.github.io/helm-charts 7 | minimum_versions: 8 | helm: 0.0.0 9 | reckoner: 0.0.0 10 | charts: 11 | good-chart: 12 | repository: fairwinds-incubator 13 | chart: basic-demo 14 | bad-chart: 15 | repository: runatlantis 16 | chart: atlantis 17 | version: 3.12.4 18 | values: 19 | github: 20 | broken: yep # This value causes an execution error 21 | expected-skipped-chart: # This chart should never be installed due to bad-chart failing (if applied in order) 22 | repository: fairwinds-incubator 23 | chart: basic-demo 24 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/12_test_after_first_failure.yaml: -------------------------------------------------------------------------------- 1 | namespace: 12-test 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | runatlantis: 6 | url: https://runatlantis.github.io/helm-charts 7 | minimum_versions: 8 | helm: 0.0.0 9 | reckoner: 0.0.0 10 | charts: 11 | good-chart: 12 | repository: fairwinds-incubator 13 | chart: basic-demo 14 | bad-chart: 15 | repository: runatlantis 16 | chart: atlantis 17 | version: 3.5.2 18 | values: 19 | github: 20 | broken: yep # This value causes an execution error 21 | expected-skipped-chart: # This chart should never be installed due to bad-chart failing (if applied in order) 22 | repository: fairwinds-incubator 23 | chart: basic-demo 24 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/13_test_bad_schema_repository.yaml: -------------------------------------------------------------------------------- 1 | namespace: "doesnotmatter" 2 | repository: "stable" 3 | repositories: 4 | stable: "this is invalid schema" 5 | charts: 6 | my-chart: 7 | chart: basic-demo 8 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/13_test_lint_bad_secret.yaml: -------------------------------------------------------------------------------- 1 | namespace: "doesnotmatter" 2 | repository: "stable" 3 | repositories: 4 | stable: "this is invalid schema" 5 | charts: 6 | my-chart: 7 | chart: basic-demo 8 | secrets: 9 | - backend: testbackend 10 | - name: testname 11 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/13_test_lint_good_secret.yaml: -------------------------------------------------------------------------------- 1 | namespace: "doesnotmatter" 2 | repository: "stable" 3 | charts: 4 | my-chart: 5 | chart: basic-demo 6 | secrets: 7 | - backend: ShellExecutor 8 | name: testname 9 | script: 10 | - echo 11 | - "test_other_allowed" 12 | repositories: 13 | stable: 14 | url: https://charts.example.com/stable 15 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/13_test_required_schema.yaml: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/15_test_default_namespace_annotation_and_labels.yaml: -------------------------------------------------------------------------------- 1 | namespace: 15-annotatednamespace 2 | namespace_management: 3 | default: 4 | metadata: 5 | annotations: 6 | reckoner: rocks 7 | labels: 8 | rocks: reckoner 9 | settings: 10 | overwrite: True 11 | repositories: 12 | fairwinds-incubator: 13 | url: https://charts.fairwinds.com/incubator 14 | minimum_versions: 15 | helm: 0.0.0 16 | reckoner: 0.0.0 17 | charts: 18 | namespace-test: 19 | repository: fairwinds-incubator 20 | chart: basic-demo 21 | namespace-test-merge: 22 | namespace: 15-annotatednamespace-2 23 | namespace_management: 24 | metadata: 25 | annotations: 26 | this: exists 27 | labels: 28 | this: alsoexists 29 | repository: fairwinds-incubator 30 | chart: basic-demo 31 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/16_test_overwrite_namespace_annotation_and_labels.yaml: -------------------------------------------------------------------------------- 1 | namespace: 16-annotatednamespace #namespace to install the chart in, defaults to 'kube-system' 2 | namespace_management: 3 | default: 4 | metadata: 5 | annotations: 6 | reckoner: doesnotrock 7 | labels: 8 | rocks: reckonerstill 9 | settings: 10 | overwrite: True 11 | repositories: 12 | stable: 13 | url: https://charts.helm.sh/stable 14 | incubator: 15 | url: https://charts.helm.sh/incubator 16 | fairwinds-stable: 17 | url: https://charts.fairwinds.com/stable 18 | fairwinds-incubator: 19 | url: https://charts.fairwinds.com/incubator 20 | minimum_versions: #set minimum version requirements here 21 | helm: 0.0.0 22 | reckoner: 0.0.0 23 | charts: 24 | namespace-test: 25 | repository: fairwinds-incubator 26 | chart: basic-demo 27 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/17_test_dont_overwrite_ns_meta.yaml: -------------------------------------------------------------------------------- 1 | namespace: 17-annotatednamespace #namespace to install the chart in, defaults to 'kube-system' 2 | namespace_management: 3 | default: 4 | metadata: 5 | annotations: 6 | reckoner: doesnotrock 7 | labels: 8 | rocks: reckonerstill 9 | settings: 10 | overwrite: False 11 | repositories: 12 | fairwinds-incubator: 13 | url: https://charts.fairwinds.com/incubator 14 | minimum_versions: #set minimum version requirements here 15 | helm: 0.0.0 16 | reckoner: 0.0.0 17 | charts: 18 | namespace-test: 19 | repository: fairwinds-incubator 20 | chart: basic-demo 21 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/18_strong_typing.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: 18-testing 3 | minimum_versions: 4 | helm: 0.0.0 5 | reckoner: 0.0.0 6 | repositories: 7 | fairwinds-incubator: 8 | url: https://charts.fairwinds.com/incubator 9 | charts: 10 | chart-values: 11 | repository: fairwinds-incubator 12 | chart: basic-demo 13 | values: 14 | expect-number-float: 1.05 15 | expect-bool-false: false 16 | expect-bool-true: true 17 | expect-string-true: "true" 18 | expect-string-integer: "1000" 19 | expect-number-integer: 1000 20 | expect-null-from-null: null 21 | expect-string-from-null: "null" 22 | chart-env-values: 23 | repository: fairwinds-incubator 24 | chart: basic-demo 25 | values: 26 | expect-string-from-env-var-yes: "${yes_var}" 27 | expect-bool-yes-from-env-var-yes-no-quotes: ${yes_var} 28 | expect-bool-true-from-env-var-true-no-quotes: ${true_var} 29 | expect-string-from-env-var-true: "${true_var}" 30 | expect-bool-false-from-env-var-false-no-quotes: ${false_var} 31 | expect-string-from-env-var-false: "${false_var}" 32 | expect-string-from-integer: "${int_var}" 33 | expect-number-from-integer-no-quotes: ${int_var} 34 | expect-number-from-float-no-quotes: ${float_var} 35 | expect-string-from-float: "${float_var}" 36 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/20_test_strong_ordering.yaml: -------------------------------------------------------------------------------- 1 | namespace: 20-test 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | first-chart: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | second-chart: 13 | repository: fairwinds-incubator 14 | chart: basic-demo 15 | hooks: 16 | pre_install: 17 | - sleep 5 18 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/21_test_diff.yaml: -------------------------------------------------------------------------------- 1 | namespace: 21-test-diff 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: #set minimum version requirements here 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | chart-with-namespace: 10 | namespace: 21-test-diff 11 | repository: fairwinds-incubator 12 | chart: basic-demo 13 | chart-without-namespace: 14 | repository: fairwinds-incubator 15 | chart: basic-demo 16 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/23_test_environment_variable_namespace_on_chart.yaml: -------------------------------------------------------------------------------- 1 | namespace: 23-should-not-get-used #namespace to install the chart in, defaults to 'kube-system' 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: #set minimum version requirements here 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | env-var-name-space: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | namespace: $TEST_ENVIRONMENT_NAMESPACE 13 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/24_test_no_context.yaml: -------------------------------------------------------------------------------- 1 | namespace: 24-test-no-context 2 | context: thiscontextdoesnotexist 3 | repositories: 4 | fairwinds-incubator: 5 | url: https://charts.fairwinds.com/incubator 6 | minimum_versions: #set minimum version requirements here 7 | helm: 0.0.0 8 | reckoner: 0.0.0 9 | charts: 10 | chart-with-no-context: 11 | repository: fairwinds-incubator 12 | chart: basic-demo 13 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/25_test_import.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: 25-test-import 3 | minimum_versions: 4 | helm: 0.0.0 5 | reckoner: 0.0.0 6 | repositories: 7 | fairwinds-incubator: 8 | url: https://charts.fairwinds.com/incubator 9 | charts: 10 | import-test: 11 | repository: fairwinds-incubator 12 | chart: basic-demo 13 | version: 1.0.0 14 | values: 15 | expect-value: 1.05 16 | expect-bool-value: false 17 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/26_test_shell_executor_secret.yaml: -------------------------------------------------------------------------------- 1 | namespace: 26-shell-executor #namespace to install the chart in, defaults to 'kube-system' 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: #set minimum version requirements here 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | secrets: 9 | - name: TEST_SECRET 10 | backend: ShellExecutor 11 | script: 12 | - echo 13 | - "THISVALUEISSECRET" 14 | charts: 15 | shell-executor-chart: 16 | repository: fairwinds-incubator 17 | chart: basic-demo 18 | values: 19 | usersupplied: ${TEST_SECRET} 20 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/27_gitops.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | schema: v2 3 | namespace: myns 4 | repository: stable 5 | context: mycontext 6 | repositories: 7 | fairwinds-stable: 8 | url: https://charts.fairwinds.com/stable 9 | minimum_versions: 10 | helm: 3.4.1 11 | reckoner: 4.3.1 12 | gitops: 13 | argocd: 14 | kind: Application 15 | apiVersion: argoproj.io/v1alpha1 16 | metadata: 17 | namespace: argocd 18 | annotations: 19 | one_key: one_value 20 | spec: 21 | destination: 22 | namespace: some_value 23 | server: https://kubernetes.default.svc 24 | project: default 25 | source: 26 | # path: manifests 27 | repoURL: https://github.com/someuser/clustername.git 28 | directory: 29 | recurse: true 30 | # plugin: 31 | # name: argocd-vault-plugin 32 | syncPolicy: 33 | automated: 34 | prune: true 35 | syncOptions: 36 | - CreateNamespace=true 37 | - PruneLast=true 38 | namespace_management: 39 | default: 40 | metadata: 41 | annotations: 42 | insights.fairwinds.com/adminOnly: "true" 43 | labels: 44 | ManagedBy: Fairwinds 45 | settings: 46 | overwrite: true 47 | releases: 48 | - name: rbac-manager 49 | namespace: rbac-manager 50 | namespace_management: 51 | settings: 52 | overwrite: true 53 | chart: rbac-manager 54 | version: 1.11.1 55 | repository: fairwinds-stable 56 | 57 | gitops: 58 | argocd: 59 | metadata: 60 | annotations: 61 | notifications.argoproj.io/subscribe.on-sync-succeeded.slack: some_channel 62 | spec: 63 | project: different_project 64 | source: 65 | path: some_totally_different_path 66 | repoURL: https://github.com/another_user/another_repo.git 67 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/test_does_not_overwrite_namespace_annotation_and_labels.yaml: -------------------------------------------------------------------------------- 1 | namespace: annotatednamespace #namespace to install the chart in, defaults to 'kube-system' 2 | namespace_management: 3 | default: 4 | metadata: 5 | annotations: 6 | reckoner: doesnotrockalsoshoulenotbewritten 7 | labels: 8 | rocks: reckonerstillshouldnotbewritten 9 | settings: 10 | overwrite: False 11 | repositories: 12 | fairwinds-incubator: 13 | url: https://charts.fairwinds.com/incubator 14 | minimum_versions: #set minimum version requirements here 15 | helm: 0.0.0 16 | reckoner: 0.0.0 17 | charts: 18 | namespace-test: 19 | repository: fairwinds-incubator 20 | chart: basic-demo 21 | namespace: annotatednamespace 22 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/testing_in_folder/subfolder/relative_hook_namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: relative-hook 5 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/testing_in_folder/subfolder/yaml_values.yaml: -------------------------------------------------------------------------------- 1 | new_key: new_value 2 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/testing_in_folder/test_files_in_folders.yaml: -------------------------------------------------------------------------------- 1 | namespace: 14-infra 2 | repositories: 3 | fairwinds-incubator: 4 | url: https://charts.fairwinds.com/incubator 5 | minimum_versions: 6 | helm: 0.0.0 7 | reckoner: 0.0.0 8 | charts: 9 | chart-one: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | files: 13 | - subfolder/yaml_values.yaml 14 | -------------------------------------------------------------------------------- /end_to_end_testing/course_files/testing_in_folder/testing_in_subfolder/22_testing_relative_hook_location.yaml: -------------------------------------------------------------------------------- 1 | namespace: 22-infra 2 | minimum_versions: 3 | helm: 0.0.0 4 | reckoner: 0.0.0 5 | repositories: 6 | fairwinds-incubator: 7 | url: https://charts.fairwinds.com/incubator 8 | charts: 9 | test-relative-hooks: 10 | repository: fairwinds-incubator 11 | chart: basic-demo 12 | hooks: 13 | pre_install: 14 | - kubectl apply -f ../subfolder/relative_hook_namespace.yaml 15 | -------------------------------------------------------------------------------- /end_to_end_testing/pre_go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | docker cp . e2e-command-runner:/reckoner 4 | docker cp /dist e2e-command-runner:/reckoner 5 | -------------------------------------------------------------------------------- /end_to_end_testing/run_go.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -x 4 | set -e 5 | 6 | # Install Go 7 | curl -LO https://go.dev/dl/go1.20.7.linux-amd64.tar.gz 8 | 9 | tar -C /usr/local -xzf go1.20.7.linux-amd64.tar.gz 10 | export PATH=$PATH:/usr/local/go/bin 11 | go version 12 | 13 | # build 14 | cd /reckoner 15 | make build 16 | 17 | mv /reckoner/reckoner /usr/local/bin/reckoner 18 | reckoner version 19 | 20 | curl -LO https://github.com/ovh/venom/releases/download/v1.1.0/venom.linux-amd64 21 | mv venom.linux-amd64 /usr/local/bin/venom 22 | chmod +x /usr/local/bin/venom 23 | 24 | mkdir -p /tmp/test-results 25 | 26 | cd /reckoner/end_to_end_testing 27 | 28 | # The parallelization number must remain relatively low otherwise the tests become flaky due to resources and pending pods and such 29 | venom run tests/* --output-dir=/tmp/test-results 30 | exit $? 31 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/00_setup.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Setup 3 | testcases: 4 | - name: Helm Version 5 | steps: 6 | - script: | 7 | helm version 8 | assertions: 9 | - result.code ShouldEqual 0 10 | - result.systemout ShouldContainSubstring "v3" 11 | - name: Reckoner Version 12 | steps: 13 | - script: | 14 | reckoner version 15 | assertions: 16 | - result.code ShouldEqual 0 17 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/01_basic.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Basic Functionality 3 | vars: 4 | course: ../course_files/01_test_basic.yaml 5 | namespace: 01-infra 6 | release: first-chart 7 | testcases: 8 | - name: 01 - get manifests nothing installed yet 9 | steps: 10 | - script: | 11 | reckoner get-manifests {{.course}} -a 12 | assertions: 13 | - result.code ShouldEqual 1 14 | - name: 01 - plot course 15 | steps: 16 | - script: | 17 | reckoner plot -a {{.course}} 18 | assertions: 19 | - result.code ShouldEqual 0 20 | - script: | 21 | helm -n {{.namespace}} get all {{.release}} 22 | assertions: 23 | - result.code ShouldEqual 0 24 | - result.systemout ShouldContainSubstring '{{.namespace}}' 25 | - result.systemout ShouldContainSubstring '{{.release}}' 26 | - name: 01 - template course --run-all 27 | steps: 28 | - script: | 29 | reckoner template {{.course}} -a 30 | assertions: 31 | - result.code ShouldEqual 0 32 | - name: 01 - template course --only release 33 | steps: 34 | - script: | 35 | reckoner template {{.course}} -o {{.release}} 36 | assertions: 37 | - result.code ShouldEqual 0 38 | - name: 01 - get manifests --run-all 39 | steps: 40 | - script: | 41 | reckoner get-manifests {{.course}} -a 42 | assertions: 43 | - result.code ShouldEqual 0 44 | - name: 01 - get manifests not in course 45 | steps: 46 | - script: | 47 | reckoner get-manifests {{.course}} -o doesnotexist 48 | assertions: 49 | - result.code ShouldEqual 1 50 | - name: 01 - diff all 51 | steps: 52 | - script: | 53 | reckoner diff {{.course}} -a 54 | assertions: 55 | - result.code ShouldEqual 0 56 | - name: 01 - update all 57 | steps: 58 | - script: | 59 | reckoner update {{.course}} -a 60 | assertions: 61 | - result.code ShouldEqual 0 62 | - name: 01 - template only release 63 | steps: 64 | - script: | 65 | reckoner template {{.course}} -o {{.release}} 66 | assertions: 67 | - result.code ShouldEqual 0 68 | - name: 01 - cleanup 69 | steps: 70 | - script: | 71 | helm -n {{.namespace}} delete {{.release}} 72 | kubectl delete ns {{.namespace}} 73 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/02_namespace_creation.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Namespace Creation 3 | vars: 4 | course: ../course_files/02_test_create_namespace.yaml 5 | namespace: 02-farglebargle 6 | release: namespace-test 7 | testcases: 8 | - name: 02 - plot --no-create-namespace course fails 9 | steps: 10 | - script: | 11 | reckoner plot --no-create-namespace -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 1 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldNotEqual 0 18 | - name: 02 - plot course 19 | steps: 20 | - script: | 21 | reckoner plot -a {{.course}} 22 | assertions: 23 | - result.code ShouldEqual 0 24 | - script: | 25 | helm -n {{.namespace}} get all {{.release}} 26 | assertions: 27 | - result.code ShouldEqual 0 28 | - result.systemout ShouldContainSubstring "{{.namespace}}" 29 | - result.systemout ShouldContainSubstring "{{.release}}" 30 | - name: 02 - cleanup 31 | steps: 32 | - script: | 33 | helm -n {{.namespace}} delete {{.release}} 34 | kubectl delete ns {{.namespace}} 35 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/03_test_env_var.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Environment Variable Interpolation 3 | vars: 4 | course: ../course_files/03_test_env_var.yaml 5 | namespace: 03-infra 6 | testcases: 7 | - name: 03 - plot without var 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} 11 | assertions: 12 | - result.code ShouldEqual 1 13 | - name: 03 - plot course with var 14 | steps: 15 | - script: | 16 | myvar=testing reckoner plot -a {{.course}} 17 | assertions: 18 | - result.code ShouldEqual 0 19 | - script: | 20 | helm -n {{.namespace}} ls 21 | assertions: 22 | - result.code ShouldEqual 0 23 | - result.systemout ShouldContainSubstring 'check-values' 24 | - name: 03 - check values 25 | steps: 26 | - script: | 27 | helm -n {{.namespace}} get values check-values -ojson | jq -e ".[\"non-used\"] == \"testing\"" 28 | assertions: 29 | - result.code ShouldEqual 0 30 | - name: 03 - cleanup 31 | steps: 32 | - script: | 33 | helm -n {{.namespace}} delete check-values 34 | kubectl delete ns {{.namespace}} 35 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/04_bad_chart.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Bad Chart 3 | vars: 4 | course: ../course_files/04_test_failed_chart.yaml 5 | namespace: 04-test 6 | release: bad-chart 7 | testcases: 8 | - name: 04 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 1 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldEqual 1 18 | - name: 04 - cleanup 19 | steps: 20 | - script: | 21 | helm -n {{.namespace}} delete {{.release}} || true 22 | kubectl delete ns {{.namespace}} || true 23 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/05_exit_post_install.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Exit on Failed Post Install 3 | vars: 4 | course: ../course_files/05_test_exit_on_post_install_hook.yaml 5 | namespace: 05-infra 6 | release: basic-demo 7 | testcases: 8 | - name: 05 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 1 14 | - name: 05 - cleanup 15 | steps: 16 | - script: | 17 | helm -n {{.namespace}} delete {{.release}} 18 | kubectl delete ns {{.namespace}} 19 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/06_exit_pre_install.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Exit on Failed Pre Install 3 | vars: 4 | course: ../course_files/06_test_exit_on_pre_install_hook.yaml 5 | namespace: 06-infra 6 | release: basic-demo 7 | testcases: 8 | - name: 06 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 1 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldEqual 1 18 | - name: 06 - cleanup 19 | steps: 20 | - script: | 21 | helm -n {{.namespace}} delete {{.release}} 22 | kubectl delete ns {{.namespace}} 23 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/07_good_hooks.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Successful Hooks 3 | vars: 4 | course: ../course_files/07_test_good_hooks.yaml 5 | namespace: 07-redis-test-namespace 6 | release: should-install 7 | testcases: 8 | - name: 07 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 0 14 | - result.systemerr ShouldContainSubstring "Running release pre hook" 15 | - result.systemerr ShouldContainSubstring "Running release post hook" 16 | - script: | 17 | helm -n {{.namespace}} get all {{.release}} 18 | assertions: 19 | - result.code ShouldEqual 0 20 | - name: 07 - cleanup 21 | steps: 22 | - script: | 23 | helm -n {{.namespace}} delete {{.release}} 24 | kubectl delete ns {{.namespace}} 25 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/08_multi_chart.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Multiple Charts in Multiple Namespaces 3 | vars: 4 | course: ../course_files/08_test_multi_chart.yaml 5 | testcases: 6 | - name: 08 - plot course 7 | steps: 8 | - script: | 9 | reckoner plot -a {{.course}} 10 | assertions: 11 | - result.code ShouldEqual 0 12 | - name: 08 - check first chart 13 | steps: 14 | - script: | 15 | helm -n 08-first get all first-chart 16 | assertions: 17 | - result.code ShouldEqual 0 18 | - name: 08 - check second chart 19 | steps: 20 | - script: | 21 | helm -n 08-second get all second-chart 22 | assertions: 23 | - result.code ShouldEqual 0 24 | - name: 08 - cleanup 25 | steps: 26 | - script: | 27 | helm -n 08-second delete second-chart 28 | helm -n 08-first delete first-chart 29 | kubectl delete ns 08-first 08-second 30 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/09_one_of_multi_chart.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: One of Multiple Charts 3 | vars: 4 | course: ../course_files/09_test_multi_chart.yaml 5 | testcases: 6 | - name: 09 - plot course -o first-chart-09 7 | steps: 8 | - script: | 9 | reckoner plot {{.course}} -o first-chart-09 10 | assertions: 11 | - result.code ShouldEqual 0 12 | - name: 09 - check first chart 13 | steps: 14 | - script: | 15 | helm -n 09-first get all first-chart-09 16 | assertions: 17 | - result.code ShouldEqual 0 18 | - name: 09 - check second chart 19 | steps: 20 | - script: | 21 | helm -n 09-second get all second-chart-09 22 | assertions: 23 | - result.code ShouldEqual 1 24 | - name: 09 - cleanup 25 | steps: 26 | - script: | 27 | helm -n 09-first delete first-chart-09 28 | kubectl delete ns 09-first 29 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/10_git_charts.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Charts from Git Repos 3 | vars: 4 | course: ../course_files/10_test_git_chart.yaml 5 | testcases: 6 | - name: 10 - plot course 7 | steps: 8 | - script: | 9 | reckoner plot -a {{.course}} 10 | assertions: 11 | - result.code ShouldEqual 0 12 | - script: | 13 | helm ls --all-namespaces 14 | assertions: 15 | - result.code ShouldEqual 0 16 | - result.systemout ShouldContainSubstring 'polaris' 17 | - result.systemout ShouldContainSubstring 'goldilocks-10' 18 | - result.systemout ShouldContainSubstring 'another-polaris' 19 | - result.systemout ShouldContainSubstring '10-test' 20 | - result.systemout ShouldContainSubstring 'polaris-release' 21 | - name: 10 - template course 22 | steps: 23 | - script: | 24 | reckoner template -a {{.course}} 25 | assertions: 26 | - result.code ShouldEqual 0 27 | - result.systemout ShouldContainSubstring 'goldilocks' 28 | - result.systemout ShouldContainSubstring 'polaris' 29 | - result.systemout ShouldContainSubstring 'another-polaris' 30 | - name: 10 - cleanup namespace creation 31 | steps: 32 | - script: | 33 | helm -n 10-test delete goldilocks-10 34 | helm -n 10-polaris delete polaris-release 35 | helm -n 10-another-polaris delete polaris 36 | kubectl delete ns 10-test 10-polaris 10-another-polaris 37 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/11_stop_after_first_failure.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Stop After First Failure 3 | vars: 4 | course: ../course_files/11_test_after_first_failure.yaml 5 | namespace: 11-test 6 | testcases: 7 | - name: 11 - test stop after first failure 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} 11 | assertions: 12 | - result.code ShouldEqual 1 13 | - script: | 14 | helm -n {{.namespace}} get all good-chart 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - script: | 18 | helm -n {{.namespace}} get all bad-chart 19 | assertions: 20 | - result.code ShouldEqual 1 21 | - script: | 22 | helm -n {{.namespace}} get all expected-skipped-chart 23 | assertions: 24 | - result.code ShouldEqual 1 25 | - name: 11 - cleanup 26 | steps: 27 | - script: | 28 | helm -n {{.namespace}} delete good-chart 29 | kubectl delete ns {{.namespace}} 30 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/12_continue_after_first_failure.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Continue After First Failure 3 | vars: 4 | course: ../course_files/12_test_after_first_failure.yaml 5 | namespace: 12-test 6 | testcases: 7 | - name: 12 - plot course 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} --continue-on-error 11 | assertions: 12 | - result.code ShouldEqual 1 13 | - script: | 14 | helm -n {{.namespace}} get all good-chart 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - script: | 18 | helm -n {{.namespace}} get all bad-chart 19 | assertions: 20 | - result.code ShouldEqual 1 21 | - script: | 22 | helm -n {{.namespace}} get all expected-skipped-chart 23 | assertions: 24 | - result.code ShouldEqual 0 25 | - name: 12 - cleanup 26 | steps: 27 | - script: | 28 | helm -n {{.namespace}} delete good-chart expected-skipped-chart 29 | kubectl delete ns {{.namespace}} 30 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/13_schema.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Bad Chart 3 | vars: 4 | namespace: 13-test 5 | testcases: 6 | - name: 13 - plot 13_test_bad_schema_repository.yaml 7 | steps: 8 | - script: | 9 | reckoner plot -a ../course_files/13_test_bad_schema_repository.yaml 10 | assertions: 11 | - result.code ShouldEqual 1 12 | - result.systemerr ShouldContainSubstring 'Course file has schema validation errors' 13 | - name: 13 - plot 13_test_required_schema.yaml 14 | steps: 15 | - script: | 16 | reckoner plot -a ../course_files/13_test_required_schema.yaml 17 | assertions: 18 | - result.code ShouldEqual 1 19 | - result.systemerr ShouldContainSubstring 'Course file has schema validation errors' 20 | - name: 13 - lint 13_test_lint_bad_secret.yaml 21 | steps: 22 | - script: | 23 | reckoner lint ../course_files/13_test_lint_bad_secret.yaml 24 | assertions: 25 | - result.code ShouldEqual 1 26 | - result.systemerr ShouldContainSubstring 'Course file has schema validation errors' 27 | 28 | - name: 13 - lint 13_test_lint_good_secret.yaml 29 | steps: 30 | - script: | 31 | reckoner lint ../course_files/13_test_lint_good_secret.yaml 32 | assertions: 33 | - result.code ShouldEqual 0 34 | - result.systemerr ShouldContainSubstring 'No schema validation errors found' 35 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/14_folders.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Files in Folders 3 | vars: 4 | course: ../course_files/testing_in_folder/test_files_in_folders.yaml 5 | namespace: 14-infra 6 | release: chart-one 7 | testcases: 8 | - name: 14 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 0 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldEqual 0 18 | - script: | 19 | helm -n {{.namespace}} get values {{.release}} -ojson | jq -e ".[\"new_key\"] == \"new_value\"" 20 | assertions: 21 | - result.code ShouldEqual 0 22 | - name: 14 - cleanup 23 | steps: 24 | - script: | 25 | helm -n {{.namespace}} delete {{.release}} 26 | kubectl delete ns {{.namespace}} 27 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/15_namespace_management.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Default Namespace Management 3 | vars: 4 | course: ../course_files/15_test_default_namespace_annotation_and_labels.yaml 5 | namespace: 15-annotatednamespace 6 | release: namespace-test 7 | testcases: 8 | - name: 15 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 0 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldEqual 0 18 | - script: | 19 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.annotations[\"reckoner\"] == \"rocks\"" 20 | assertions: 21 | - result.code ShouldEqual 0 22 | - script: | 23 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.labels[\"rocks\"] == \"reckoner\"" 24 | assertions: 25 | - result.code ShouldEqual 0 26 | - script: | 27 | kubectl get ns {{.namespace}}-2 -ojson | jq -e ".metadata.annotations[\"this\"] == \"exists\"" 28 | assertions: 29 | - result.code ShouldEqual 0 30 | - script: | 31 | kubectl get ns {{.namespace}}-2 -ojson | jq -e ".metadata.labels[\"this\"] == \"alsoexists\"" 32 | assertions: 33 | - result.code ShouldEqual 0 34 | - script: | 35 | kubectl get ns {{.namespace}}-2 -ojson | jq -e ".metadata.annotations[\"reckoner\"] == \"rocks\"" 36 | assertions: 37 | - result.code ShouldEqual 0 38 | - script: | 39 | kubectl get ns {{.namespace}}-2 -ojson | jq -e ".metadata.labels[\"rocks\"] == \"reckoner\"" 40 | assertions: 41 | - result.code ShouldEqual 0 42 | - name: 15 - cleanup 43 | steps: 44 | - script: | 45 | kubectl delete ns {{.namespace}} {{.namespace}}-2 46 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/16_namespace_management_overwrite.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Namespace Management Overwrite 3 | vars: 4 | course: ../course_files/16_test_overwrite_namespace_annotation_and_labels.yaml 5 | namespace: 16-annotatednamespace 6 | release: namespace-test 7 | testcases: 8 | - name: 16 - plot course 9 | steps: 10 | - script: | 11 | kubectl create ns {{.namespace}} 12 | kubectl annotate ns {{.namespace}} reckoner=overwriteme 13 | kubectl label ns {{.namespace}} rocks=overwriteme 14 | reckoner plot -a {{.course}} 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - script: | 18 | helm -n {{.namespace}} get all {{.release}} 19 | assertions: 20 | - result.code ShouldEqual 0 21 | - script: | 22 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.annotations[\"reckoner\"] == \"doesnotrock\"" 23 | assertions: 24 | - result.code ShouldEqual 0 25 | - script: | 26 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.labels[\"rocks\"] == \"reckonerstill\"" 27 | assertions: 28 | - result.code ShouldEqual 0 29 | - name: 16 - cleanup 30 | steps: 31 | - script: | 32 | helm -n {{.namespace}} delete {{.release}} 33 | kubectl delete ns {{.namespace}} 34 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/17_namespace_managment_dont_overwrite.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Namespace Management Overwrite 3 | vars: 4 | course: ../course_files/17_test_dont_overwrite_ns_meta.yaml 5 | namespace: 17-annotatednamespace 6 | release: namespace-test 7 | testcases: 8 | - name: 17 - plot course 9 | steps: 10 | - script: | 11 | kubectl create ns {{.namespace}} 12 | kubectl annotate ns {{.namespace}} reckoner=dontoverwriteme 13 | kubectl label ns {{.namespace}} rocks=dontoverwriteme 14 | reckoner plot -a {{.course}} 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - script: | 18 | helm -n {{.namespace}} get all {{.release}} 19 | assertions: 20 | - result.code ShouldEqual 0 21 | - script: | 22 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.annotations[\"reckoner\"] == \"doesnotrock\"" 23 | assertions: 24 | - result.code ShouldEqual 1 25 | - script: | 26 | kubectl get ns {{.namespace}} -ojson | jq -e ".metadata.labels[\"rocks\"] == \"reckonerstill\"" 27 | assertions: 28 | - result.code ShouldEqual 1 29 | - name: 17 - cleanup 30 | steps: 31 | - script: | 32 | helm -n {{.namespace}} delete {{.release}} 33 | kubectl delete ns {{.namespace}} 34 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/18_strong_typing.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Strong Typing 3 | vars: 4 | course: ../course_files/18_strong_typing.yaml 5 | namespace: 18-testing 6 | testcases: 7 | - name: 18 - plot course 8 | steps: 9 | - script: | 10 | yes_var=yes true_var=true false_var=false int_var=123 float_var=1.234 reckoner plot {{.course}} -a 2> /dev/null 11 | assertions: 12 | - result.code ShouldEqual 0 13 | - name: 18 - check values 14 | steps: 15 | - script: | 16 | set -e 17 | charts="$(helm ls --namespace {{.namespace}} --output json | jq -r '.[].name')" 18 | for _release_install in ${charts}; do 19 | values="$(helm get values --namespace {{.namespace}} "${_release_install}" --output json | jq -e -r 'keys|.[]')" 20 | for key in ${values}; do 21 | helm -n {{.namespace}} get values ${_release_install} -ojson | jq ". | has(\"${key}\")" > /dev/null 22 | # Check type of key found in json 23 | case "${key}" in 24 | expect-float*) 25 | _expected_type="number" 26 | ;; 27 | expect-number*) 28 | _expected_type="number" 29 | ;; 30 | expect-string*) 31 | _expected_type="string" 32 | ;; 33 | expect-bool*) 34 | _expected_type="boolean" 35 | ;; 36 | expect-null*) 37 | _expected_type="null" 38 | ;; 39 | *) 40 | exit 1 41 | ;; 42 | esac 43 | helm -n {{.namespace}} get values ${_release_install} -ojson | jq -e ".[\"${key}\"] | type == \"${_expected_type}\"" > /dev/null 44 | done 45 | done 46 | assertions: 47 | - result.code ShouldEqual 0 48 | - name: 18 - cleanup 49 | steps: 50 | - script: | 51 | kubectl delete ns {{.namespace}} 52 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/20_strong_ordering.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Strong Ordering 3 | vars: 4 | course: ../course_files/20_test_strong_ordering.yaml 5 | namespace: 20-test 6 | testcases: 7 | - name: 20 - plot course 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} 11 | assertions: 12 | - result.code ShouldEqual 0 13 | - script: | 14 | helm -n {{.namespace}} get all first-chart 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - script: | 18 | helm -n {{.namespace}} get all second-chart 19 | assertions: 20 | - result.code ShouldEqual 0 21 | - name: 20 - check install timestamps 22 | steps: 23 | - script: | 24 | first_chart_timestamp="$(helm status first-chart -n {{.namespace}} -ojson | jq .info.last_deployed -r | cut -c -19 | jq --raw-input 'strptime("%Y-%m-%dT%H:%M:%S")|mktime')" 25 | second_chart_timestamp="$(helm status second-chart -n {{.namespace}} -ojson | jq .info.last_deployed -r | cut -c -19 | jq --raw-input 'strptime("%Y-%m-%dT%H:%M:%S")|mktime')" 26 | echo "first_chart_timestamp: ${first_chart_timestamp}" 27 | echo "second_chart_timestamp: ${second_chart_timestamp}" 28 | test $((first_chart_timestamp-second_chart_timestamp)) -lt 0 29 | assertions: 30 | - result.code ShouldEqual 0 31 | - name: 20 - cleanup 32 | steps: 33 | - script: | 34 | helm -n {{.namespace}} delete first-chart second-chart 35 | kubectl delete ns {{.namespace}} 36 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/21_diff.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Diff 3 | vars: 4 | course: ../course_files/21_test_diff.yaml 5 | namespace: 21-test 6 | testcases: 7 | - name: 21 - plot course 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} 11 | assertions: 12 | - result.code ShouldEqual 0 13 | - script: | 14 | reckoner diff {{.course}} -o chart-with-namespace 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - result.systemerr ShouldContainSubstring "There are no differences in release" 18 | - script: | 19 | reckoner diff {{.course}} -o chart-without-namespace 20 | assertions: 21 | - result.code ShouldEqual 0 22 | - result.systemerr ShouldContainSubstring "There are no differences in release" 23 | - name: 21 - cleanup 24 | steps: 25 | - script: | 26 | helm -n {{.namespace}} delete chart-with-namespace 27 | helm -n {{.namespace}}-diff delete chart-without-namespace 28 | kubectl delete ns {{.namespace}} 29 | kubectl delete ns {{.namespace}}-diff 30 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/22_test_relative_hooks.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Relative Hook Location 3 | vars: 4 | course: ../course_files/testing_in_folder/testing_in_subfolder/22_testing_relative_hook_location.yaml 5 | namespace: 22-infra 6 | testcases: 7 | - name: 22 - plot course 8 | steps: 9 | - script: | 10 | reckoner plot -a {{.course}} 11 | assertions: 12 | - result.code ShouldEqual 0 13 | - script: | 14 | helm -n {{.namespace}} get all test-relative-hooks 15 | assertions: 16 | - result.code ShouldEqual 0 17 | - name: 22 - cleanup 18 | steps: 19 | - script: | 20 | helm -n {{.namespace}} delete test-relative-hooks 21 | kubectl delete ns {{.namespace}} relative-hook 22 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/23_chart_namespace_environment_variable.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Chart Namespace as Environment Variable 3 | vars: 4 | course: ../course_files/23_test_environment_variable_namespace_on_chart.yaml 5 | namespace: 23-test-environment-namespace 6 | release: env-var-name-space 7 | testcases: 8 | - name: 23 - plot course 9 | steps: 10 | - script: | 11 | export TEST_ENVIRONMENT_NAMESPACE={{.namespace}} 12 | reckoner plot {{.course}} -o {{.release}} 13 | assertions: 14 | - result.code ShouldEqual 0 15 | - script: | 16 | helm -n {{.namespace}} get all {{.release}} 17 | assertions: 18 | - result.code ShouldEqual 0 19 | - name: 23 - cleanup 20 | steps: 21 | - script: | 22 | helm -n {{.namespace}} delete {{.release}} 23 | kubectl delete ns {{.namespace}} 24 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/24_no_context.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: No Context 3 | vars: 4 | course: ../course_files/24_test_no_context.yaml 5 | namespace: 24-test-no-context 6 | testcases: 7 | - name: 24 - get-manifests 8 | steps: 9 | - script: | 10 | reckoner get-manifests {{.course}} -o chart-with-no-context 11 | assertions: 12 | - result.code ShouldEqual 1 13 | - name: 24 - plot 14 | steps: 15 | - script: | 16 | reckoner plot {{.course}} -o chart-with-no-context 17 | assertions: 18 | - result.code ShouldEqual 1 19 | - name: 24 - diff 20 | steps: 21 | - script: | 22 | reckoner diff {{.course}} -o chart-with-no-context 23 | assertions: 24 | - result.code ShouldEqual 1 25 | - name: 24 - template 26 | steps: 27 | - script: | 28 | reckoner template {{.course}} -o chart-with-no-context 29 | assertions: 30 | - result.code ShouldEqual 0 31 | - name: 24 - cleanup 32 | steps: 33 | - script: | 34 | # We shouldn't need this, but if for some reason the tests above don't pass, the NS may get created. Mainly needed for local tests. 35 | kubectl delete ns {{.namespace}} || true 36 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/25_import.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Shell Executor Secret 3 | vars: 4 | course: ../course_files/25_test_import.yaml 5 | namespace: 25-test-import 6 | release: import-test 7 | testcases: 8 | - name: 25 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 0 14 | - name: 25 - import release 15 | steps: 16 | - script: | 17 | reckoner import --namespace {{.namespace}} --release_name {{.release}} --repository fairwinds-incubator 18 | assertions: 19 | - "result.code ShouldEqual 0" 20 | - 'result.systemout ShouldContainSubstring "namespace: {{.namespace}}"' 21 | - 'result.systemout ShouldContainSubstring "version: 1.0.0"' 22 | - 'result.systemout ShouldContainSubstring "chart: basic-demo"' 23 | - 'result.systemout ShouldContainSubstring "name: {{.release}}"' 24 | - 'result.systemout ShouldContainSubstring "expect-value: 1.05"' 25 | - 'result.systemout ShouldContainSubstring "expect-bool-value: false"' 26 | - name: 25 - cleanup 27 | steps: 28 | - script: | 29 | helm -n {{.namespace}} delete 30 | kubectl delete ns {{.namespace}} 31 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/26_shell_executor_secret.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Shell Executor Secret 3 | vars: 4 | course: ../course_files/26_test_shell_executor_secret.yaml 5 | namespace: 26-shell-executor 6 | release: shell-executor-chart 7 | testcases: 8 | - name: 26 - plot course 9 | steps: 10 | - script: | 11 | reckoner plot -a {{.course}} 12 | assertions: 13 | - result.code ShouldEqual 0 14 | - script: | 15 | helm -n {{.namespace}} get all {{.release}} 16 | assertions: 17 | - result.code ShouldEqual 0 18 | - script: | 19 | helm -n {{.namespace}} get values {{.release}} 20 | assertions: 21 | - result.code ShouldEqual 0 22 | - result.systemout ShouldContainSubstring THISVALUEISSECRET 23 | - name: 26 - cleanup 24 | steps: 25 | - script: | 26 | helm -n {{.namespace}} delete {{.release}} 27 | kubectl delete ns {{.namespace}} 28 | -------------------------------------------------------------------------------- /end_to_end_testing/tests/27_gitops.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | name: Shell Executor Secret 3 | vars: 4 | course: ../course_files/27_gitops.yaml 5 | namespace: 27-gitops 6 | release: shell-executor-chart 7 | testcases: 8 | - name: 27 - template course 9 | steps: 10 | - script: | 11 | reckoner template -a {{.course}} --output-dir 27_gitops_output 12 | assertions: 13 | - result.code ShouldEqual 0 14 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/fairwindsops/reckoner 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/Masterminds/semver/v3 v3.2.0 7 | github.com/blang/semver v3.5.1+incompatible 8 | github.com/davecgh/go-spew v1.1.1 9 | github.com/fatih/color v1.14.1 10 | github.com/go-git/go-git/v5 v5.6.0 11 | github.com/gookit/color v1.5.2 12 | github.com/imdario/mergo v0.3.13 13 | github.com/mattn/go-colorable v0.1.13 14 | github.com/rhysd/go-github-selfupdate v1.2.3 15 | github.com/sergi/go-diff v1.3.1 16 | github.com/spf13/cobra v1.6.1 17 | github.com/spf13/pflag v1.0.5 18 | github.com/stretchr/testify v1.8.2 19 | github.com/thoas/go-funk v0.9.3 20 | github.com/xeipuuv/gojsonschema v1.2.0 21 | gopkg.in/yaml.v3 v3.0.1 22 | k8s.io/api v0.26.2 23 | k8s.io/apimachinery v0.26.2 24 | k8s.io/client-go v0.26.2 25 | k8s.io/klog/v2 v2.90.1 26 | sigs.k8s.io/controller-runtime v0.14.5 27 | ) 28 | 29 | require ( 30 | github.com/Microsoft/go-winio v0.6.0 // indirect 31 | github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect 32 | github.com/acomagu/bufpipe v1.0.4 // indirect 33 | github.com/cloudflare/circl v1.3.2 // indirect 34 | github.com/emicklei/go-restful/v3 v3.10.1 // indirect 35 | github.com/emirpasic/gods v1.18.1 // indirect 36 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 37 | github.com/go-git/gcfg v1.5.0 // indirect 38 | github.com/go-git/go-billy/v5 v5.4.1 // indirect 39 | github.com/go-logr/logr v1.2.3 // indirect 40 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 41 | github.com/go-openapi/jsonreference v0.20.2 // indirect 42 | github.com/go-openapi/swag v0.22.3 // indirect 43 | github.com/gogo/protobuf v1.3.2 // indirect 44 | github.com/golang/protobuf v1.5.2 // indirect 45 | github.com/google/gnostic v0.6.9 // indirect 46 | github.com/google/go-cmp v0.5.9 // indirect 47 | github.com/google/go-github/v30 v30.1.0 // indirect 48 | github.com/google/go-querystring v1.1.0 // indirect 49 | github.com/google/gofuzz v1.2.0 // indirect 50 | github.com/inconshreveable/go-update v0.0.0-20160112193335-8152e7eb6ccf // indirect 51 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 52 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 53 | github.com/josharian/intern v1.0.0 // indirect 54 | github.com/json-iterator/go v1.1.12 // indirect 55 | github.com/kevinburke/ssh_config v1.2.0 // indirect 56 | github.com/mailru/easyjson v0.7.7 // indirect 57 | github.com/mattn/go-isatty v0.0.17 // indirect 58 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 59 | github.com/modern-go/reflect2 v1.0.2 // indirect 60 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 61 | github.com/pjbgf/sha1cd v0.3.0 // indirect 62 | github.com/pkg/errors v0.9.1 // indirect 63 | github.com/pmezard/go-difflib v1.0.0 // indirect 64 | github.com/skeema/knownhosts v1.1.0 // indirect 65 | github.com/tcnksm/go-gitconfig v0.1.2 // indirect 66 | github.com/ulikunitz/xz v0.5.11 // indirect 67 | github.com/xanzy/ssh-agent v0.3.3 // indirect 68 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 69 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 70 | github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect 71 | golang.org/x/crypto v0.7.0 // indirect 72 | golang.org/x/mod v0.9.0 // indirect 73 | golang.org/x/net v0.8.0 // indirect 74 | golang.org/x/oauth2 v0.6.0 // indirect 75 | golang.org/x/sys v0.6.0 // indirect 76 | golang.org/x/term v0.6.0 // indirect 77 | golang.org/x/text v0.8.0 // indirect 78 | golang.org/x/time v0.3.0 // indirect 79 | golang.org/x/tools v0.6.0 // indirect 80 | google.golang.org/appengine v1.6.7 // indirect 81 | google.golang.org/protobuf v1.28.1 // indirect 82 | gopkg.in/inf.v0 v0.9.1 // indirect 83 | gopkg.in/warnings.v0 v0.1.2 // indirect 84 | gopkg.in/yaml.v2 v2.4.0 // indirect 85 | k8s.io/kube-openapi v0.0.0-20230303024457-afdc3dddf62d // indirect 86 | k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect 87 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 88 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 89 | sigs.k8s.io/yaml v1.3.0 // indirect 90 | ) 91 | -------------------------------------------------------------------------------- /img/reckoner-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /img/reckoner-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 6 | 7 | 24 | 27 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License. 12 | 13 | package main 14 | 15 | import ( 16 | _ "embed" 17 | 18 | "github.com/fairwindsops/reckoner/cmd" 19 | ) 20 | 21 | var ( 22 | // version is set during build 23 | version = "0.0.0" 24 | // commit is set during build 25 | commit = "n/a" 26 | //go:embed pkg/course/coursev2.schema.json 27 | courseSchema []byte 28 | ) 29 | 30 | func main() { 31 | cmd.Execute(version, commit, courseSchema) 32 | } 33 | -------------------------------------------------------------------------------- /pkg/course/argocd.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package course 14 | 15 | // We may use these imports to track ArgoCD Applications exactly. However, 16 | // this also pulls in kubernetes api packages, which makes reckoner rely 17 | // on particular versions of kubernetes. At the time of writing, no such 18 | // relationship exists. Once we've firmly chosen a path, this comment 19 | // should be removed, and potentially this entire file. 20 | // import ( 21 | // argoAppv1alpha1 "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 22 | // metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 23 | // ) 24 | 25 | type ArgoApplicationSpecSyncPolicyAutomated struct { 26 | Prune bool `yaml:"prune,omitempty"` 27 | } 28 | 29 | type ArgoApplicationSpecSyncPolicy struct { 30 | Automated ArgoApplicationSpecSyncPolicyAutomated `yaml:"automated,omitempty"` 31 | Options []string `yaml:"syncOptions,omitempty"` 32 | } 33 | 34 | type ArgoApplicationSpecSourceDirectory struct { 35 | Recurse bool `yaml:"recurse,omitempty"` 36 | } 37 | 38 | type ArgoApplicationSpecSource struct { 39 | Directory ArgoApplicationSpecSourceDirectory `yaml:"directory,omitempty"` 40 | Path string `yaml:"path"` 41 | RepoURL string `yaml:"repoURL"` 42 | } 43 | 44 | type ArgoApplicationSpecDestination struct { 45 | Server string `yaml:"server,omitempty"` 46 | Namespace string `yaml:"namespace,omitempty"` 47 | } 48 | 49 | type ArgoApplicationSpec struct { 50 | Source ArgoApplicationSpecSource `yaml:"source"` 51 | Destination ArgoApplicationSpecDestination `yaml:"destination"` 52 | Project string `yaml:"project"` 53 | SyncPolicy ArgoApplicationSpecSyncPolicy `yaml:"syncPolicy,omitempty"` 54 | IgnoreDifferences []ArgoResourceIgnoreDifferences `yaml:"ignoreDifferences,omitempty"` 55 | } 56 | 57 | // ArgoApplicationMetadata contains the k8s metadata for the gitops agent CustomResource. 58 | // This is the resource/manifest/config the agent will read in, not the resources deployed by the agent. 59 | type ArgoApplicationMetadata struct { 60 | Name string `yaml:"name"` 61 | Namespace string `yaml:"namespace,omitempty"` 62 | Annotations map[string]string `yaml:"annotations,omitempty"` 63 | Labels map[string]string `yaml:"labels,omitempty"` 64 | } 65 | 66 | type ArgoApplication struct { 67 | Kind string `yaml:"kind"` 68 | APIVersion string `yaml:"apiVersion"` 69 | Metadata ArgoApplicationMetadata `yaml:"metadata"` 70 | Spec ArgoApplicationSpec `yaml:"spec"` 71 | } 72 | 73 | // ResourceIgnoreDifferences contains resource filter and list of json paths which should be ignored during comparison with live state. 74 | type ArgoResourceIgnoreDifferences struct { 75 | Group string `yaml:"group,omitempty"` 76 | Kind string `yaml:"kind"` 77 | Name string `yaml:"name,omitempty"` 78 | Namespace string `yaml:"namespace,omitempty"` 79 | JSONPointers []string `yaml:"jsonPointers,omitempty"` 80 | JQPathExpressions []string `yaml:"jqPathExpressions,omitempty"` 81 | // ManagedFieldsManagers is a list of trusted managers. Fields mutated by those managers will take precedence over the 82 | // desired state defined in the SCM and won't be displayed in diffs 83 | ManagedFieldsManagers []string `yaml:"managedFieldsManagers,omitempty"` 84 | } 85 | -------------------------------------------------------------------------------- /pkg/course/coursev2.schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json-schema.org/draft/2020-12/schema", 3 | "definitions": { 4 | "repository": { 5 | "type": "object", 6 | "additionalProperties": false, 7 | "properties": { 8 | "url": { 9 | "type": "string" 10 | }, 11 | "path": { 12 | "type": "string" 13 | }, 14 | "git": { 15 | "type": "string" 16 | }, 17 | "name": { 18 | "type": "string" 19 | } 20 | } 21 | }, 22 | "hooks": { 23 | "type": "object", 24 | "additionalProperties": false, 25 | "properties": { 26 | "init": { 27 | "oneOf": [ 28 | { 29 | "type": "array" 30 | }, 31 | { 32 | "type": "string" 33 | } 34 | ] 35 | }, 36 | "post_install": { 37 | "type": "array" 38 | }, 39 | "pre_install": { 40 | "type": "array" 41 | } 42 | } 43 | }, 44 | "gitops": { 45 | "type": "object" 46 | }, 47 | "release": { 48 | "type": "array", 49 | "additionalProperties": { 50 | "type": "object", 51 | "additionalProperties": false, 52 | "properties": { 53 | "name": { 54 | "type": "string", 55 | "pattern": "^[a-zA-Z0-9_-]{1,63}$", 56 | "x-custom-error-message": "Chart release names must be alphanumeric with \"_\" and \"-\" and be between 1 and 63 characters" 57 | }, 58 | "namespace": { 59 | "type": "string" 60 | }, 61 | "namespace_management": { 62 | "type": "object", 63 | "additionalProperties": false, 64 | "properties": { 65 | "metadata": { 66 | "type": "object", 67 | "additionalProperties": false, 68 | "properties": { 69 | "annotations": { 70 | "type": "object" 71 | }, 72 | "labels": { 73 | "type": "object" 74 | } 75 | } 76 | }, 77 | "settings": { 78 | "type": "object" 79 | } 80 | } 81 | }, 82 | "chart": { 83 | "type": "string" 84 | }, 85 | "repository": { 86 | "oneOf": [ 87 | { 88 | "type": "string" 89 | }, 90 | { 91 | "$ref": "#/definitions/repository" 92 | } 93 | ], 94 | "x-custom-error-message": "Problem Parsing Repositories Schema; expecting string or map" 95 | }, 96 | "version": { 97 | "type": "string" 98 | }, 99 | "hooks": { 100 | "$ref": "#/definitions/hooks" 101 | }, 102 | "plugin": { 103 | "type": "string" 104 | }, 105 | "files": { 106 | "type": "array", 107 | "items": { 108 | "type": "string" 109 | } 110 | }, 111 | "values": { 112 | "type": "object" 113 | } 114 | }, 115 | "x-custom-error-message": "Problem Parsing Chart Schema" 116 | } 117 | } 118 | }, 119 | "type": "object", 120 | "additionalProperties": false, 121 | "properties": { 122 | "schema": { 123 | "type": "string", 124 | "additionalProperties": false 125 | }, 126 | "_references": { 127 | "type": "object" 128 | }, 129 | "namespace": { 130 | "type": "string" 131 | }, 132 | "namespace_management": { 133 | "type": "object", 134 | "additionalProperties": false, 135 | "properties": { 136 | "default": { 137 | "type": "object", 138 | "additionalProperties": false, 139 | "properties": { 140 | "metadata": { 141 | "type": "object", 142 | "additionalProperties": false, 143 | "properties": { 144 | "annotations": { 145 | "type": "object" 146 | }, 147 | "labels": { 148 | "type": "object" 149 | } 150 | } 151 | }, 152 | "settings": { 153 | "type": "object" 154 | } 155 | } 156 | } 157 | } 158 | }, 159 | "releases": { 160 | "$ref": "#/definitions/release" 161 | }, 162 | "hooks": { 163 | "$ref": "#/definitions/hooks" 164 | }, 165 | "gitops": { 166 | "$ref": "#/definitions/gitops" 167 | }, 168 | "minimum_versions": { 169 | "type": "object", 170 | "additionalProperties": false, 171 | "properties": { 172 | "helm": { 173 | "type": "string" 174 | }, 175 | "reckoner": { 176 | "type": "string" 177 | } 178 | } 179 | }, 180 | "repositories": { 181 | "type": "object", 182 | "additionalProperties": { 183 | "$ref": "#/definitions/repository" 184 | } 185 | }, 186 | "repository": { 187 | "type": "string" 188 | }, 189 | "context": { 190 | "type": "string" 191 | }, 192 | "helm_args": { 193 | "type": "array", 194 | "items": { 195 | "type": "string" 196 | } 197 | }, 198 | "secrets": { 199 | "type": "array", 200 | "items": { 201 | "type": "object", 202 | "additionalProperties": true, 203 | "properties": { 204 | "backend": { 205 | "type": "string" 206 | }, 207 | "name": { 208 | "type": "string" 209 | } 210 | }, 211 | "required": [ 212 | "backend", 213 | "name" 214 | ] 215 | } 216 | } 217 | }, 218 | "required": [ 219 | "namespace", 220 | "releases" 221 | ] 222 | } -------------------------------------------------------------------------------- /pkg/course/namespace.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package course 14 | 15 | import ( 16 | "k8s.io/klog/v2" 17 | ) 18 | 19 | // populateNamespaceManagement populates each release with the default namespace management settings if they are not set 20 | func (f *FileV2) populateNamespaceManagement() { 21 | var emptyNamespaceMgmt NamespaceConfig 22 | if f.NamespaceMgmt == nil { 23 | f.NamespaceMgmt = &NamespaceMgmt{} 24 | } 25 | if f.NamespaceMgmt.Default == nil { 26 | f.NamespaceMgmt.Default = &emptyNamespaceMgmt 27 | f.NamespaceMgmt.Default.Settings.Overwrite = boolPtr(false) 28 | } else if f.NamespaceMgmt.Default.Settings.Overwrite == nil { 29 | f.NamespaceMgmt.Default.Settings.Overwrite = boolPtr(false) 30 | } 31 | 32 | for releaseIndex, release := range f.Releases { 33 | newRelease := *release 34 | if newRelease.NamespaceMgmt == nil { 35 | klog.V(5).Infof("using default namespace management for release: %s", release.Name) 36 | newRelease.NamespaceMgmt = f.NamespaceMgmt.Default 37 | } else { 38 | newRelease.NamespaceMgmt = mergeNamespaceManagement(*f.NamespaceMgmt.Default, *newRelease.NamespaceMgmt) 39 | 40 | } 41 | f.Releases[releaseIndex] = &newRelease 42 | } 43 | } 44 | 45 | // mergeNamespaceManagement merges the default namespace management settings with the release specific settings 46 | func mergeNamespaceManagement(defaults NamespaceConfig, mergeInto NamespaceConfig) *NamespaceConfig { 47 | for k, v := range defaults.Metadata.Annotations { 48 | if mergeInto.Metadata.Annotations == nil { 49 | mergeInto.Metadata.Annotations = map[string]string{} 50 | } 51 | if mergeInto.Metadata.Annotations[k] == "" { 52 | mergeInto.Metadata.Annotations[k] = v 53 | } 54 | } 55 | 56 | for k, v := range defaults.Metadata.Labels { 57 | if mergeInto.Metadata.Labels == nil { 58 | mergeInto.Metadata.Labels = map[string]string{} 59 | } 60 | if mergeInto.Metadata.Labels[k] == "" { 61 | mergeInto.Metadata.Labels[k] = v 62 | } 63 | } 64 | 65 | if mergeInto.Settings.Overwrite == nil { 66 | mergeInto.Settings.Overwrite = defaults.Settings.Overwrite 67 | } 68 | 69 | return &mergeInto 70 | } 71 | -------------------------------------------------------------------------------- /pkg/course/namespace_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package course 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func Test_mergeNamespaceManagement(t *testing.T) { 22 | type args struct { 23 | defaults NamespaceConfig 24 | mergeInto NamespaceConfig 25 | } 26 | tests := []struct { 27 | name string 28 | args args 29 | want *NamespaceConfig 30 | }{ 31 | { 32 | name: "basic merge", 33 | args: args{ 34 | defaults: NamespaceConfig{ 35 | Metadata: NSMetadata{ 36 | Annotations: map[string]string{ 37 | "default-annotation": "default-value", 38 | }, 39 | Labels: map[string]string{ 40 | "default-label": "default-value", 41 | }, 42 | }, 43 | Settings: NSSettings{ 44 | Overwrite: boolPtr(false), 45 | }, 46 | }, 47 | mergeInto: NamespaceConfig{ 48 | Metadata: NSMetadata{ 49 | Annotations: map[string]string{ 50 | "merge-annotation": "merge-value", 51 | }, 52 | Labels: map[string]string{ 53 | "merge-label": "merge-value", 54 | }, 55 | }, 56 | Settings: NSSettings{ 57 | Overwrite: boolPtr(false), 58 | }, 59 | }, 60 | }, 61 | want: &NamespaceConfig{ 62 | Metadata: NSMetadata{ 63 | Annotations: map[string]string{ 64 | "default-annotation": "default-value", 65 | "merge-annotation": "merge-value", 66 | }, 67 | Labels: map[string]string{ 68 | "default-label": "default-value", 69 | "merge-label": "merge-value", 70 | }, 71 | }, 72 | Settings: NSSettings{ 73 | Overwrite: boolPtr(false), 74 | }, 75 | }, 76 | }, 77 | } 78 | for _, tt := range tests { 79 | t.Run(tt.name, func(t *testing.T) { 80 | got := mergeNamespaceManagement(tt.args.defaults, tt.args.mergeInto) 81 | assert.EqualValues(t, tt.want, got) 82 | }) 83 | } 84 | } 85 | 86 | func TestFileV2_populateNamespaceManagement(t *testing.T) { 87 | tests := []struct { 88 | name string 89 | file *FileV2 90 | want *FileV2 91 | }{ 92 | { 93 | name: "empty default", 94 | file: &FileV2{ 95 | Releases: []*Release{}, 96 | NamespaceMgmt: &NamespaceMgmt{}, 97 | }, 98 | want: &FileV2{ 99 | NamespaceMgmt: &NamespaceMgmt{ 100 | Default: &NamespaceConfig{ 101 | Settings: NSSettings{ 102 | Overwrite: boolPtr(false), 103 | }, 104 | }, 105 | }, 106 | Releases: []*Release{}, 107 | }, 108 | }, 109 | { 110 | name: "empty default overwrite", 111 | file: &FileV2{ 112 | Releases: []*Release{}, 113 | NamespaceMgmt: &NamespaceMgmt{ 114 | Default: &NamespaceConfig{ 115 | Settings: NSSettings{ 116 | Overwrite: nil, 117 | }, 118 | }, 119 | }, 120 | }, 121 | want: &FileV2{ 122 | NamespaceMgmt: &NamespaceMgmt{ 123 | Default: &NamespaceConfig{ 124 | Settings: NSSettings{ 125 | Overwrite: boolPtr(false), 126 | }, 127 | }, 128 | }, 129 | Releases: []*Release{}, 130 | }, 131 | }, 132 | { 133 | name: "release default", 134 | file: &FileV2{ 135 | Releases: []*Release{ 136 | { 137 | Name: "default", 138 | }, 139 | }, 140 | NamespaceMgmt: &NamespaceMgmt{ 141 | Default: nil, 142 | }, 143 | }, 144 | want: &FileV2{ 145 | NamespaceMgmt: &NamespaceMgmt{ 146 | Default: &NamespaceConfig{ 147 | Settings: NSSettings{ 148 | Overwrite: boolPtr(false), 149 | }, 150 | }, 151 | }, 152 | Releases: []*Release{ 153 | { 154 | Name: "default", 155 | NamespaceMgmt: &NamespaceConfig{ 156 | Settings: NSSettings{ 157 | Overwrite: boolPtr(false), 158 | }, 159 | }, 160 | }, 161 | }, 162 | }, 163 | }, 164 | { 165 | name: "release specific", 166 | file: &FileV2{ 167 | Releases: []*Release{ 168 | { 169 | Name: "default", 170 | NamespaceMgmt: &NamespaceConfig{ 171 | Settings: NSSettings{ 172 | Overwrite: boolPtr(false), 173 | }, 174 | Metadata: NSMetadata{ 175 | Annotations: map[string]string{ 176 | "release-annotation": "release-value", 177 | }, 178 | Labels: map[string]string{ 179 | "release-label": "release-value", 180 | }, 181 | }, 182 | }, 183 | }, 184 | { 185 | Name: "release2", 186 | NamespaceMgmt: &NamespaceConfig{}, 187 | }, 188 | }, 189 | NamespaceMgmt: &NamespaceMgmt{ 190 | Default: &NamespaceConfig{ 191 | Settings: NSSettings{}, 192 | Metadata: NSMetadata{ 193 | Annotations: map[string]string{ 194 | "course-annotation": "course-value", 195 | }, 196 | Labels: map[string]string{ 197 | "course-label": "course-value", 198 | }, 199 | }, 200 | }, 201 | }, 202 | }, 203 | want: &FileV2{ 204 | NamespaceMgmt: &NamespaceMgmt{ 205 | Default: &NamespaceConfig{ 206 | Settings: NSSettings{ 207 | Overwrite: boolPtr(false), 208 | }, 209 | Metadata: NSMetadata{ 210 | Annotations: map[string]string{ 211 | "course-annotation": "course-value", 212 | }, 213 | Labels: map[string]string{ 214 | "course-label": "course-value", 215 | }, 216 | }, 217 | }, 218 | }, 219 | Releases: []*Release{ 220 | { 221 | Name: "default", 222 | NamespaceMgmt: &NamespaceConfig{ 223 | Settings: NSSettings{ 224 | Overwrite: boolPtr(false), 225 | }, 226 | Metadata: NSMetadata{ 227 | Annotations: map[string]string{ 228 | "course-annotation": "course-value", 229 | "release-annotation": "release-value", 230 | }, 231 | Labels: map[string]string{ 232 | "course-label": "course-value", 233 | "release-label": "release-value", 234 | }, 235 | }, 236 | }, 237 | }, 238 | { 239 | Name: "release2", 240 | NamespaceMgmt: &NamespaceConfig{ 241 | Settings: NSSettings{ 242 | Overwrite: boolPtr(false), 243 | }, 244 | Metadata: NSMetadata{ 245 | Annotations: map[string]string{ 246 | "course-annotation": "course-value", 247 | }, 248 | Labels: map[string]string{ 249 | "course-label": "course-value", 250 | }, 251 | }, 252 | }, 253 | }, 254 | }, 255 | }, 256 | }, 257 | } 258 | for _, tt := range tests { 259 | t.Run(tt.name, func(t *testing.T) { 260 | tt.file.populateNamespaceManagement() 261 | assert.EqualValues(t, tt.want, tt.file) 262 | }) 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /pkg/course/shellSecrets.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package course 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "os/exec" 19 | 20 | "k8s.io/klog/v2" 21 | ) 22 | 23 | // Executor represents a shell script to run 24 | type executor struct { 25 | Executable string 26 | Args []string 27 | } 28 | 29 | // newShellExecutor returns an executor with the given script 30 | func newShellExecutor(script []string) (*executor, error) { 31 | var args []string 32 | if len(script) == 1 { 33 | args = []string{} 34 | } else { 35 | args = script[1:] 36 | } 37 | path, err := exec.LookPath(script[0]) 38 | if err != nil { 39 | return nil, fmt.Errorf("failed to find executable for ShellExecutor secret: %s - %w", script[0], err) 40 | } 41 | return &executor{ 42 | Executable: path, 43 | Args: args, 44 | }, nil 45 | } 46 | 47 | // Get returns the value of the secret and also satisfies the secrets.Getter interface 48 | func (s executor) Get(key string) (string, error) { 49 | cmd := exec.Command(s.Executable, s.Args...) 50 | var stdoutBuf, stderrBuf bytes.Buffer 51 | 52 | cmd.Stdout = &stdoutBuf 53 | cmd.Stderr = &stderrBuf 54 | 55 | err := cmd.Run() 56 | outStr, errStr := stdoutBuf.String(), stderrBuf.String() 57 | if err != nil { 58 | klog.V(8).Infof("stdout: %s", outStr) 59 | klog.V(7).Infof("stderr: %s", errStr) 60 | return "", fmt.Errorf("exit code %d running command %s - %w", cmd.ProcessState.ExitCode(), cmd.String(), err) 61 | } 62 | return outStr, nil 63 | } 64 | -------------------------------------------------------------------------------- /pkg/course/shellSecrets_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package course 14 | 15 | import ( 16 | "regexp" 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func Test_executor_Get(t *testing.T) { 23 | type fields struct { 24 | Executable string 25 | Args []string 26 | } 27 | type args struct { 28 | key string 29 | } 30 | tests := []struct { 31 | name string 32 | fields fields 33 | args args 34 | want string 35 | wantErr bool 36 | }{ 37 | { 38 | name: "test", 39 | fields: fields{ 40 | Executable: "echo", 41 | Args: []string{"-n", "hello"}, 42 | }, 43 | args: args{ 44 | key: "test", 45 | }, 46 | want: "hello", 47 | wantErr: false, 48 | }, 49 | { 50 | name: "error", 51 | fields: fields{ 52 | Executable: "exit", 53 | Args: []string{"1"}, 54 | }, 55 | args: args{ 56 | key: "test", 57 | }, 58 | want: "hello", 59 | wantErr: true, 60 | }, 61 | } 62 | for _, tt := range tests { 63 | t.Run(tt.name, func(t *testing.T) { 64 | s := executor{ 65 | Executable: tt.fields.Executable, 66 | Args: tt.fields.Args, 67 | } 68 | got, err := s.Get(tt.args.key) 69 | if tt.wantErr { 70 | assert.Error(t, err) 71 | } else { 72 | assert.NoError(t, err) 73 | assert.EqualValues(t, tt.want, got) 74 | } 75 | }) 76 | } 77 | } 78 | 79 | func Test_newShellExecutor(t *testing.T) { 80 | type args struct { 81 | script []string 82 | } 83 | tests := []struct { 84 | name string 85 | args args 86 | want *executor 87 | wantErr bool 88 | }{ 89 | { 90 | name: "basic", 91 | args: args{[]string{"echo", "-n", "hello"}}, 92 | want: &executor{ 93 | Executable: "/bin/echo", 94 | Args: []string{"-n", "hello"}, 95 | }, 96 | wantErr: false, 97 | }, 98 | { 99 | name: "no args", 100 | args: args{[]string{"echo"}}, 101 | want: &executor{ 102 | Executable: "/bin/echo", 103 | Args: []string{}, 104 | }, 105 | wantErr: false, 106 | }, 107 | { 108 | name: "no args", 109 | args: args{[]string{"farglebargleshouldreallynotbeanexecutable"}}, 110 | wantErr: true, 111 | }, 112 | } 113 | for _, tt := range tests { 114 | t.Run(tt.name, func(t *testing.T) { 115 | got, err := newShellExecutor(tt.args.script) 116 | if tt.wantErr { 117 | assert.Error(t, err) 118 | } else { 119 | assert.NoError(t, err) // check for errors generally 120 | assert.Regexp(t, regexp.MustCompile("^.+bin/echo$"), got.Executable, "") // handle any *bin/echo, such as /usr/bin/echo 121 | assert.EqualValues(t, tt.want.Args, got.Args) // also verify args 122 | } 123 | }) 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /pkg/course/testdata/convert1.yaml: -------------------------------------------------------------------------------- 1 | namespace: namespace 2 | context: farglebargle 3 | repository: stable 4 | helm_args: 5 | - --atomic 6 | repositories: 7 | git-repo-test: 8 | git: https://github.com/FairwindsOps/charts 9 | path: stable 10 | helm-repo: 11 | url: https://ahelmrepo.example.com 12 | charts: 13 | basic: 14 | chart: somechart 15 | repository: 16 | name: helm-repo 17 | version: "2.0.0" 18 | values: 19 | dummyvalue: false 20 | gitrelease: 21 | chart: "gitchart" 22 | repository: 23 | git: giturl 24 | path: gitpath 25 | version: main 26 | standard: 27 | chart: "basic" 28 | repository: "helm-repo" 29 | -------------------------------------------------------------------------------- /pkg/course/testdata/unmarshalerror.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | namespace: 3 | isNot: a map 4 | -------------------------------------------------------------------------------- /pkg/course/testdata/v2_env.yaml: -------------------------------------------------------------------------------- 1 | schema: v2 2 | namespace: $EXAMPLE_ENV_NS 3 | repository: helm-repo 4 | context: farglebargle 5 | repositories: 6 | helm-repo: 7 | url: $HELM_REPO_URL 8 | releases: 9 | - name: basic 10 | namespace: namespace 11 | chart: somechart 12 | version: 2.0.0 13 | repository: helm-repo 14 | values: 15 | envVar: $EXAMPLE_ENV_VAR 16 | -------------------------------------------------------------------------------- /pkg/course/testdata/v2_namespace.yaml: -------------------------------------------------------------------------------- 1 | schema: v2 2 | namespace: infra 3 | repository: fairwinds-incubator 4 | context: kind-kind 5 | repositories: 6 | bitnami: 7 | url: https://charts.bitnami.com/bitnami 8 | fairwinds-incubator: 9 | url: https://charts.fairwinds.com/incubator 10 | namespace_management: 11 | default: 12 | metadata: 13 | annotations: 14 | ManagedBy: Reckoner 15 | labels: 16 | ManagedBy: Fairwinds 17 | settings: 18 | overwrite: true 19 | releases: 20 | - name: metrics-server 21 | namespace: metrics-server 22 | chart: metrics-server 23 | version: 5.11.7 24 | repository: bitnami 25 | values: 26 | apiService: 27 | create: true 28 | extraArgs: 29 | kubelet-insecure-tls: true 30 | kubelet-preferred-address-types: InternalIP 31 | - name: basic-demo 32 | namespace: basic-demo 33 | namespace_management: 34 | metadata: 35 | labels: 36 | goldilocks.fairwinds.com/enabled: "true" 37 | settings: 38 | overwrite: true 39 | chart: basic-demo 40 | -------------------------------------------------------------------------------- /pkg/helm/helm.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package helm 14 | 15 | import ( 16 | "bytes" 17 | "errors" 18 | "fmt" 19 | "os/exec" 20 | "strings" 21 | 22 | "github.com/Masterminds/semver/v3" 23 | "github.com/thoas/go-funk" 24 | "k8s.io/klog/v2" 25 | ) 26 | 27 | // Client is a local helm client 28 | type Client struct { 29 | HelmExecutable string 30 | } 31 | 32 | // NewClient ensures a helm command exists 33 | func NewClient() (*Client, error) { 34 | path, err := exec.LookPath("helm") 35 | if err != nil { 36 | return nil, fmt.Errorf("helm must be installed and available in path: %s", err.Error()) 37 | } 38 | klog.V(3).Infof("found helm at %s", path) 39 | return &Client{path}, nil 40 | } 41 | 42 | // Exec returns the output and error of a helm command given several arguments 43 | // Returns stdOut and stdErr as well as any error 44 | func (h Client) Exec(arg ...string) (string, string, error) { 45 | cmd := exec.Command(h.HelmExecutable, arg...) 46 | 47 | klog.V(8).Infof("running helm command: %v", cmd) 48 | 49 | var stdoutBuf, stderrBuf bytes.Buffer 50 | 51 | cmd.Stdout = &stdoutBuf 52 | cmd.Stderr = &stderrBuf 53 | 54 | err := cmd.Run() 55 | outStr, errStr := stdoutBuf.String(), stderrBuf.String() 56 | if err != nil { 57 | klog.V(8).Infof("stdout: %s", outStr) 58 | klog.V(7).Infof("stderr: %s", errStr) 59 | return "", errStr, fmt.Errorf("exit code %d running command %s", cmd.ProcessState.ExitCode(), cmd.String()) 60 | } 61 | 62 | return outStr, errStr, nil 63 | } 64 | 65 | // Version returns the helm client version 66 | func (h Client) Version() (*semver.Version, error) { 67 | out, _, err := h.Exec("version", "--template={{.Version}}") 68 | if err != nil { 69 | return nil, err 70 | } 71 | version, err := semver.NewVersion(out) 72 | if err != nil { 73 | return nil, err 74 | } 75 | return version, nil 76 | } 77 | 78 | // AddRepository adds a Helm repository 79 | func (h Client) AddRepository(repoName, url string) error { 80 | _, _, err := h.Exec("repo", "add", repoName, url) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | return nil 86 | } 87 | 88 | // Cache returns the local helm cache if defined 89 | func (h Client) Cache() (string, error) { 90 | stdOut, stdErr, _ := h.Exec("env") 91 | if stdErr != "" { 92 | return "", fmt.Errorf("error running helm env: %s", stdErr) 93 | } 94 | for _, line := range strings.Split(stdOut, "\n") { 95 | if strings.Contains(line, "HELM_REPOSITORY_CACHE") { 96 | value := strings.Split(line, "=")[1] 97 | return strings.Trim(value, "\""), nil 98 | } 99 | } 100 | return "", fmt.Errorf("could not find HELM_REPOSITORY_CACHE in helm env output") 101 | } 102 | 103 | // UpdateDependencies will update dependencies for a given release if it is stored locally (i.e. pulled from git) 104 | func (h Client) UpdateDependencies(path string) error { 105 | klog.V(5).Infof("updating chart dependencies for %s", path) 106 | _, stdErr, _ := h.Exec("dependency", "update", path) 107 | if stdErr != "" { 108 | return fmt.Errorf("error running helm dependency update: %s", stdErr) 109 | } 110 | 111 | return nil 112 | } 113 | 114 | // GetManifestString will run 'helm get manifest' on a given namespace and release and return string output. 115 | func (h Client) GetManifestString(namespace, release string) (string, error) { 116 | out, err := h.get("manifest", namespace, release) 117 | if err != nil { 118 | return "", err 119 | } 120 | return out, err 121 | } 122 | 123 | // GetUserSuppliedValues will run 'helm get values' on a given namespace and release and return []byte output suitable for yaml Marshaling. 124 | func (h Client) GetUserSuppliedValuesYAML(namespace, release string) ([]byte, error) { 125 | out, err := h.get("values", namespace, release, "--output", "yaml") 126 | if err != nil { 127 | return nil, err 128 | } 129 | return []byte(out), err 130 | } 131 | 132 | // ListNamespaceReleasesYAML will run 'helm list' on a given namespace and return []byte output suitable for yaml Marshaling. 133 | func (h Client) ListNamespaceReleasesYAML(namespace string) ([]byte, error) { 134 | out, err := h.list(namespace, "--output", "yaml") 135 | if err != nil { 136 | return nil, err 137 | } 138 | return []byte(out), nil 139 | } 140 | 141 | // get can run any 'helm get' command 142 | func (h Client) get(kind, namespace, release string, extraArgs ...string) (string, error) { 143 | validKinds := []string{"all", "hooks", "manifest", "notes", "values"} 144 | if !funk.Contains(validKinds, kind) { 145 | return "", errors.New("invalid kind passed to helm: " + kind) 146 | } 147 | args := []string{"get", kind, "--namespace", namespace, release} 148 | args = append(args, extraArgs...) 149 | stdOut, stdErr, err := h.Exec(args...) 150 | if err != nil && stdErr != "" { 151 | return "", errors.New(stdErr) 152 | } 153 | return stdOut, nil 154 | } 155 | 156 | // list can run any 'helm list' command 157 | func (h Client) list(namespace string, extraArgs ...string) (string, error) { 158 | args := []string{"list", "--namespace", namespace} 159 | args = append(args, extraArgs...) 160 | stdOut, stdErr, err := h.Exec(args...) 161 | if err != nil && stdErr != "" { 162 | return "", errors.New(stdErr) 163 | } 164 | return stdOut, nil 165 | } 166 | -------------------------------------------------------------------------------- /pkg/reckoner/argocd.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "bytes" 17 | "errors" 18 | "os" 19 | "strings" 20 | 21 | "github.com/fairwindsops/reckoner/pkg/course" 22 | "github.com/fatih/color" 23 | "github.com/imdario/mergo" 24 | "gopkg.in/yaml.v3" 25 | "k8s.io/klog/v2" 26 | ) 27 | 28 | func generateArgoApplication(release course.Release, courseFile course.FileV2) (app course.ArgoApplication, err error) { 29 | app = courseFile.GitOps.ArgoCD // use global config at root of course file 30 | 31 | // if release.GitOps.ArgoCD. exists, override the app. with that one, recursively. 32 | err = mergo.Merge(&app, release.GitOps.ArgoCD, mergo.WithOverride) 33 | if err != nil { 34 | return app, err 35 | } 36 | 37 | // default to a kind of Application if it was omitted in the course file 38 | if app.Kind == "" { 39 | app.Kind = "Application" 40 | } 41 | 42 | // default to an API version of v1alpha1 if it was omitted in the course file 43 | if app.APIVersion == "" { 44 | app.APIVersion = "argoproj.io/v1alpha1" 45 | } 46 | 47 | // unless they overrode it in the course file, assume the name of the argocd app is the same as the helm release 48 | // app:release should be a 1:1 49 | if app.Metadata.Name == "" { 50 | app.Metadata.Name = release.Name 51 | } 52 | 53 | // Application.Metadata.Namespace is where the ArgoCD Application resource will go (not the helm release) 54 | if app.Metadata.Namespace == "" { 55 | klog.V(3).Infoln("No namespace declared in course file. Your ArgoCD Application manifests will likely get applied to the agent's default context.") 56 | } 57 | 58 | // default source path to release name 59 | if app.Spec.Source.Path == "" { 60 | klog.V(3).Infoln("No .gitops.argocd.spec.source.path declared in course file for " + release.Name + ". The path has been set to its name.") 61 | app.Spec.Source.Path = release.Name 62 | } 63 | 64 | // don't support ArgoCD Application spec.destination.namespace at all 65 | if app.Spec.Destination.Namespace != "" { 66 | klog.V(3).Infoln("Refusing to respect the .gitops.argocd.spec.destination.namespace value declared in course file for " + release.Name + ". Using the release namespace instead, if it exists. If none is specified, the default at the root of the course YAML file will be used. Remove the namespace from the ArgoCD destination to stop seeing this warning.") 67 | } 68 | 69 | // Application.Spec.Destination.Namespace is where the helm releases will be applied 70 | if release.Namespace != "" { // there's a specific namespace for this release 71 | app.Spec.Destination.Namespace = release.Namespace // specify it as the destination namespace 72 | } else { // nothing was specified in the release 73 | app.Spec.Destination.Namespace = courseFile.DefaultNamespace // use the default namespace at the root of the course file 74 | } 75 | 76 | if app.Spec.Destination.Server == "" { 77 | klog.V(3).Infoln("No .gitops.argocd.spec.destination.server declared in course file for " + release.Name + ". Assuming you want the kubernetes service in the default namespace. Specify to make this warning go away.") 78 | app.Spec.Destination.Server = "https://kubernetes.default.svc" 79 | } 80 | 81 | if app.Spec.Project == "" { 82 | klog.V(3).Infoln("No .gitops.argocd.spec.project declared in course file for " + release.Name + ". We'll set it to a sensible default value of 'default'. Specify to make this warning go away.") 83 | app.Spec.Project = "default" 84 | } 85 | 86 | return app, err 87 | } 88 | 89 | func (c *Client) WriteArgoApplications(outputDir string) (err error) { 90 | appsOutputDir := outputDir + "/argocd-apps" 91 | for _, dir := range []string{outputDir, appsOutputDir} { 92 | if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { 93 | err := os.Mkdir(dir, os.ModePerm) 94 | if err != nil { 95 | return err 96 | } 97 | } 98 | } 99 | 100 | for _, release := range c.CourseFile.Releases { 101 | // generate an argocd application resource 102 | app, err := generateArgoApplication(*release, c.CourseFile) 103 | if err != nil { 104 | return err 105 | } 106 | 107 | if app.Metadata.Name == "" { 108 | color.Yellow("No metadata found for release " + release.Name + ". Skipping ArgoCD Applicationgeneration...") 109 | continue 110 | } 111 | 112 | // generate name of app file 113 | appOutputFile := appsOutputDir + "/" + strings.ToLower(app.Metadata.Name) + ".yaml" 114 | 115 | // prepare to write stuff (pretty) 116 | var b bytes.Buffer // used for encoding & return 117 | yamlEncoder := yaml.NewEncoder(&b) // create an encoder to handle custom configuration 118 | yamlEncoder.SetIndent(2) // people expect two-space indents instead of the default four 119 | err = yamlEncoder.Encode(&app) // encode proper YAML into slice of bytes 120 | if err != nil { // check for errors 121 | return err // bubble up 122 | } 123 | 124 | // write stuff 125 | err = writeYAML(b.Bytes(), appOutputFile) 126 | if err != nil { // check for errors 127 | return err // bubble up 128 | } 129 | } 130 | 131 | return err 132 | } 133 | -------------------------------------------------------------------------------- /pkg/reckoner/argocd_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/fairwindsops/reckoner/pkg/course" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | func Test_generateArgoApplication(t *testing.T) { 23 | tests := []struct { 24 | name string 25 | cFile course.FileV2 26 | want course.ArgoApplication 27 | wantErr bool 28 | }{ 29 | { 30 | name: "ensure_defaults", 31 | cFile: course.FileV2{ 32 | Releases: []*course.Release{ 33 | { 34 | Name: "somename", 35 | Namespace: "somens", 36 | Repository: "somerepo", 37 | GitOps: course.GitOps{ // release-specific *addition* 38 | ArgoCD: course.ArgoApplication{ 39 | Metadata: course.ArgoApplicationMetadata{ 40 | Annotations: map[string]string{ 41 | "notifications.argoproj.io/subscribe.on-sync-succeeded.slack": "fairwindsops-infra-argocd", 42 | }, 43 | }, 44 | Spec: course.ArgoApplicationSpec{ 45 | Source: course.ArgoApplicationSpecSource{ 46 | Directory: course.ArgoApplicationSpecSourceDirectory{ 47 | Recurse: true, // release-specific *override* 48 | }, 49 | }, 50 | }, 51 | }, 52 | }, 53 | }, 54 | }, 55 | GitOps: course.GitOps{ 56 | ArgoCD: course.ArgoApplication{ 57 | Spec: course.ArgoApplicationSpec{ 58 | Source: course.ArgoApplicationSpecSource{ 59 | Directory: course.ArgoApplicationSpecSourceDirectory{ 60 | Recurse: false, // exists here only to be overridden by the release-specific instance 61 | }, 62 | // Path: "somepath", // omitting this tests more functionality 63 | RepoURL: "https://domain.tld/someorg/somerepo.git", 64 | }, 65 | // Destination: course.ArgoApplicationSpecDestination{}, // omitting this tests defaults 66 | }, 67 | }, 68 | }, 69 | }, 70 | want: course.ArgoApplication{ 71 | Kind: "Application", 72 | APIVersion: "argoproj.io/v1alpha1", 73 | Metadata: course.ArgoApplicationMetadata{ 74 | Name: "somename", 75 | Annotations: map[string]string{ 76 | "notifications.argoproj.io/subscribe.on-sync-succeeded.slack": "fairwindsops-infra-argocd", 77 | }, 78 | }, 79 | Spec: course.ArgoApplicationSpec{ 80 | Source: course.ArgoApplicationSpecSource{ 81 | Directory: course.ArgoApplicationSpecSourceDirectory{ 82 | Recurse: true, 83 | }, 84 | Path: "somename", 85 | RepoURL: "https://domain.tld/someorg/somerepo.git", 86 | }, 87 | Destination: course.ArgoApplicationSpecDestination{ 88 | Server: "https://kubernetes.default.svc", 89 | Namespace: "somens", 90 | }, 91 | Project: "default", 92 | }, 93 | }, 94 | wantErr: false, 95 | }, 96 | } 97 | for _, tt := range tests { 98 | t.Run(tt.name, func(t *testing.T) { 99 | got, err := generateArgoApplication(*tt.cFile.Releases[0], tt.cFile) 100 | if (err != nil) != tt.wantErr { 101 | t.Errorf("generateArgoApplication() error = %v, wantErr %v", err, tt.wantErr) 102 | return 103 | } 104 | assert.Equal(t, tt.want, got) 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /pkg/reckoner/diff_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/fatih/color" 19 | "github.com/stretchr/testify/assert" 20 | ) 21 | 22 | var ( 23 | serviceAccountTemplate = `apiVersion: v1 24 | kind: ServiceAccount 25 | metadata: 26 | name: example-controller 27 | labels: 28 | app.kubernetes.io/name: example 29 | helm.sh/chart: example-1.0.0 30 | app.kubernetes.io/instance: example 31 | app.kubernetes.io/managed-by: Helm 32 | app.kubernetes.io/component: controller 33 | ` 34 | modifiedServiceAccountTemplate = `apiVersion: v1 35 | kind: ServiceAccount 36 | metadata: 37 | name: example-controller-1 38 | labels: 39 | app.kubernetes.io/name: example 40 | helm.sh/chart: example-1.0.0 41 | app.kubernetes.io/instance: example 42 | app.kubernetes.io/managed-by: Helm 43 | app.kubernetes.io/component: controller 44 | ` 45 | clusterRoleTemplate = `apiVersion: rbac.authorization.k8s.io/v1 46 | kind: ClusterRole 47 | metadata: 48 | name: example-controller 49 | labels: 50 | app.kubernetes.io/name: example 51 | helm.sh/chart: example-1.0.0 52 | app.kubernetes.io/instance: example 53 | app.kubernetes.io/managed-by: Helm 54 | app.kubernetes.io/component: controller 55 | rules: 56 | - apiGroups: 57 | - 'apps' 58 | resources: 59 | - '*' 60 | verbs: 61 | - 'get' 62 | - 'list' 63 | - 'watch' 64 | - apiGroups: 65 | - '' 66 | resources: 67 | - 'namespaces' 68 | - 'pods' 69 | verbs: 70 | - 'get' 71 | - 'list' 72 | - 'watch' 73 | - apiGroups: 74 | - 'autoscaling.k8s.io' 75 | resources: 76 | - 'verticalpodautoscalers' 77 | verbs: 78 | - 'get' 79 | - 'list' 80 | - 'create' 81 | - 'delete' 82 | - 'update' 83 | ` 84 | testGetManifestSlice = []Manifest{ 85 | { 86 | Source: "example/templates/controller-serviceaccount.yaml", 87 | Kind: "ServiceAccount", 88 | Metadata: Metadata{ 89 | Name: "example-controller", 90 | }, 91 | Content: serviceAccountTemplate, 92 | }, 93 | { 94 | Source: "example/templates/controller-clusterrole.yaml", 95 | Kind: "ClusterRole", 96 | Metadata: Metadata{ 97 | Name: "example-controller", 98 | }, 99 | Content: clusterRoleTemplate, 100 | }, 101 | } 102 | testTemplateSlice = []Manifest{ 103 | { 104 | Source: "example/templates/controller-serviceaccount.yaml", 105 | Kind: "ServiceAccount", 106 | Metadata: Metadata{ 107 | Name: "example-controller", 108 | }, 109 | Content: serviceAccountTemplate, 110 | }, 111 | { 112 | Source: "example/templates/controller-clusterrole.yaml", 113 | Kind: "ClusterRole", 114 | Metadata: Metadata{ 115 | Name: "example-controller", 116 | }, 117 | Content: clusterRoleTemplate, 118 | }, 119 | } 120 | testTemplateSliceModified = []Manifest{ 121 | { 122 | Source: "example/templates/controller-serviceaccount.yaml", 123 | Kind: "ServiceAccount", 124 | Metadata: Metadata{ 125 | Name: "example-controller", 126 | }, 127 | Content: modifiedServiceAccountTemplate, 128 | }, 129 | { 130 | Source: "example/templates/controller-clusterrole.yaml", 131 | Kind: "ClusterRole", 132 | Metadata: Metadata{ 133 | Name: "example-controller", 134 | }, 135 | Content: clusterRoleTemplate, 136 | }, 137 | } 138 | wantedManifestDiffSlice = []ManifestDiff{ 139 | { 140 | ReleaseName: "example", 141 | Kind: "ServiceAccount", 142 | Name: "example-controller", 143 | Source: "example/templates/controller-serviceaccount.yaml", 144 | Diff: "", 145 | NewFile: false, 146 | }, 147 | { 148 | ReleaseName: "example", 149 | Kind: "ClusterRole", 150 | Name: "example-controller", 151 | Source: "example/templates/controller-clusterrole.yaml", 152 | Diff: "", 153 | NewFile: false, 154 | }, 155 | } 156 | wantedManifestDiffSliceModified = []ManifestDiff{ 157 | { 158 | ReleaseName: "example", 159 | Kind: "ServiceAccount", 160 | Name: "example-controller", 161 | Source: "example/templates/controller-serviceaccount.yaml", 162 | Diff: "\nkind: ServiceAccount\nmetadata:\n- name: example-controller\n+ name: example-controller-1\n labels:\n\tapp.kubernetes.io/name: example\n\thelm.sh/chart: example-1.0.0\n\tapp.kubernetes.io/instance: example\n\n", 163 | NewFile: false, 164 | }, 165 | { 166 | ReleaseName: "example", 167 | Kind: "ClusterRole", 168 | Name: "example-controller", 169 | Source: "example/templates/controller-clusterrole.yaml", 170 | Diff: "", 171 | NewFile: false, 172 | }, 173 | } 174 | ) 175 | 176 | func Test_populateDiffs(t *testing.T) { 177 | tests := []struct { 178 | name string 179 | releaseName string 180 | mSlice []Manifest 181 | tSlice []Manifest 182 | want []ManifestDiff 183 | wantErr bool 184 | }{ 185 | { 186 | name: "populateDiffs_no_diff", 187 | releaseName: "example", 188 | mSlice: testGetManifestSlice, 189 | tSlice: testTemplateSlice, 190 | want: wantedManifestDiffSlice, 191 | wantErr: false, 192 | }, 193 | { 194 | name: "populateDiffs_diff", 195 | releaseName: "example", 196 | mSlice: testGetManifestSlice, 197 | tSlice: testTemplateSliceModified, 198 | want: wantedManifestDiffSliceModified, 199 | wantErr: false, 200 | }, 201 | } 202 | for _, tt := range tests { 203 | color.NoColor = true // disable color output so that we can more easily compare output strings in Diff 204 | t.Run(tt.name, func(t *testing.T) { 205 | got, err := populateDiffs(tt.releaseName, tt.mSlice, tt.tSlice) 206 | if (err != nil) != tt.wantErr { 207 | t.Errorf("populateDiffs() error = %v, wantErr %v", err, tt.wantErr) 208 | return 209 | } 210 | assert.Equal(t, tt.want, got) 211 | }) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /pkg/reckoner/git.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "errors" 17 | "fmt" 18 | "io" 19 | "os" 20 | 21 | "github.com/fairwindsops/reckoner/pkg/course" 22 | "github.com/fatih/color" 23 | git "github.com/go-git/go-git/v5" 24 | "github.com/go-git/go-git/v5/plumbing" 25 | ) 26 | 27 | func (c Client) cloneGitRepository(release *course.Release) error { 28 | releaseRepository := c.CourseFile.Repositories[release.Repository] 29 | if release.Version == "" { 30 | color.Yellow("Git repository in use with no version specified. Defaulting to master branch. This default will be removed in the future, please define a version for this release: %s", release.Name) 31 | release.Version = "master" 32 | } 33 | 34 | repo, worktree, err := setupGitRepoPath(*release.GitClonePath, releaseRepository.Git) 35 | if err != nil { 36 | return err 37 | } 38 | 39 | err = repo.Fetch(&git.FetchOptions{ 40 | Tags: git.AllTags, 41 | }) 42 | if err != nil && !errors.Is(err, git.NoErrAlreadyUpToDate) { 43 | return fmt.Errorf("Error fetching git repository %s - %s", *release.GitClonePath, err) 44 | } 45 | 46 | hash, err := determineGitRevisionHash(repo, release.Version) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | err = worktree.Checkout(&git.CheckoutOptions{ 52 | Hash: *hash, 53 | Force: true, 54 | }) 55 | if err != nil { 56 | return fmt.Errorf("Error checking out git repository %s - %s", *release.GitClonePath, err) 57 | } 58 | 59 | return nil 60 | } 61 | 62 | func setupGitRepoPath(clonePath, url string) (*git.Repository, *git.Worktree, error) { 63 | var repo *git.Repository 64 | var worktree *git.Worktree 65 | if _, err := os.Stat(clonePath); errors.Is(err, os.ErrNotExist) { 66 | err := os.Mkdir(clonePath, 0755) 67 | if err != nil { 68 | return nil, nil, fmt.Errorf("Error creating directory %s - %s", clonePath, err) 69 | } 70 | } 71 | if empty, err := dirIsEmpty(clonePath); empty && err == nil { 72 | repo, err = git.PlainClone(clonePath, false, &git.CloneOptions{ 73 | URL: url, 74 | }) 75 | if err != nil { 76 | return nil, nil, fmt.Errorf("Error initializing git repository %s - %s", clonePath, err) 77 | } 78 | worktree, err = repo.Worktree() 79 | if err != nil { 80 | return nil, nil, fmt.Errorf("Error getting git working tree %s - %s", clonePath, err) 81 | } 82 | } else { 83 | if err != nil { 84 | return nil, nil, fmt.Errorf("Error checking if directory is empty %s - %s", clonePath, err) 85 | } 86 | repo, err = git.PlainOpen(clonePath) 87 | if errors.Is(err, git.ErrRepositoryNotExists) { 88 | err := os.RemoveAll(clonePath) 89 | if err != nil { 90 | return nil, nil, fmt.Errorf("Error removing directory %s - %s", clonePath, err) 91 | } 92 | err = os.MkdirAll(clonePath, 0755) 93 | if err != nil { 94 | return nil, nil, fmt.Errorf("Error creating directory %s - %s", clonePath, err) 95 | } 96 | repo, err = git.PlainClone(clonePath, false, &git.CloneOptions{ 97 | URL: url, 98 | }) 99 | if err != nil { 100 | return nil, nil, fmt.Errorf("Error initializing git repository %s - %s", clonePath, err) 101 | } 102 | } else if err != nil { 103 | return nil, nil, fmt.Errorf("Error opening git repository %s - %s", clonePath, err) 104 | } 105 | worktree, err = repo.Worktree() 106 | if err != nil { 107 | return nil, nil, fmt.Errorf("Error getting git working tree %s - %s", clonePath, err) 108 | } 109 | } 110 | return repo, worktree, nil 111 | } 112 | 113 | func determineGitRevisionHash(repo *git.Repository, version string) (*plumbing.Hash, error) { 114 | hash, err := repo.ResolveRevision(plumbing.Revision(version)) 115 | if errors.Is(err, plumbing.ErrReferenceNotFound) { 116 | hash, err = repo.ResolveRevision(plumbing.Revision(fmt.Sprintf("origin/%s", version))) 117 | } 118 | if err != nil { 119 | return nil, fmt.Errorf("Error resolving git revision %s - %s", version, err) 120 | } 121 | return hash, nil 122 | } 123 | 124 | func dirIsEmpty(name string) (bool, error) { 125 | f, err := os.Open(name) 126 | if err != nil { 127 | return false, err 128 | } 129 | defer f.Close() 130 | 131 | _, err = f.Readdirnames(1) 132 | if errors.Is(err, io.EOF) { 133 | return true, nil 134 | } 135 | return false, err 136 | } 137 | -------------------------------------------------------------------------------- /pkg/reckoner/hook.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "os/exec" 17 | "strings" 18 | 19 | "github.com/fatih/color" 20 | ) 21 | 22 | func (c Client) execHook(hooks []string, kind string) error { 23 | if c.DryRun { 24 | color.Yellow("hook not run due to --dry-run: %v", c.DryRun) 25 | return nil 26 | } 27 | 28 | if len(hooks) == 0 { 29 | return nil 30 | } 31 | 32 | for _, hook := range hooks { 33 | color.Green("Running %s hook: %s", kind, hook) 34 | commands := strings.Split(hook, " ") 35 | args := commands[1:] 36 | 37 | command := exec.Command(commands[0], args...) 38 | command.Dir = c.BaseDirectory 39 | 40 | data, runError := command.CombinedOutput() 41 | color.Green("Hook '%s' output: %s", command.String(), string(data)) 42 | if runError != nil { 43 | return runError 44 | } 45 | 46 | } 47 | return nil 48 | } 49 | -------------------------------------------------------------------------------- /pkg/reckoner/import.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "strings" 19 | 20 | "github.com/Masterminds/semver/v3" 21 | "github.com/fairwindsops/reckoner/pkg/course" 22 | "github.com/fairwindsops/reckoner/pkg/helm" 23 | "gopkg.in/yaml.v3" 24 | ) 25 | 26 | type ImportInfo struct { 27 | Chart string 28 | Version string 29 | Name string 30 | Namespace string 31 | } 32 | 33 | type ImportValues map[string]interface{} 34 | 35 | func ImportOutput(release, namespace, repository string) (string, error) { 36 | helmClient, err := helm.NewClient() 37 | if err != nil { 38 | return "", err 39 | } 40 | releaseInfo, err := gatherReleaseInfo(helmClient, namespace, release) 41 | if err != nil { 42 | return "", err 43 | } 44 | if err := releaseInfo.parseChartVersion(); err != nil { 45 | return "", err 46 | } 47 | userValues, err := gatherUserSuppliedValues(helmClient, namespace, release) 48 | if err != nil { 49 | return "", err 50 | } 51 | 52 | // set up and populate a release struct that will be used to output in yaml format 53 | var releaseOut = new(course.Release) 54 | releaseOut.Name = releaseInfo.Name 55 | releaseOut.Namespace = releaseInfo.Namespace 56 | releaseOut.Chart = releaseInfo.Chart 57 | releaseOut.Version = releaseInfo.Version 58 | releaseOut.Values = userValues 59 | releaseOut.Repository = repository 60 | 61 | // in order to set indent to 2 spaces we need to use a yaml encoder instead of yaml.Marshal 62 | ret := bytes.NewBuffer([]byte{}) 63 | encoder := yaml.NewEncoder(ret) 64 | defer encoder.Close() 65 | encoder.SetIndent(2) 66 | if err := encoder.Encode(releaseOut); err != nil { 67 | return "", err 68 | } 69 | 70 | return ret.String(), err 71 | } 72 | 73 | func gatherReleaseInfo(helmClient *helm.Client, namespace, release string) (*ImportInfo, error) { 74 | var allReleases = new([]*ImportInfo) 75 | releaseInfo, err := helmClient.ListNamespaceReleasesYAML(namespace) 76 | if err != nil { 77 | return nil, err 78 | } 79 | if err := yaml.Unmarshal(releaseInfo, allReleases); err != nil { 80 | return nil, err 81 | } 82 | for _, r := range *allReleases { 83 | if r.Name == release { 84 | return r, nil 85 | } 86 | } 87 | return nil, fmt.Errorf("could not find release %s in namespace %s", release, namespace) 88 | } 89 | 90 | func gatherUserSuppliedValues(helmClient *helm.Client, namespace, release string) (ImportValues, error) { 91 | var values = make(ImportValues) 92 | userValues, err := helmClient.GetUserSuppliedValuesYAML(namespace, release) 93 | if err != nil { 94 | return nil, err 95 | } 96 | if err := yaml.Unmarshal(userValues, values); err != nil { 97 | return nil, err 98 | } 99 | return values, nil 100 | } 101 | 102 | func (i *ImportInfo) parseChartVersion() error { 103 | splitChartVersion := strings.Split(i.Chart, "-") 104 | if len(splitChartVersion) < 2 { 105 | return fmt.Errorf("could not parse chart version from %s - expected at least one hyphen between chart name and version", i.Chart) 106 | } 107 | splitPoint := len(splitChartVersion) - 1 108 | i.Chart = strings.Join(splitChartVersion[:splitPoint], "-") 109 | i.Version = splitChartVersion[splitPoint] 110 | if _, err := semver.NewVersion(i.Version); err != nil { 111 | return fmt.Errorf("go %s as version string which is not valid semver", i.Version) 112 | } 113 | return nil 114 | } 115 | -------------------------------------------------------------------------------- /pkg/reckoner/import_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | const ( 22 | testReleaseName = "helm-chart" 23 | testNamespace = "helm-ns" 24 | ) 25 | 26 | func TestParseChartVersion(t *testing.T) { 27 | type test struct { 28 | name string 29 | importInfo *ImportInfo 30 | want *ImportInfo 31 | wantErr bool 32 | } 33 | tests := []test{ 34 | { 35 | name: "succeed", 36 | importInfo: &ImportInfo{ 37 | Chart: "helm-chart-0.1.2", 38 | Version: "", 39 | Name: testReleaseName, 40 | Namespace: testNamespace, 41 | }, 42 | want: &ImportInfo{ 43 | Chart: "helm-chart", 44 | Version: "0.1.2", 45 | Name: testReleaseName, 46 | Namespace: testNamespace, 47 | }, 48 | wantErr: false, 49 | }, 50 | { 51 | name: "error", 52 | importInfo: &ImportInfo{ 53 | Chart: "helmchart", 54 | Version: "", 55 | Name: testReleaseName, 56 | Namespace: testNamespace, 57 | }, 58 | want: &ImportInfo{}, 59 | wantErr: true, 60 | }, 61 | { 62 | name: "version error", 63 | importInfo: &ImportInfo{ 64 | Chart: "helm-chart", 65 | Version: "", 66 | Name: testReleaseName, 67 | Namespace: testNamespace, 68 | }, 69 | want: &ImportInfo{}, 70 | wantErr: true, 71 | }, 72 | } 73 | 74 | for _, tt := range tests { 75 | t.Run(tt.name, func(t *testing.T) { 76 | err := tt.importInfo.parseChartVersion() 77 | got := tt.importInfo 78 | if tt.wantErr { 79 | assert.Error(t, err) 80 | } else { 81 | assert.NoError(t, err) 82 | assert.EqualValues(t, tt.want, got) 83 | } 84 | }) 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /pkg/reckoner/manifest_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import "testing" 16 | 17 | var testManifestsString = ` 18 | --- 19 | # Source: example/templates/controller-serviceaccount.yaml 20 | apiVersion: v1 21 | kind: ServiceAccount 22 | metadata: 23 | name: example-controller 24 | labels: 25 | app.kubernetes.io/name: example 26 | helm.sh/chart: example-1.0.0 27 | app.kubernetes.io/instance: example 28 | app.kubernetes.io/managed-by: Helm 29 | app.kubernetes.io/component: controller 30 | --- 31 | # Source: example/templates/controller-clusterrole.yaml 32 | apiVersion: rbac.authorization.k8s.io/v1 33 | kind: ClusterRole 34 | metadata: 35 | name: example-controller 36 | labels: 37 | app.kubernetes.io/name: example 38 | helm.sh/chart: example-1.0.0 39 | app.kubernetes.io/instance: example 40 | app.kubernetes.io/managed-by: Helm 41 | app.kubernetes.io/component: controller 42 | rules: 43 | - apiGroups: 44 | - 'apps' 45 | resources: 46 | - '*' 47 | verbs: 48 | - 'get' 49 | - 'list' 50 | - 'watch' 51 | - apiGroups: 52 | - '' 53 | resources: 54 | - 'namespaces' 55 | - 'pods' 56 | verbs: 57 | - 'get' 58 | - 'list' 59 | - 'watch' 60 | - apiGroups: 61 | - 'autoscaling.k8s.io' 62 | resources: 63 | - 'verticalpodautoscalers' 64 | verbs: 65 | - 'get' 66 | - 'list' 67 | - 'create' 68 | - 'delete' 69 | - 'update' 70 | ` 71 | 72 | func TestManifestUnmarshal(t *testing.T) { 73 | manifests, err := ManifestUnmarshal(testManifestsString) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | if len(manifests) != 2 { 78 | t.Fatalf("Expected 2 manifests, got %d", len(manifests)) 79 | } 80 | 81 | // Test first manifest 82 | if manifests[0].Kind != "ServiceAccount" { 83 | t.Errorf("Expected first manifest's Kind to be ServiceAccount, got %s", manifests[0].Kind) 84 | } 85 | if manifests[0].Metadata.Name != "example-controller" { 86 | t.Errorf("Expected first manifest's Metadata.Name to be example-controller, got %s", manifests[0].Metadata.Name) 87 | } 88 | if manifests[0].Source != "example/templates/controller-serviceaccount.yaml" { 89 | t.Errorf("Expected first manifest's Source to be example/templates/controller-serviceaccount.yaml, got %s", manifests[0].Source) 90 | } 91 | 92 | // Test second manifest 93 | if manifests[1].Kind != "ClusterRole" { 94 | t.Errorf("Expected second manifest's Kind to be ClusterRole, got %s", manifests[1].Kind) 95 | } 96 | if manifests[1].Metadata.Name != "example-controller" { 97 | t.Errorf("Expected second manifest's Metadata.Name to be example-controller, got %s", manifests[1].Metadata.Name) 98 | } 99 | if manifests[1].Source != "example/templates/controller-clusterrole.yaml" { 100 | t.Errorf("Expected second manifest's Source to be example/templates/controller-clusterrole.yaml, got %s", manifests[1].Source) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /pkg/reckoner/manifests.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "io" 19 | "regexp" 20 | "strings" 21 | 22 | "gopkg.in/yaml.v3" 23 | ) 24 | 25 | // Manifest represents a single kubernetes yaml manifest from a helm release. It could be from a 'template' command or 'get' command. 26 | type Manifest struct { 27 | Source string 28 | Kind string 29 | Metadata Metadata 30 | Content string 31 | } 32 | 33 | // Metadata only includes a Name field for a given resource. 34 | type Metadata struct { 35 | Name string 36 | } 37 | 38 | func (c *Client) GetManifests() (string, error) { 39 | var fullOutput string 40 | for _, release := range c.CourseFile.Releases { 41 | manifests, err := c.Helm.GetManifestString(release.Namespace, release.Name) 42 | if err != nil { 43 | return "", fmt.Errorf("error getting manifests for release %s - %s", release.Name, err) 44 | } 45 | fullOutput = fullOutput + manifests 46 | } 47 | return strings.TrimSuffix(fullOutput, "\n"), nil 48 | } 49 | 50 | // ManifestUnmarshal converts a manifest string that includes all resources from a chart 51 | // and breaks them up into their individual resource manifests. 52 | // 53 | // Returns a slice of Manifest structs. 54 | func ManifestUnmarshal(in string) ([]Manifest, error) { 55 | var manifests []Manifest 56 | dec := yaml.NewDecoder(bytes.NewReader([]byte(in))) 57 | for { 58 | var manifest Manifest 59 | err := dec.Decode(&manifest) 60 | if err == io.EOF { 61 | break 62 | } 63 | if err != nil { 64 | return nil, err 65 | } 66 | manifests = append(manifests, manifest) 67 | } 68 | return manifests, nil 69 | } 70 | 71 | // UnmarshalYAML satisfies the yaml.Unmarshaler interface for a Manifest object. 72 | // This ensures that a manifest object can properly pull the Source, Kind, and Metadata fields 73 | // and then populates the contents field with the raw yaml, not including the Source comment. 74 | func (m *Manifest) UnmarshalYAML(value *yaml.Node) error { 75 | if value.Kind != yaml.MappingNode { 76 | return fmt.Errorf("Manifest must contain YAML mapping, has %v", value.Kind) 77 | } 78 | for i := 0; i < len(value.Content); i += 2 { 79 | commentRe := regexp.MustCompile(`^# Source: (.*)$`) 80 | matchedComment := commentRe.FindStringSubmatch(value.Content[i].HeadComment) 81 | if len(matchedComment) > 0 { 82 | m.Source = matchedComment[1] 83 | } 84 | switch value.Content[i].Value { 85 | case "kind": 86 | if err := value.Content[i+1].Decode(&m.Kind); err != nil { 87 | return err 88 | } 89 | case "metadata": 90 | if err := value.Content[i+1].Decode(&m.Metadata); err != nil { 91 | return err 92 | } 93 | } 94 | } 95 | content := map[string]interface{}{} 96 | if err := value.Decode(&content); err != nil { 97 | return err 98 | } 99 | contentBytes, err := yaml.Marshal(content) 100 | if err != nil { 101 | return err 102 | } 103 | m.Content = string(contentBytes) 104 | return nil 105 | } 106 | -------------------------------------------------------------------------------- /pkg/reckoner/namespace.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "context" 17 | "encoding/json" 18 | 19 | "github.com/fairwindsops/reckoner/pkg/course" 20 | "github.com/fatih/color" 21 | "github.com/thoas/go-funk" 22 | v1 "k8s.io/api/core/v1" 23 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 24 | "k8s.io/apimachinery/pkg/types" 25 | ) 26 | 27 | // CreateNamespace creates a kubernetes namespace with the given annotations and labels 28 | func (c *Client) CreateNamespace(namespace string, annotations, labels map[string]string, runningNamespaceList *v1.NamespaceList) error { 29 | ns := &v1.Namespace{ 30 | ObjectMeta: metav1.ObjectMeta{ 31 | Name: namespace, 32 | Annotations: annotations, 33 | Labels: labels, 34 | }, 35 | } 36 | returnedNS, err := c.KubeClient.CoreV1().Namespaces().Create(context.TODO(), ns, metav1.CreateOptions{}) 37 | if err != nil { 38 | return err 39 | } 40 | runningNamespaceList.Items = append(runningNamespaceList.Items, *returnedNS) 41 | return nil 42 | } 43 | 44 | // PatchNamespace patches a kubernetes namespace with the given annotations and labels 45 | func (c *Client) PatchNamespace(namespace string, annotations, labels map[string]string) error { 46 | ns := &v1.Namespace{ 47 | ObjectMeta: metav1.ObjectMeta{ 48 | Annotations: annotations, 49 | Labels: labels, 50 | }, 51 | } 52 | data, err := json.Marshal(ns) 53 | if err != nil { 54 | return err 55 | } 56 | _, err = c.KubeClient.CoreV1().Namespaces().Patch(context.TODO(), namespace, types.StrategicMergePatchType, data, metav1.PatchOptions{}) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | return nil 62 | } 63 | 64 | // NamespaceManagement manages namespace names, annotations and labels 65 | func (c *Client) NamespaceManagement() error { 66 | if c.DryRun { 67 | color.Yellow("namespace management not run due to --dry-run: %v", c.DryRun) 68 | return nil 69 | } 70 | if !c.CreateNamespaces { 71 | color.Yellow("namespace management not run to do --create-namespaces=%t", c.CreateNamespaces) 72 | return nil 73 | } 74 | 75 | namespaces, err := c.KubeClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 76 | if err != nil { 77 | return err 78 | } 79 | for _, release := range c.CourseFile.Releases { 80 | err := c.CreateOrPatchNamespace(*release.NamespaceMgmt.Settings.Overwrite, release.Namespace, *release.NamespaceMgmt, namespaces) 81 | if err != nil { 82 | return err 83 | } 84 | 85 | } 86 | return nil 87 | } 88 | 89 | // CreateOrPatchNamespace creates or patches namespace based on the configurations 90 | func (c *Client) CreateOrPatchNamespace(overWrite bool, namespaceName string, namespaceMgmt course.NamespaceConfig, namespaces *v1.NamespaceList) error { 91 | ns := checkIfNamespaceExists(namespaces, namespaceName) 92 | var err error 93 | if ns != nil { 94 | annotations, labels := labelsAndAnnotationsToUpdate(overWrite, namespaceMgmt.Metadata.Annotations, namespaceMgmt.Metadata.Labels, ns) 95 | err = c.PatchNamespace(namespaceName, annotations, labels) 96 | } else { 97 | err = c.CreateNamespace(namespaceName, namespaceMgmt.Metadata.Annotations, namespaceMgmt.Metadata.Labels, namespaces) 98 | } 99 | return err 100 | } 101 | 102 | func checkIfNamespaceExists(nsList *v1.NamespaceList, nsName string) *v1.Namespace { 103 | nsInterface := funk.Find(nsList.Items, func(ns v1.Namespace) bool { 104 | return ns.Name == nsName 105 | }) 106 | if nsInterface != nil { 107 | namespace := nsInterface.(v1.Namespace) 108 | return &namespace 109 | } 110 | return nil 111 | } 112 | 113 | func labelsAndAnnotationsToUpdate(overwrite bool, annotations, labels map[string]string, ns *v1.Namespace) (finalAnnotations, finalLabels map[string]string) { 114 | if overwrite { 115 | return annotations, labels 116 | } 117 | finalLabels = filterMapString(labels, ns.Labels) 118 | finalAnnotations = filterMapString(annotations, ns.Annotations) 119 | return finalAnnotations, finalLabels 120 | } 121 | 122 | func filterMapString(newLabelsOrAnnotations, nsLabelOrAnnotations map[string]string) map[string]string { 123 | finalLabelsOrAnnotations := map[string]string{} 124 | keys := funk.Keys(newLabelsOrAnnotations).([]string) 125 | for _, key := range keys { 126 | if !funk.Contains(nsLabelOrAnnotations, key) { 127 | finalLabelsOrAnnotations[key] = newLabelsOrAnnotations[key] 128 | } 129 | } 130 | return finalLabelsOrAnnotations 131 | } 132 | -------------------------------------------------------------------------------- /pkg/reckoner/namespace_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "context" 17 | "testing" 18 | 19 | "github.com/stretchr/testify/assert" 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | "k8s.io/client-go/kubernetes/fake" 23 | ) 24 | 25 | func TestCreateNamespace(t *testing.T) { 26 | fakeKubeClinet := fake.NewSimpleClientset(&v1.Namespace{}) 27 | fakeClient := Client{ 28 | KubeClient: fakeKubeClinet, 29 | } 30 | 31 | name := "reckoner" 32 | annotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "true"} 33 | labels := map[string]string{"app.kubernetes.io/name": "ingress-nginx"} 34 | err := fakeClient.CreateNamespace(name, annotations, labels, &v1.NamespaceList{}) 35 | assert.NoError(t, err) 36 | 37 | namespace, err := fakeKubeClinet.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{}) 38 | assert.NoError(t, err) 39 | assert.Equal(t, name, namespace.Name) 40 | assert.NoError(t, err) 41 | assert.Equal(t, labels, namespace.Labels) 42 | assert.Equal(t, annotations, namespace.Annotations) 43 | 44 | } 45 | 46 | func TestPatchNamespace(t *testing.T) { 47 | fakeKubeClinet := fake.NewSimpleClientset(&v1.Namespace{}) 48 | fakeClient := Client{ 49 | KubeClient: fakeKubeClinet, 50 | } 51 | 52 | name := "reckoner" 53 | annotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "true"} 54 | labels := map[string]string{"app.kubernetes.io/name": "ingress-nginx"} 55 | err := fakeClient.CreateNamespace(name, annotations, labels, &v1.NamespaceList{}) 56 | assert.NoError(t, err) 57 | 58 | newAnnotations := map[string]string{"service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0"} 59 | newLabels := map[string]string{"istio-injection": "enabled"} 60 | err = fakeClient.PatchNamespace(name, newAnnotations, newLabels) 61 | assert.NoError(t, err) 62 | 63 | updatedAnnotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "true", "service.beta.kubernetes.io/aws-load-balancer-internal": "0.0.0.0/0"} 64 | updatedLabels := map[string]string{"app.kubernetes.io/name": "ingress-nginx", "istio-injection": "enabled"} 65 | updatedNamespace, err := fakeKubeClinet.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{}) 66 | assert.NoError(t, err) 67 | assert.Equal(t, name, updatedNamespace.Name) 68 | assert.Equal(t, updatedLabels, updatedNamespace.Labels) 69 | assert.Equal(t, updatedAnnotations, updatedNamespace.Annotations) 70 | 71 | } 72 | 73 | func TestCheckIfNamespaceExist(t *testing.T) { 74 | fakeKubeClinet := fake.NewSimpleClientset(&v1.Namespace{}) 75 | fakeClient := Client{ 76 | KubeClient: fakeKubeClinet, 77 | } 78 | 79 | name := "reckoner" 80 | annotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "true"} 81 | labels := map[string]string{"app.kubernetes.io/name": "ingress-nginx"} 82 | err := fakeClient.CreateNamespace(name, annotations, labels, &v1.NamespaceList{}) 83 | assert.NoError(t, err) 84 | 85 | nsList, err := fakeClient.KubeClient.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{}) 86 | assert.NoError(t, err) 87 | 88 | ns := checkIfNamespaceExists(nsList, name) 89 | assert.NotNil(t, ns) 90 | assert.Equal(t, name, ns.Name) 91 | 92 | ns = checkIfNamespaceExists(nsList, "emptyns") 93 | assert.Nil(t, ns) 94 | } 95 | 96 | func TestLabelsAndAnnotationsToUpdate(t *testing.T) { 97 | fakeKubeClinet := fake.NewSimpleClientset(&v1.Namespace{}) 98 | fakeClient := Client{ 99 | KubeClient: fakeKubeClinet, 100 | } 101 | 102 | name := "reckoner" 103 | annotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "true"} 104 | labels := map[string]string{"app.kubernetes.io/name": "ingress-nginx"} 105 | err := fakeClient.CreateNamespace(name, annotations, labels, &v1.NamespaceList{}) 106 | assert.NoError(t, err) 107 | 108 | newLabels := map[string]string{"app.kubernetes.io/name": "ingress-nginx-new", "label-key-1": "label-value-1"} 109 | newAnnotations := map[string]string{"nginx.ingress.kubernetes.io/http2-push-preload": "false"} 110 | ns, err := fakeClient.KubeClient.CoreV1().Namespaces().Get(context.TODO(), name, metav1.GetOptions{}) 111 | assert.NoError(t, err) 112 | 113 | updatedAnnotations, updatedLabels := labelsAndAnnotationsToUpdate(true, newAnnotations, newLabels, ns) 114 | assert.Equal(t, newAnnotations, updatedAnnotations) 115 | assert.Equal(t, newLabels, updatedLabels) 116 | 117 | expectedLabels := map[string]string{"label-key-1": "label-value-1"} 118 | expectedAnnotations := map[string]string{} 119 | updatedAnnotations, updatedLabels = labelsAndAnnotationsToUpdate(false, newAnnotations, newLabels, ns) 120 | assert.Equal(t, expectedLabels, updatedLabels) 121 | assert.Equal(t, expectedAnnotations, updatedAnnotations) 122 | } 123 | -------------------------------------------------------------------------------- /pkg/reckoner/plot_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "os" 17 | "testing" 18 | 19 | "github.com/fairwindsops/reckoner/pkg/course" 20 | "github.com/stretchr/testify/assert" 21 | ) 22 | 23 | const ( 24 | baseDir = "path/to/chart" 25 | namespace = "basic-ns" 26 | version = "v0.0.0" 27 | valuesFile = "a-values-file.yaml" 28 | helmChart = "helmchart" 29 | helmRelease = "basic-release" 30 | helmRepository = "helmrepo" 31 | ) 32 | 33 | func Test_buildHelmArgs(t *testing.T) { 34 | type args struct { 35 | command string 36 | release course.Release 37 | additionalArgs []string 38 | } 39 | tests := []struct { 40 | name string 41 | baseDir string 42 | args args 43 | want []string 44 | wantErr bool 45 | }{ 46 | { 47 | name: "basic template", 48 | baseDir: baseDir, 49 | args: args{ 50 | command: "template", 51 | release: course.Release{ 52 | Name: helmRelease, 53 | Namespace: namespace, 54 | Chart: helmChart, 55 | Version: version, 56 | Repository: helmRepository, 57 | Files: []string{ 58 | valuesFile, 59 | }, 60 | }, 61 | }, 62 | want: []string{ 63 | "template", 64 | helmRelease, 65 | helmRepository + "/" + helmChart, 66 | "--values=" + baseDir + "/" + valuesFile, 67 | "--namespace=" + namespace, 68 | "--version=" + version, 69 | }, 70 | wantErr: false, 71 | }, 72 | { 73 | name: "basic upgrade", 74 | baseDir: baseDir, 75 | args: args{ 76 | command: "upgrade", 77 | release: course.Release{ 78 | Name: helmRelease, 79 | Namespace: namespace, 80 | Chart: helmChart, 81 | Version: version, 82 | Repository: helmRepository, 83 | Files: []string{ 84 | valuesFile, 85 | }, 86 | }, 87 | }, 88 | want: []string{ 89 | "upgrade", 90 | "--install", 91 | helmRelease, 92 | helmRepository + "/" + helmChart, 93 | "--values=" + baseDir + "/" + valuesFile, 94 | "--namespace=" + namespace, 95 | "--version=" + version, 96 | }, 97 | wantErr: false, 98 | }, 99 | { 100 | name: "additional args", 101 | baseDir: baseDir, 102 | args: args{ 103 | command: "upgrade", 104 | release: course.Release{ 105 | Name: helmRelease, 106 | Namespace: namespace, 107 | Chart: helmChart, 108 | Version: version, 109 | Repository: helmRepository, 110 | Files: []string{ 111 | valuesFile, 112 | }, 113 | }, 114 | additionalArgs: []string{ 115 | "--atomic", 116 | }, 117 | }, 118 | want: []string{ 119 | "upgrade", 120 | "--install", 121 | "--atomic", 122 | helmRelease, 123 | helmRepository + "/" + helmChart, 124 | "--values=" + baseDir + "/" + valuesFile, 125 | "--namespace=" + namespace, 126 | "--version=" + version, 127 | }, 128 | wantErr: false, 129 | }, 130 | } 131 | for _, tt := range tests { 132 | t.Run(tt.name, func(t *testing.T) { 133 | got, _, err := buildHelmArgs(tt.args.command, tt.baseDir, tt.args.release, tt.args.additionalArgs) 134 | if tt.wantErr { 135 | assert.Error(t, err) 136 | } else { 137 | assert.NoError(t, err) 138 | assert.EqualValues(t, tt.want, got) 139 | } 140 | }) 141 | } 142 | } 143 | 144 | func Test_makeTempValuesFile(t *testing.T) { 145 | tests := []struct { 146 | name string 147 | values map[string]interface{} 148 | want string 149 | wantErr bool 150 | }{ 151 | { 152 | name: "basic", 153 | values: map[string]interface{}{ 154 | "foo": "bar", 155 | }, 156 | want: "foo: bar\n", 157 | wantErr: false, 158 | }, 159 | } 160 | for _, tt := range tests { 161 | t.Run(tt.name, func(t *testing.T) { 162 | got, err := makeTempValuesFile(tt.values) 163 | if tt.wantErr { 164 | assert.Error(t, err) 165 | } else { 166 | assert.NoError(t, err) 167 | valuesFile, err := os.ReadFile(got.Name()) 168 | assert.NoError(t, err) 169 | assert.EqualValues(t, tt.want, string(valuesFile)) 170 | } 171 | os.Remove(got.Name()) 172 | }) 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /pkg/reckoner/split.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "bytes" 17 | "errors" 18 | "io" 19 | "os" 20 | "strings" 21 | 22 | "gopkg.in/yaml.v3" 23 | ) 24 | 25 | type k8sMetaBasic struct { 26 | Kind string `yaml:"kind"` 27 | Metadata struct { 28 | Name string `yaml:"name"` 29 | Namespace string `yaml:"namespace,omitempty"` 30 | } `yaml:"metadata"` 31 | } 32 | 33 | // splitYAML will take a slice of bytes, assumed to be a multi-page (1+) YAML document, 34 | // and append each page/object it finds in the document to a slice which is 35 | // returned at the end of processing. Each object is typed as a slice of bytes. 36 | // Therefore, this function returns a slice of a slice of bytes. 37 | // splitYAML was shamelessly ripped from https://go.dev/play/p/MZNwxdUzxPo, which was 38 | // found via github issue https://github.com/go-yaml/yaml/pull/301#issuecomment-792871300 39 | func splitYAML(in []byte) (out [][]byte, err error) { 40 | decoder := yaml.NewDecoder(bytes.NewReader(in)) // create a YAML decoder to run through our document 41 | 42 | for { // keep going until we run out of bytes to process (end-of-file/EOF) 43 | var value interface{} // something we can use to decode and marshal 44 | var b bytes.Buffer // used for encoding & return 45 | 46 | err = decoder.Decode(&value) // attempt to decode a page in the document 47 | if err == io.EOF { // we ran out of pages/objects/bytes 48 | break // stop trying to process anything 49 | } 50 | // we might have bytes, but we still need to check if we could decode successfully 51 | if err != nil { // check for errors 52 | return nil, err // bubble up 53 | } 54 | 55 | yamlEncoder := yaml.NewEncoder(&b) // create an encoder to handle custom configuration 56 | yamlEncoder.SetIndent(2) // people expect two-space indents instead of the default four 57 | err = yamlEncoder.Encode(&value) // encode proper YAML into slice of bytes 58 | if err != nil { // check for errors 59 | return nil, err // bubble up 60 | } 61 | 62 | // we believe we have a valid YAML object 63 | out = append(out, bytes.TrimSpace(b.Bytes())) // so append it to the list to be returned later 64 | } 65 | 66 | return out, nil // list of YAML objects, each a []byte 67 | } 68 | 69 | func writeYAML(in []byte, filename string) (err error) { 70 | file, err := os.Create(filename) 71 | if err != nil { 72 | return err 73 | } 74 | 75 | _, err = file.Write(in) 76 | if err != nil { 77 | return err 78 | } 79 | 80 | return err 81 | } 82 | 83 | func (c *Client) WriteSplitYaml(in []byte, basePath string, releaseName string) (err error) { 84 | releasePath := basePath + "/" + releaseName 85 | 86 | objects, err := splitYAML(in) // get list of YAML documents 87 | if err != nil { // check for errors 88 | return err // bubble up 89 | } 90 | 91 | // ensure "${--output-dir}/release_name" exists 92 | for _, dir := range []string{basePath, releasePath} { 93 | if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) { 94 | err := os.Mkdir(dir, os.ModePerm) 95 | if err != nil { 96 | return err 97 | } 98 | } 99 | } 100 | 101 | for _, object := range objects { // loop through documents 102 | meta := k8sMetaBasic{} 103 | err = yaml.Unmarshal(object, &meta) 104 | if err != nil { 105 | return err 106 | } 107 | 108 | if len(meta.Kind) < 1 || len(meta.Metadata.Name) < 1 { // skip empty objects; these are the required fields in k8sMetaBasic{} 109 | continue 110 | } 111 | 112 | // This section will build out the name of the file based on what was found inside of the object. 113 | // For example, an object with: 114 | // kind: Deployment 115 | // metadata: 116 | // name: cool-app-api 117 | // namespace: cool-app 118 | // will have the filename of: --output-dir/release_name/cool-app_deployment_cool-app-api.yaml 119 | // We also replace colons with underscores for windows compatibility since RBAC names may have colons 120 | filename := releasePath + "/" // path for the release 121 | if len(meta.Metadata.Namespace) > 0 { // only add the namespace to the filename when it's found in the object 122 | filename = filename + strings.ToLower(meta.Metadata.Namespace) + "_" // lowercased for simplicity 123 | } // continue building the rest of the filename 124 | filename = filename + strings.ToLower(meta.Kind) + "_" + strings.ToLower(meta.Metadata.Name) + ".yaml" // lowercased for simplicity 125 | filename = strings.ReplaceAll(filename, ":", "_") // replace colons with underscores for windows compatibility; RBAC names may have colons 126 | 127 | err = writeYAML(object, filename) // write out 128 | if err != nil { 129 | return err 130 | } 131 | } 132 | 133 | return err 134 | } 135 | -------------------------------------------------------------------------------- /pkg/reckoner/split_test.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "testing" 17 | 18 | "github.com/stretchr/testify/assert" 19 | ) 20 | 21 | func Test_splitYAML(t *testing.T) { 22 | tests := []struct { 23 | name string 24 | yamlFile []byte 25 | want [][]byte 26 | wantNumberOfDocs int 27 | wantErr bool 28 | }{ 29 | { 30 | name: "one_document", 31 | yamlFile: []byte("---\nfield: \"value\"\n"), 32 | want: [][]byte{ 33 | []byte("field: value"), 34 | }, 35 | wantErr: false, 36 | }, 37 | { 38 | name: "multi_documents", 39 | yamlFile: []byte("---\nfield:\n nested: value\n---\nanother: second\n"), 40 | want: [][]byte{ 41 | []byte("field:\n nested: value"), 42 | []byte("another: second"), 43 | }, 44 | wantErr: false, 45 | }, 46 | } 47 | 48 | for _, tt := range tests { 49 | t.Run(tt.name, func(t *testing.T) { 50 | docs, err := splitYAML(tt.yamlFile) 51 | if (err != nil) != tt.wantErr { 52 | t.Errorf("splitYAML() error = %v, wantErr %v", err, tt.wantErr) 53 | return 54 | } 55 | if len(tt.want) != len(docs) { 56 | t.Errorf("splitYAML() produced different number of YAML documents than expected; wanted = %v, got %v", tt.wantNumberOfDocs, len(docs)) 57 | } 58 | 59 | assert.Equal(t, tt.want, docs) 60 | }) 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /pkg/reckoner/update.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package reckoner 14 | 15 | import ( 16 | "github.com/fairwindsops/reckoner/pkg/course" 17 | "github.com/fatih/color" 18 | ) 19 | 20 | func (c *Client) Update() error { 21 | updatedReleases := []*course.Release{} 22 | for idx, release := range c.CourseFile.Releases { 23 | thisReleaseDiff, err := c.diffRelease(release.Name, release.Namespace) 24 | if err != nil { 25 | if c.Continue() { 26 | color.Red("error with release %s: %s, continuing.", release.Name, err.Error()) 27 | continue 28 | } 29 | return err 30 | } 31 | if thisReleaseDiff != "" { 32 | color.Yellow("Update available for %s in namespace %s. Added to plot list.", release.Name, release.Namespace) 33 | updatedReleases = append(updatedReleases, c.CourseFile.Releases[idx]) 34 | continue 35 | } 36 | color.Green("No update necessary for %s in namespace %s.", release.Name, release.Namespace) 37 | } 38 | c.CourseFile.Releases = updatedReleases 39 | err := c.Plot() 40 | if err != nil { 41 | return err 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /pkg/secrets/secrets.go: -------------------------------------------------------------------------------- 1 | // Licensed under the Apache License, Version 2.0 (the "License"); 2 | // you may not use this file except in compliance with the License. 3 | // You may obtain a copy of the License at 4 | // 5 | // http://www.apache.org/licenses/LICENSE-2.0 6 | // 7 | // Unless required by applicable law or agreed to in writing, software 8 | // distributed under the License is distributed on an "AS IS" BASIS, 9 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 10 | // See the License for the specific language governing permissions and 11 | // limitations under the License 12 | 13 | package secrets 14 | 15 | import "os" 16 | 17 | type Getter interface { 18 | Get(key string) (string, error) 19 | } 20 | 21 | type Backend struct { 22 | getter Getter 23 | } 24 | 25 | // NewSecretBackend creates a new SecretBackend based on a concrete secrets.Getter implementation. 26 | func NewSecretBackend(getter Getter) *Backend { 27 | return &Backend{getter: getter} 28 | } 29 | 30 | // SetEnv populates the current ENV with the given secret key by fetching it from the SecretBackend and calling os.Setenv. 31 | func (b Backend) SetEnv(key string) error { 32 | value, err := b.get(key) 33 | if err != nil { 34 | return err 35 | } 36 | return os.Setenv(key, value) 37 | } 38 | 39 | // get fetches a secret from the implemented SecretBackend. 40 | func (b Backend) get(key string) (string, error) { 41 | return b.getter.Get(key) 42 | } 43 | --------------------------------------------------------------------------------