├── .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 |
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 |
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 |
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 |
2 |
3 |
4 |
13 |
14 |
15 |
16 |
17 |
28 |
29 |
46 |
--------------------------------------------------------------------------------
/docs/README.md:
--------------------------------------------------------------------------------
1 |
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 |
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 |
26 |
--------------------------------------------------------------------------------
/img/reckoner-logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
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 |
--------------------------------------------------------------------------------