├── .github └── workflows │ ├── auto-release.yaml │ ├── generate-manifests-demos.yaml │ └── tests.yaml ├── .gitignore ├── .golangci.yaml ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── LICENSE.txt ├── Makefile ├── README.md ├── demo ├── .zz.auto-generated │ ├── prod-app-group-1 │ │ └── manifest.yaml │ ├── prod-cluster │ │ └── manifest.yaml │ ├── prod-service-bar │ │ └── manifest.yaml │ ├── prod-service-foo │ │ └── manifest.yaml │ ├── test-app-group-1 │ │ └── manifest.yaml │ ├── test-cluster │ │ └── manifest.yaml │ ├── test-service-bar │ │ └── manifest.yaml │ ├── test-service-baz │ │ └── manifest.yaml │ └── test-service-foo │ │ └── manifest.yaml ├── README.md ├── bootstrap │ ├── prod-cluster.yaml │ └── test-cluster.yaml ├── charts │ ├── app-of-apps │ │ ├── Chart.yaml │ │ └── templates │ │ │ ├── app-of-apps.tpl │ │ │ ├── apps.yaml │ │ │ └── service.tpl │ └── service │ │ ├── Chart.yaml │ │ ├── templates │ │ └── deployment.yaml │ │ └── values.yaml └── overrides │ ├── app-of-apps │ ├── prod-app-group-1.yaml │ └── test-app-group-1.yaml │ ├── bootstrap │ ├── prod-cluster.yaml │ └── test-cluster.yaml │ └── service │ ├── bar │ ├── base.yaml │ ├── prod.yaml │ └── test.yaml │ ├── baz │ ├── base.yaml │ └── test.yaml │ └── foo │ ├── base.yaml │ ├── prod.yaml │ └── test.yaml ├── go.mod ├── go.sum ├── hashing.go ├── hashing_test.go ├── main.go └── pkg ├── helm ├── helm.go ├── helm_test.go └── test_files │ ├── SymDir │ ├── crdData_multiple_crd_testfile.yaml │ ├── crdData_override_testfile.yaml │ ├── crdData_override_testfile_sym_link.yaml │ ├── crdData_testfile.yaml │ ├── crdData_testfile_2.yaml │ ├── crdData_testfile_3.yaml │ ├── empty_manifest.yaml │ ├── hash_testfile.sum │ └── nonSymDir │ └── .gitkeep └── kustomize └── kustomize.go /.github/workflows/auto-release.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: Auto Release 3 | 4 | on: push 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: checkout 11 | uses: actions/checkout@v3 12 | - name: set up go version 13 | uses: actions/setup-go@v3 14 | - name: Go Build x64 15 | run: go build -o mani-diffy 16 | - name: Go Build arm64 17 | run: GOOS=darwin GOARCH=arm64 go build -o mani-diffy-darwin-arm64 18 | - name: Create Release Text 19 | run: echo ${{ github.sha }} > Release.txt 20 | - name: Test Build x64 21 | run: file mani-diffy | grep "x86-64" 22 | - name: Test Build arm64 23 | run: file mani-diffy-darwin-arm64 | grep "arm64" 24 | - name: Release 25 | uses: softprops/action-gh-release@v1 26 | if: startsWith(github.ref, 'refs/tags/') 27 | with: 28 | files: | 29 | mani-diffy 30 | mani-diffy-darwin-arm64 31 | Release.txt 32 | -------------------------------------------------------------------------------- /.github/workflows/generate-manifests-demos.yaml: -------------------------------------------------------------------------------- 1 | name: Generate manifests for demo 2 | on: [push] 3 | 4 | env: 5 | USE_RELEASE: true 6 | RELEASE_TAG: v0.1.1 7 | 8 | jobs: 9 | build-and-run: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v2 14 | 15 | - name: Set up Git 16 | run: | 17 | git config --global user.name 'Bot' 18 | git config --global user.email 'bot@users.noreply.github.com' 19 | 20 | - name: Download mani-diffy binary 21 | if: env.USE_RELEASE == 'true' 22 | uses: actions/github-script@v5 23 | with: 24 | script: | 25 | const { owner, repo } = context.repo 26 | const release = await github.rest.repos.getReleaseByTag({ 27 | owner, 28 | repo, 29 | tag: core.getInput('release_tag') 30 | }) 31 | const asset = release.data.assets.find(asset => asset.name === 'mani-diffy') 32 | const downloadUrl = asset.browser_download_url 33 | await exec.exec('wget', ['-O', 'mani-diffy', downloadUrl]) 34 | await exec.exec('chmod', ['+x', 'mani-diffy']) 35 | release_tag: ${{ env.RELEASE_TAG }} 36 | 37 | - name: Compile 38 | if: env.USE_RELEASE == 'false' 39 | run: | 40 | make build-binaries 41 | 42 | - name: Run for demo 43 | run: | 44 | cd demo 45 | rm -rf .zz.auto-generated 46 | ../mani-diffy -hash-store=json 47 | 48 | - name: Commit and push changes to /demo 49 | run: | 50 | if git diff --quiet; then 51 | echo "No changes to commit" 52 | else 53 | git add . 54 | git commit -m "Some manifests have changed in the demo." 55 | git push 56 | fi 57 | cd .. 58 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | name: tests 3 | 4 | on: 5 | pull_request: 6 | types: [assigned, opened, synchronize, reopened, ready_for_review] 7 | 8 | jobs: 9 | go_test: 10 | # strategy: 11 | # matrix: 12 | # go-versions: [1.18.x] 13 | # os: [ubuntu-latest, macos-latest] 14 | # runs-on: ${{ matrix.os }} 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: checkout 18 | uses: actions/checkout@v3 19 | - name: set up go version 20 | uses: actions/setup-go@v3 21 | with: 22 | # go-version: ${{ matrix.go-versions }} 23 | go-version: '1.20' 24 | - name: run go tests 25 | run: | 26 | go test -v ./... 27 | golangci: 28 | # https://github.com/golangci/golangci-lint-action 29 | name: lint 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/setup-go@v3 33 | with: 34 | go-version: '1.20' 35 | - uses: actions/checkout@v3 36 | - name: golangci-lint 37 | uses: golangci/golangci-lint-action@v3 38 | with: 39 | version: v1.53 40 | # use config in .golangci.yaml to configure the action further 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ignore binaries build during testing 2 | mani-diffy* 3 | hashes.json 4 | hash.sum 5 | 6 | .idea 7 | .DS_Store -------------------------------------------------------------------------------- /.golangci.yaml: -------------------------------------------------------------------------------- 1 | # https://golangci-lint.run/usage/configuration/#config-file 2 | run: 3 | allow-parallel-runners: true 4 | timeout: 5m 5 | go: '1.20' 6 | skip-dirs-use-default: false 7 | 8 | linters: 9 | enable: 10 | - errcheck 11 | - errorlint 12 | - exportloopref 13 | - gocritic 14 | - gofmt 15 | - goimports 16 | - gosec 17 | - govet 18 | - misspell 19 | - revive 20 | - staticcheck 21 | - tenv 22 | - unconvert 23 | - unused 24 | - unparam 25 | 26 | issues: 27 | # Excluding configuration per-path, per-linter, per-text and per-source 28 | exclude-rules: 29 | - text: "G306:" 30 | linters: 31 | - gosec 32 | - text: "G204:" 33 | linters: 34 | - gosec 35 | - text: "G112:" 36 | linters: 37 | - gosec 38 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @chime/maintainers 2 | -------------------------------------------------------------------------------- /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 "support@chime.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 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2023 Chime Financial 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | test: 2 | go test ./... 3 | 4 | test-verbose: 5 | go test -v ./... 6 | 7 | benchmark: 8 | go test -bench=. 9 | 10 | benchmark-all: 11 | go test ./... -bench=. 12 | 13 | lint: 14 | golangci-lint run 15 | 16 | build-binaries: 17 | go build -o mani-diffy 18 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # mani-diffy 2 | 3 | ![Tests](https://github.com/chime/mani-diffy/actions/workflows/tests.yaml/badge.svg) 4 | 5 | This program walks a hierarchy of Argo CD Application templates, renders Kubernetes manifests from the input templates, and posts the rendered files back for the user to review and validate. 6 | 7 | It is designed to be called from a CI job within a pull request, enabling the author to update templates and see the resulting manifests directly within the pull request before the changes are applied to the Kubernetes cluster. 8 | 9 | The rendered manifests are kept within the repository, making diffs between revisions easy to parse, dramatically improving safety when updating complex application templates. 10 | 11 | --- 12 | ## How it works: 13 | 1. A user makes their desired change to the application's templates (charts, overrides, etc) and submits a PR with the change. 14 | 2. A Github action executes `mani-diffy`, rendering all manifests affected by the change. 15 | 3. Any updated manifests are submitted back to the same PR as a new commit. 16 | 4. The author and any reviewers will be able to review the diff between the new changes and the previous version of the manifests. 17 | 18 | # See it in action 19 | 20 | 🫵 Submit a PR where you make a change to the overrides of the [`demo`](demo/README.md), and you'll see the [Github action]( [README](../../.github/workflows/generate-manifests-demos.yaml)) add a commit to your PR with the resulting changes. 21 | 22 | 1 23 | 2 24 | 25 | # See it in action in a video ! 26 | 27 | In this screen recording a pull request is opened to make the following changes to the [`demo`](demo/README.md): 28 | 29 | 1. Bump the count of pods for the `foo` service in the prod cluster 30 | 2. Add an annotation to all services 31 | 32 | https://github.com/chime/mani-diffy/assets/9005904/6c496996-f7af-4932-bf5d-01a5b57bbd99 33 | 34 | 35 | ## Post Renderers 36 | 37 | `mani-diffy` also supports something called a "post renderer". This is a command that will be called immediately after an Application is rendered. This can be used to run linting, or alter the output of the generated manifest. 38 | 39 | ``` 40 | mani-diffy -post-renderer="bin/post-render" -output=.zz-auto-generated 41 | ``` 42 | 43 | The command will be called with the output directory as the first argument (e.g. `.zz-auto-generated/`) 44 | 45 | --- 46 | 47 | ## Pre-requisites 48 | 49 | This is for a new user that is looking to use mani-diffy on a new repo. 50 | 51 | In order to make use of mani-diffy on the repo that holds all of your ArgoCD applications the pre-requisites are: 52 | 53 | - You have a "root" Application 54 | - All of your charts and Application manifests live in the same repo. 55 | 56 | `mani-diffy` itself makes no assumptions about how the repo is structured, as long as it can successfully render the charts it encounters while walking the Application tree. 57 | 58 | However, you may find it useful to organize your repo similarly to the demo app, with 3 key directories : 59 | 60 | 1. a "root" or "bootstrap" directory that holds all the ArgoCD applications manifests. 61 | 2. a "charts" directory that contains all the helm charts needed for the ArgoCD applications. 62 | 3. a "rendered" or "generated" directory, where all rendered charts will be committed. 63 | 64 | You can see an example of that in the [`demo`](demo/README.md) directory. 65 | 66 | # FAQ 67 | 68 | Q: Is ArgoCD using the rendered manifests in `.zz.auto-generated` ? 69 | 70 | A: No, ArgoCD renders the charts itself. There is no expected discrepancy between the manifest files rendered by mani-diffy and by ArgoCD as long as they are using the same version of Helm. 71 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/prod-app-group-1/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: app-of-apps/templates/apps.yaml 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: prod-service-bar 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | repoURL: https://github.com/chime/mani-diffy.git 14 | path: charts/service 15 | helm: 16 | version: v3 17 | parameters: 18 | - name: env 19 | value: prod 20 | valueFiles: 21 | - ../../overrides/service/bar/base.yaml 22 | - ../../overrides/service/bar/prod.yaml 23 | syncPolicy: 24 | automated: {} 25 | --- 26 | # Source: app-of-apps/templates/apps.yaml 27 | apiVersion: argoproj.io/v1alpha1 28 | kind: Application 29 | metadata: 30 | name: prod-service-foo 31 | spec: 32 | destination: 33 | namespace: argocd 34 | server: https://kubernetes.default.svc 35 | project: default 36 | source: 37 | repoURL: https://github.com/chime/mani-diffy.git 38 | path: charts/service 39 | helm: 40 | version: v3 41 | parameters: 42 | - name: env 43 | value: prod 44 | valueFiles: 45 | - ../../overrides/service/foo/base.yaml 46 | - ../../overrides/service/foo/prod.yaml 47 | syncPolicy: 48 | automated: {} 49 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/prod-cluster/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: app-of-apps/templates/apps.yaml 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: prod-app-group-1 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | repoURL: https://github.com/chime/mani-diffy.git 14 | targetRevision: HEAD 15 | 16 | path: charts/app-of-apps 17 | helm: 18 | parameters: 19 | - name: renderBaseDir 20 | value: /zz.auto-generated/root 21 | - name: cluster 22 | value: use1-prod-eks-cluster 23 | - name: env 24 | value: prod 25 | - name: ns 26 | value: app-group-1 27 | valueFiles: 28 | - ../../overrides/app-of-apps/prod-app-group-1.yaml 29 | 30 | syncPolicy: 31 | automated: {} 32 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/prod-service-bar/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: service/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: bar-web 7 | annotations: 8 | appTag: latest 9 | spec: 10 | replicas: 20 11 | selector: 12 | matchLabels: 13 | app: bar-web 14 | template: 15 | metadata: 16 | labels: 17 | app: bar-web 18 | spec: 19 | containers: 20 | - name: web 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: [] 23 | resources: 24 | limits: 25 | memory: 200Mi 26 | cpu: 100m 27 | --- 28 | # Source: service/templates/deployment.yaml 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: bar-worker 33 | annotations: 34 | appTag: latest 35 | spec: 36 | replicas: 1 37 | selector: 38 | matchLabels: 39 | app: bar-worker 40 | template: 41 | metadata: 42 | labels: 43 | app: bar-worker 44 | spec: 45 | containers: 46 | - name: worker 47 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 48 | command: [] 49 | resources: 50 | limits: 51 | memory: 200Mi 52 | cpu: 100m 53 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/prod-service-foo/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: service/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: foo-web 7 | annotations: 8 | appTag: latest 9 | spec: 10 | replicas: 10 11 | selector: 12 | matchLabels: 13 | app: foo-web 14 | template: 15 | metadata: 16 | labels: 17 | app: foo-web 18 | spec: 19 | containers: 20 | - name: web 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: [] 23 | resources: 24 | limits: 25 | memory: 32Mi 26 | cpu: 10m 27 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/test-app-group-1/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: app-of-apps/templates/apps.yaml 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: test-service-bar 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | repoURL: https://github.com/chime/mani-diffy.git 14 | path: charts/service 15 | helm: 16 | version: v3 17 | parameters: 18 | - name: env 19 | value: test 20 | valueFiles: 21 | - ../../overrides/service/bar/base.yaml 22 | - ../../overrides/service/bar/test.yaml 23 | syncPolicy: 24 | automated: {} 25 | --- 26 | # Source: app-of-apps/templates/apps.yaml 27 | apiVersion: argoproj.io/v1alpha1 28 | kind: Application 29 | metadata: 30 | name: test-service-baz 31 | spec: 32 | destination: 33 | namespace: argocd 34 | server: https://kubernetes.default.svc 35 | project: default 36 | source: 37 | repoURL: https://github.com/chime/mani-diffy.git 38 | path: charts/service 39 | helm: 40 | version: v3 41 | parameters: 42 | - name: env 43 | value: test 44 | valueFiles: 45 | - ../../overrides/service/baz/base.yaml 46 | - ../../overrides/service/baz/test.yaml 47 | syncPolicy: 48 | automated: {} 49 | --- 50 | # Source: app-of-apps/templates/apps.yaml 51 | apiVersion: argoproj.io/v1alpha1 52 | kind: Application 53 | metadata: 54 | name: test-service-foo 55 | spec: 56 | destination: 57 | namespace: argocd 58 | server: https://kubernetes.default.svc 59 | project: default 60 | source: 61 | repoURL: https://github.com/chime/mani-diffy.git 62 | path: charts/service 63 | helm: 64 | version: v3 65 | parameters: 66 | - name: env 67 | value: test 68 | valueFiles: 69 | - ../../overrides/service/foo/base.yaml 70 | - ../../overrides/service/foo/test.yaml 71 | syncPolicy: 72 | automated: {} 73 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/test-cluster/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: app-of-apps/templates/apps.yaml 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: test-app-group-1 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | repoURL: https://github.com/chime/mani-diffy.git 14 | targetRevision: HEAD 15 | 16 | path: charts/app-of-apps 17 | helm: 18 | parameters: 19 | - name: renderBaseDir 20 | value: /zz.auto-generated/root 21 | - name: cluster 22 | value: use1-test-eks-cluster 23 | - name: env 24 | value: test 25 | - name: ns 26 | value: app-group-1 27 | valueFiles: 28 | - ../../overrides/app-of-apps/test-app-group-1.yaml 29 | 30 | syncPolicy: 31 | automated: {} 32 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/test-service-bar/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: service/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: bar-web 7 | annotations: 8 | appTag: latest 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchLabels: 13 | app: bar-web 14 | template: 15 | metadata: 16 | labels: 17 | app: bar-web 18 | spec: 19 | containers: 20 | - name: web 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: [] 23 | resources: 24 | limits: 25 | memory: 200Mi 26 | cpu: 100m 27 | --- 28 | # Source: service/templates/deployment.yaml 29 | apiVersion: apps/v1 30 | kind: Deployment 31 | metadata: 32 | name: bar-worker 33 | annotations: 34 | appTag: latest 35 | spec: 36 | replicas: 1 37 | selector: 38 | matchLabels: 39 | app: bar-worker 40 | template: 41 | metadata: 42 | labels: 43 | app: bar-worker 44 | spec: 45 | containers: 46 | - name: worker 47 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 48 | command: [] 49 | resources: 50 | limits: 51 | memory: 200Mi 52 | cpu: 100m 53 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/test-service-baz/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: service/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: baz-web 7 | annotations: 8 | appTag: latest 9 | spec: 10 | replicas: 1 11 | selector: 12 | matchLabels: 13 | app: baz-web 14 | template: 15 | metadata: 16 | labels: 17 | app: baz-web 18 | spec: 19 | containers: 20 | - name: web 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: [] 23 | resources: 24 | limits: 25 | memory: 32Mi 26 | cpu: 10m 27 | -------------------------------------------------------------------------------- /demo/.zz.auto-generated/test-service-foo/manifest.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # Source: service/templates/deployment.yaml 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: foo-web 7 | annotations: 8 | appTag: latest 9 | spec: 10 | replicas: 2 11 | selector: 12 | matchLabels: 13 | app: foo-web 14 | template: 15 | metadata: 16 | labels: 17 | app: foo-web 18 | spec: 19 | containers: 20 | - name: web 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: [] 23 | resources: 24 | limits: 25 | memory: 32Mi 26 | cpu: 10m 27 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to `mani-diffy`'s demo 2 | 3 | Let's demonstrate how `mani-diffy` can be used to compare rendered charts before they are merged and deployed. 4 | 5 | # Directory structure 6 | 7 | This directory is a example of a repository structure that used to implement the [App of Apps pattern](https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/#app-of-apps-pattern) with Argo CD. 8 | 9 | Let's take a tour ... 10 | 11 | ``` 12 | ├── README.md 13 | ├── bootstrap 14 | │   ├── prod-cluster.yaml 15 | │   └── test-cluster.yaml 16 | ├── charts 17 | │   ├── app-of-apps 18 | │   └── service 19 | ├── overrides 20 | │ ├── app-of-apps 21 | │ ├── bootstrap 22 | │ └── service 23 | └── .zz.auto-generated 24 |    ├── hashes.json 25 |    ├── prod-app-group-1 26 |    ├── prod-cluster 27 |    ├── prod-service-bar 28 |    ... 29 |    └── test-service-foo 30 | ``` 31 | 32 | ## `bootstrap` 33 | 34 | In the `bootstrap` directory you can find the ArgoCD applications that serve as the root of our tree of app of apps. 35 | This directory is the entry point for `mani-diffy`, it's where `mani-diffy` looks first to find its way to rendering all the other charts before they get to Argo CD, so you can review changes safely. 36 | 37 | ## `charts` 38 | 39 | In the `charts` directory you can find all the charts that are needed in our project. 40 | In this example we only have 2 charts. 41 | 1. `app-of-apps` a chart that when rendered produces manifests files containing Argo application 42 | 2. `service` a chart that when rendered produces manifests files containing k8s resources needed for a service to run (ex: `kind: Deployment`) 43 | 44 | ## `overrides` 45 | 46 | The `overrides` directory contains values needed to render the charts. 47 | These values are key to the app of apps pattern, indeed it's in the overrides that we define which apps run in which cluster and how many pods we'll be running per service and environment. 48 | 49 | ## `.zz.auto-generated` 50 | 51 | The `.zz.auto-generated` directory is the output dir for `mani-diffy`. 52 | This is where the rendered manifests that represent the state of the world are kept. 53 | When a user makes a change, `mani-diffy` will render the new manifests and commit them making the difference easy to review on a pull request. 54 | When `mani-diffy` runs for the first time it will create this dir and add rendered manifests to it. 55 | 56 | 57 | # What's in the demo ? 58 | 59 | We have 2 environments (test and prod) and an "app-of-apps" Chart used to create multiple layers of Argo CD applications. 60 | 61 | - 1st layer: we have one Argo app for each cluster (ex: `prod-cluster.yaml`) 62 | - 2nd layer: for each cluster, we have one or many app groups. An app group is a way to logically separate services. 63 | - 3rd layer: for each app group, we have a few services. A service is a set of k8s resources that together are all we need for the service to be running (deployments, configmaps etc). 64 | 65 | When the manifests have been generated, you can see all the output in `.zz.auto-generated/`. 66 | 67 | Notice that there is one more app running in the test cluster: `test-baz-app`. 68 | 69 | ```mermaid 70 | graph TD; 71 | root["Root
kind: Application"]-->prod-cluster; 72 | root-->test-cluster; 73 | 74 | prod-cluster["Prod Cluster
kind: Application"]-->prod-app-group-1["Prod App Group 1
kind: Application"]; 75 | prod-app-group-1-->prod-service-foo["Prod Service Foo
kind: Application"]; 76 | prod-app-group-1-->prod-service-bar["Prod Service Bar
kind: Application"]; 77 | prod-service-bar-->prod-bar-worker["Bar Worker
kind: Deployment"]; 78 | prod-service-bar-->prod-bar-web["Bar Web
kind: Deployment"]; 79 | prod-service-foo-->prod-foo-worker["foo Worker
kind: Deployment"]; 80 | prod-service-foo-->prod-foo-web["foo Web
kind: Deployment"]; 81 | 82 | test-cluster["test Cluster
kind: Application"]-->test-app-group-1["test App Group 1
kind: Application"]; 83 | test-app-group-1-->test-service-foo["test Service Foo
kind: Application"]; 84 | test-app-group-1-->test-service-bar["test Service Bar
kind: Application"]; 85 | test-app-group-1-->test-service-baz["test Service Baz
kind: Application"]; 86 | test-service-bar-->test-bar-worker["Bar Worker
kind: Deployment"]; 87 | test-service-bar-->test-bar-web["Bar Web
kind: Deployment"]; 88 | test-service-foo-->test-foo-worker["foo Worker
kind: Deployment"]; 89 | test-service-foo-->test-foo-web["foo Web
kind: Deployment"]; 90 | test-service-baz-->test-baz-worker["baz Worker
kind: Deployment"]; 91 | test-service-baz-->test-baz-web["baz Web
kind: Deployment"]; 92 | ``` 93 | -------------------------------------------------------------------------------- /demo/bootstrap/prod-cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: prod-cluster 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: argocd 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | helm: 13 | parameters: 14 | - name: region 15 | value: us-east-1 16 | - name: renderBaseDir 17 | value: /zz.auto-generated/root 18 | valueFiles: 19 | - ../../overrides/bootstrap/prod-cluster.yaml 20 | path: charts/app-of-apps 21 | repoURL: https://github.com/chime/mani-diffy.git 22 | targetRevision: HEAD 23 | syncPolicy: 24 | syncOptions: 25 | - CreateNamespace=true 26 | -------------------------------------------------------------------------------- /demo/bootstrap/test-cluster.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: test-cluster 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: argocd 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | helm: 13 | parameters: 14 | - name: region 15 | value: us-east-1 16 | - name: renderBaseDir 17 | value: /zz.auto-generated/root 18 | valueFiles: 19 | - ../../overrides/bootstrap/test-cluster.yaml 20 | path: charts/app-of-apps 21 | repoURL: https://github.com/chime/mani-diffy.git 22 | targetRevision: HEAD 23 | syncPolicy: 24 | syncOptions: 25 | - CreateNamespace=true 26 | -------------------------------------------------------------------------------- /demo/charts/app-of-apps/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | appVersion: "1.0" 3 | description: "app-of-apps helm chart" 4 | name: app-of-apps 5 | # DON'T BUMP THIS. We don't use helm versioning. 6 | version: 1.0.0 7 | icon: https://chime.com/favicon.ico 8 | -------------------------------------------------------------------------------- /demo/charts/app-of-apps/templates/app-of-apps.tpl: -------------------------------------------------------------------------------- 1 | {{- define "app-of-apps" -}} 2 | --- 3 | apiVersion: argoproj.io/v1alpha1 4 | kind: Application 5 | metadata: 6 | name: {{ .child }} 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | repoURL: https://github.com/chime/mani-diffy.git 14 | targetRevision: HEAD 15 | 16 | path: charts/app-of-apps 17 | helm: 18 | parameters: 19 | - name: renderBaseDir 20 | value: {{ .root.renderBaseDir }} 21 | {{- with .root.parameters }} 22 | {{ . | toYaml | indent 8 }} 23 | {{- end }} 24 | {{ .childParams.parameters | toYaml | indent 8 }} 25 | valueFiles: 26 | - ../../overrides/app-of-apps/{{ .child }}.yaml 27 | 28 | syncPolicy: 29 | automated: {} 30 | {{ end }} -------------------------------------------------------------------------------- /demo/charts/app-of-apps/templates/apps.yaml: -------------------------------------------------------------------------------- 1 | {{- range $child, $params := $.Values.children -}} 2 | {{ include ($params.chart) (dict "child" $child "childParams" $params "root" $.Values "files" $.Files) }} 3 | {{- end -}} -------------------------------------------------------------------------------- /demo/charts/app-of-apps/templates/service.tpl: -------------------------------------------------------------------------------- 1 | {{- define "service" -}} 2 | {{ $appName := .child -}} 3 | --- 4 | apiVersion: argoproj.io/v1alpha1 5 | kind: Application 6 | metadata: 7 | name: {{ .root.env }}-service-{{ $appName }} 8 | spec: 9 | destination: 10 | namespace: argocd 11 | server: https://kubernetes.default.svc 12 | project: default 13 | source: 14 | repoURL: https://github.com/chime/mani-diffy.git 15 | path: charts/service 16 | helm: 17 | version: v3 18 | parameters: 19 | - name: env 20 | value: {{ .root.env }} 21 | valueFiles: 22 | - ../../overrides/service/{{ $appName }}/base.yaml 23 | - ../../overrides/service/{{ .child }}/{{ .root.env }}.yaml 24 | syncPolicy: 25 | automated: {} 26 | {{ end }} -------------------------------------------------------------------------------- /demo/charts/service/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: service 3 | version: 0.1.0 4 | -------------------------------------------------------------------------------- /demo/charts/service/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | {{- $rootValues := .Values -}} 2 | {{- range $processName, $process := $rootValues.processes }} 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: {{ printf "%s-%s" $rootValues.appName $processName }} 7 | annotations: 8 | appTag: {{ default "latest" $rootValues.appTag }} 9 | spec: 10 | replicas: {{ default 1 $process.replicas }} 11 | selector: 12 | matchLabels: 13 | app: {{ printf "%s-%s" $rootValues.appName $processName }} 14 | template: 15 | metadata: 16 | labels: 17 | app: {{ printf "%s-%s" $rootValues.appName $processName }} 18 | spec: 19 | containers: 20 | - name: {{ $processName }} 21 | image: "gcr.io/heptio-images/ks-guestbook-demo:0.1" 22 | command: {{ default (list) $process.command }} 23 | resources: 24 | limits: 25 | memory: {{ default "32Mi" $process.memory }} 26 | cpu: {{ default "10m" $process.cpu }} 27 | --- 28 | {{- end }} -------------------------------------------------------------------------------- /demo/charts/service/values.yaml: -------------------------------------------------------------------------------- 1 | name: demo 2 | replicas: 1 3 | 4 | -------------------------------------------------------------------------------- /demo/overrides/app-of-apps/prod-app-group-1.yaml: -------------------------------------------------------------------------------- 1 | children: 2 | foo: 3 | chart: service 4 | bar: 5 | chart: service 6 | -------------------------------------------------------------------------------- /demo/overrides/app-of-apps/test-app-group-1.yaml: -------------------------------------------------------------------------------- 1 | children: 2 | foo: 3 | chart: service 4 | bar: 5 | chart: service 6 | baz: 7 | chart: service 8 | -------------------------------------------------------------------------------- /demo/overrides/bootstrap/prod-cluster.yaml: -------------------------------------------------------------------------------- 1 | children: 2 | prod-app-group-1: 3 | chart: app-of-apps 4 | parameters: 5 | - name: cluster 6 | value: use1-prod-eks-cluster 7 | - name: env 8 | value: prod 9 | - name: ns 10 | value: app-group-1 11 | 12 | -------------------------------------------------------------------------------- /demo/overrides/bootstrap/test-cluster.yaml: -------------------------------------------------------------------------------- 1 | children: 2 | test-app-group-1: 3 | chart: app-of-apps 4 | parameters: 5 | - name: cluster 6 | value: use1-test-eks-cluster 7 | - name: env 8 | value: test 9 | - name: ns 10 | value: app-group-1 11 | -------------------------------------------------------------------------------- /demo/overrides/service/bar/base.yaml: -------------------------------------------------------------------------------- 1 | appName: 'bar' 2 | 3 | processes: 4 | web: 5 | replicas: 2 6 | memory: "200Mi" 7 | cpu: 100m 8 | worker: 9 | replicas: 1 10 | memory: "200Mi" 11 | cpu: 100m -------------------------------------------------------------------------------- /demo/overrides/service/bar/prod.yaml: -------------------------------------------------------------------------------- 1 | processes: 2 | web: 3 | replicas: 20 -------------------------------------------------------------------------------- /demo/overrides/service/bar/test.yaml: -------------------------------------------------------------------------------- 1 | processes: 2 | web: 3 | replicas: 2 -------------------------------------------------------------------------------- /demo/overrides/service/baz/base.yaml: -------------------------------------------------------------------------------- 1 | appName: 'baz' 2 | 3 | processes: 4 | web: 5 | replicas: 1 -------------------------------------------------------------------------------- /demo/overrides/service/baz/test.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chime/mani-diffy/cbbd15440daf1a16d0f45f2208dab17b28d224c8/demo/overrides/service/baz/test.yaml -------------------------------------------------------------------------------- /demo/overrides/service/foo/base.yaml: -------------------------------------------------------------------------------- 1 | appName: 'foo' 2 | 3 | processes: 4 | web: 5 | replicas: 2 6 | 7 | -------------------------------------------------------------------------------- /demo/overrides/service/foo/prod.yaml: -------------------------------------------------------------------------------- 1 | processes: 2 | web: 3 | replicas: 10 -------------------------------------------------------------------------------- /demo/overrides/service/foo/test.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chime/mani-diffy/cbbd15440daf1a16d0f45f2208dab17b28d224c8/demo/overrides/service/foo/test.yaml -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/chime/mani-diffy 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/argoproj/argo-cd/v2 v2.6.15 7 | gopkg.in/yaml.v3 v3.0.1 8 | k8s.io/apimachinery v0.24.2 9 | ) 10 | 11 | require ( 12 | cloud.google.com/go/compute v1.19.1 // indirect 13 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 14 | dario.cat/mergo v1.0.0 // indirect 15 | github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect 16 | github.com/MakeNowJust/heredoc v0.0.0-20170808103936-bb23615498cd // indirect 17 | github.com/Masterminds/semver/v3 v3.2.0 // indirect 18 | github.com/Microsoft/go-winio v0.6.1 // indirect 19 | github.com/ProtonMail/go-crypto v0.0.0-20230828082145-3c4c8a2d2371 // indirect 20 | github.com/argoproj/gitops-engine v0.7.1-0.20230512020822-b4dd8b8c3976 // indirect 21 | github.com/argoproj/pkg v0.13.7-0.20230627120311-a4dd357b057e // indirect 22 | github.com/bombsimon/logrusr/v2 v2.0.1 // indirect 23 | github.com/bradleyfalzon/ghinstallation/v2 v2.1.0 // indirect 24 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 25 | github.com/chai2010/gettext-go v0.0.0-20170215093142-bf70f2a70fb1 // indirect 26 | github.com/cloudflare/circl v1.3.3 // indirect 27 | github.com/cyphar/filepath-securejoin v0.2.4 // indirect 28 | github.com/davecgh/go-spew v1.1.1 // indirect 29 | github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect 30 | github.com/docker/distribution v2.8.2+incompatible // indirect 31 | github.com/emicklei/go-restful/v3 v3.8.0 // indirect 32 | github.com/emirpasic/gods v1.18.1 // indirect 33 | github.com/evanphx/json-patch v5.6.0+incompatible // indirect 34 | github.com/exponent-io/jsonpath v0.0.0-20151013193312-d6023ce2651d // indirect 35 | github.com/fatih/camelcase v1.0.0 // indirect 36 | github.com/fvbommel/sortorder v1.0.1 // indirect 37 | github.com/ghodss/yaml v1.0.0 // indirect 38 | github.com/go-errors/errors v1.0.1 // indirect 39 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect 40 | github.com/go-git/go-billy/v5 v5.5.0 // indirect 41 | github.com/go-git/go-git/v5 v5.11.0 // indirect 42 | github.com/go-logr/logr v1.2.3 // indirect 43 | github.com/go-openapi/jsonpointer v0.19.5 // indirect 44 | github.com/go-openapi/jsonreference v0.20.0 // indirect 45 | github.com/go-openapi/swag v0.21.1 // indirect 46 | github.com/go-redis/cache/v8 v8.4.2 // indirect 47 | github.com/go-redis/redis/v8 v8.11.5 // indirect 48 | github.com/gobwas/glob v0.2.3 // indirect 49 | github.com/gogo/protobuf v1.3.2 // indirect 50 | github.com/golang-jwt/jwt/v4 v4.4.3 // indirect 51 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 52 | github.com/golang/protobuf v1.5.3 // indirect 53 | github.com/google/btree v1.0.1 // indirect 54 | github.com/google/gnostic v0.5.7-v3refs // indirect 55 | github.com/google/go-cmp v0.6.0 // indirect 56 | github.com/google/go-github/v45 v45.2.0 // indirect 57 | github.com/google/go-querystring v1.1.0 // indirect 58 | github.com/google/gofuzz v1.1.0 // indirect 59 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect 60 | github.com/google/uuid v1.3.0 // indirect 61 | github.com/gregjones/httpcache v0.0.0-20190611155906-901d90724c79 // indirect 62 | github.com/imdario/mergo v0.3.13 // indirect 63 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 64 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect 65 | github.com/jonboulle/clockwork v0.2.2 // indirect 66 | github.com/josharian/intern v1.0.0 // indirect 67 | github.com/json-iterator/go v1.1.12 // indirect 68 | github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect 69 | github.com/kevinburke/ssh_config v1.2.0 // indirect 70 | github.com/klauspost/compress v1.16.5 // indirect 71 | github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect 72 | github.com/mailru/easyjson v0.7.7 // indirect 73 | github.com/mitchellh/go-wordwrap v1.0.0 // indirect 74 | github.com/moby/spdystream v0.2.0 // indirect 75 | github.com/moby/term v0.0.0-20210619224110-3f7ff695adc6 // indirect 76 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 77 | github.com/modern-go/reflect2 v1.0.2 // indirect 78 | github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect 79 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 80 | github.com/opencontainers/go-digest v1.0.0 // indirect 81 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 82 | github.com/peterbourgon/diskv v2.0.1+incompatible // indirect 83 | github.com/pjbgf/sha1cd v0.3.0 // indirect 84 | github.com/pkg/errors v0.9.1 // indirect 85 | github.com/pmezard/go-difflib v1.0.0 // indirect 86 | github.com/robfig/cron/v3 v3.0.1 // indirect 87 | github.com/russross/blackfriday v1.5.2 // indirect 88 | github.com/sergi/go-diff v1.1.0 // indirect 89 | github.com/sirupsen/logrus v1.9.3 // indirect 90 | github.com/skeema/knownhosts v1.2.1 // indirect 91 | github.com/spf13/cobra v1.7.0 // indirect 92 | github.com/spf13/pflag v1.0.5 // indirect 93 | github.com/stretchr/testify v1.8.4 // indirect 94 | github.com/vmihailenco/go-tinylfu v0.2.1 // indirect 95 | github.com/vmihailenco/msgpack/v5 v5.3.4 // indirect 96 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 97 | github.com/xanzy/ssh-agent v0.3.3 // indirect 98 | github.com/xlab/treeprint v0.0.0-20181112141820-a009c3971eca // indirect 99 | go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect 100 | golang.org/x/crypto v0.16.0 // indirect 101 | golang.org/x/exp v0.0.0-20210901193431-a062eea981d2 // indirect 102 | golang.org/x/mod v0.12.0 // indirect 103 | golang.org/x/net v0.19.0 // indirect 104 | golang.org/x/oauth2 v0.7.0 // indirect 105 | golang.org/x/sync v0.3.0 // indirect 106 | golang.org/x/sys v0.15.0 // indirect 107 | golang.org/x/term v0.15.0 // indirect 108 | golang.org/x/text v0.14.0 // indirect 109 | golang.org/x/time v0.0.0-20220210224613-90d013bbcef8 // indirect 110 | golang.org/x/tools v0.13.0 // indirect 111 | google.golang.org/appengine v1.6.7 // indirect 112 | google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect 113 | google.golang.org/grpc v1.56.3 // indirect 114 | google.golang.org/protobuf v1.30.0 // indirect 115 | gopkg.in/inf.v0 v0.9.1 // indirect 116 | gopkg.in/warnings.v0 v0.1.2 // indirect 117 | gopkg.in/yaml.v2 v2.4.0 // indirect 118 | k8s.io/api v0.24.2 // indirect 119 | k8s.io/apiextensions-apiserver v0.24.2 // indirect 120 | k8s.io/apiserver v0.24.2 // indirect 121 | k8s.io/cli-runtime v0.24.2 // indirect 122 | k8s.io/client-go v0.24.2 // indirect 123 | k8s.io/component-base v0.24.2 // indirect 124 | k8s.io/component-helpers v0.24.2 // indirect 125 | k8s.io/klog/v2 v2.70.1 // indirect 126 | k8s.io/kube-aggregator v0.24.2 // indirect 127 | k8s.io/kube-openapi v0.0.0-20220627174259-011e075b9cb8 // indirect 128 | k8s.io/kubectl v0.24.2 // indirect 129 | k8s.io/kubernetes v1.24.2 // indirect 130 | k8s.io/utils v0.0.0-20220210201930-3a6ce19ff2f9 // indirect 131 | sigs.k8s.io/json v0.0.0-20211208200746-9f7c6b3444d2 // indirect 132 | sigs.k8s.io/kustomize/api v0.11.4 // indirect 133 | sigs.k8s.io/kustomize/kyaml v0.13.6 // indirect 134 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 135 | sigs.k8s.io/yaml v1.3.0 // indirect 136 | ) 137 | 138 | replace ( 139 | k8s.io/api => k8s.io/api v0.24.2 140 | k8s.io/apiextensions-apiserver => k8s.io/apiextensions-apiserver v0.24.2 141 | k8s.io/apimachinery => k8s.io/apimachinery v0.24.2 142 | k8s.io/apiserver => k8s.io/apiserver v0.24.2 143 | k8s.io/cli-runtime => k8s.io/cli-runtime v0.24.2 144 | k8s.io/client-go => k8s.io/client-go v0.24.2 145 | k8s.io/cloud-provider => k8s.io/cloud-provider v0.24.2 146 | k8s.io/cluster-bootstrap => k8s.io/cluster-bootstrap v0.24.2 147 | k8s.io/code-generator => k8s.io/code-generator v0.24.2 148 | k8s.io/component-base => k8s.io/component-base v0.24.2 149 | k8s.io/component-helpers => k8s.io/component-helpers v0.24.2 150 | k8s.io/controller-manager => k8s.io/controller-manager v0.24.2 151 | k8s.io/cri-api => k8s.io/cri-api v0.24.2 152 | k8s.io/csi-translation-lib => k8s.io/csi-translation-lib v0.24.2 153 | k8s.io/kube-aggregator => k8s.io/kube-aggregator v0.24.2 154 | k8s.io/kube-controller-manager => k8s.io/kube-controller-manager v0.24.2 155 | k8s.io/kube-proxy => k8s.io/kube-proxy v0.24.2 156 | k8s.io/kube-scheduler => k8s.io/kube-scheduler v0.24.2 157 | k8s.io/kubectl => k8s.io/kubectl v0.24.2 158 | k8s.io/kubelet => k8s.io/kubelet v0.24.2 159 | k8s.io/legacy-cloud-providers => k8s.io/legacy-cloud-providers v0.24.2 160 | k8s.io/metrics => k8s.io/metrics v0.24.2 161 | k8s.io/mount-utils => k8s.io/mount-utils v0.24.2 162 | k8s.io/pod-security-admission => k8s.io/pod-security-admission v0.24.2 163 | k8s.io/sample-apiserver => k8s.io/sample-apiserver v0.24.2 164 | ) 165 | -------------------------------------------------------------------------------- /hashing.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "log" 8 | "os" 9 | "path/filepath" 10 | 11 | yaml "gopkg.in/yaml.v3" 12 | ) 13 | 14 | type HashStore interface { 15 | // Add a hash to the HashStore. 16 | Add(name, hash string) error 17 | 18 | // Get a hash from the HashStore. 19 | Get(name string) (string, error) 20 | 21 | // Persist the hashes. 22 | Save() error 23 | } 24 | 25 | const ( 26 | HashStrategyReadWrite = "readwrite" 27 | HashStrategyRead = "read" 28 | ) 29 | 30 | // An implementation of the HashStore that stores all hashes inside a single 31 | // JSON file. 32 | type JSONHashStore struct { 33 | path string 34 | hashes map[string]string 35 | strategy string 36 | } 37 | 38 | func NewJSONHashStore(path, strategy string) (*JSONHashStore, error) { 39 | hashes := make(map[string]string) 40 | content, err := os.ReadFile(path) 41 | if err != nil { 42 | if !os.IsNotExist(err) { 43 | return nil, err 44 | } 45 | } else { 46 | if err := json.Unmarshal(content, &hashes); err != nil { 47 | // If the file is invalid JSON, instead of erroring out, 48 | // just start from scratch so a new valid file is 49 | // generated. 50 | log.Printf("Unable to parse hashes from %s: %v\n", path, err) 51 | } 52 | } 53 | 54 | hashes["//"] = "AUTO GENERATED. DO NOT EDIT." 55 | 56 | return &JSONHashStore{ 57 | path: path, 58 | hashes: hashes, 59 | strategy: strategy, 60 | }, nil 61 | } 62 | 63 | func (s *JSONHashStore) Add(name, hash string) error { 64 | s.hashes[name] = hash 65 | return nil 66 | } 67 | 68 | func (s *JSONHashStore) Get(name string) (string, error) { 69 | return s.hashes[name], nil 70 | } 71 | 72 | func (s *JSONHashStore) Save() error { 73 | if s.strategy == HashStrategyRead { 74 | // Read-only mode, so don't write. 75 | return nil 76 | } 77 | 78 | b, err := json.MarshalIndent(s.hashes, "", " ") 79 | if err != nil { 80 | return err 81 | } 82 | 83 | return os.WriteFile(s.path, b, 0644) 84 | } 85 | 86 | type ChartHash struct { 87 | Hash string `yaml:"hash"` 88 | } 89 | 90 | // An implementation of HashStore that stores hashes in a "hash.sum" file. 91 | type SumFileStore struct { 92 | path string 93 | strategy string 94 | } 95 | 96 | func NewSumFileStore(path, strategy string) *SumFileStore { 97 | return &SumFileStore{ 98 | path: path, 99 | strategy: strategy, 100 | } 101 | } 102 | 103 | func (s *SumFileStore) Add(name, hash string) error { 104 | ch := ChartHash{ 105 | Hash: hash, 106 | } 107 | data, err := yaml.Marshal(&ch) 108 | if err != nil { 109 | return err 110 | } 111 | 112 | if s.strategy == HashStrategyRead { 113 | // Read-only mode, don't write 114 | return nil 115 | } 116 | 117 | return os.WriteFile(s.filepath(name), data, 0664) 118 | } 119 | 120 | func (s *SumFileStore) Get(name string) (string, error) { 121 | filepath := s.filepath(name) 122 | 123 | yfile, err := os.ReadFile(filepath) 124 | if err != nil { 125 | if errors.Is(err, os.ErrNotExist) { 126 | // This is fine to do since there are cases where there won't be a hash. e.g. root 127 | return "", nil 128 | } 129 | return "", fmt.Errorf("error reading file hash from %s error: %w", filepath, err) 130 | } 131 | ch := ChartHash{} 132 | err2 := yaml.Unmarshal(yfile, &ch) 133 | if err2 != nil { 134 | return "", fmt.Errorf("error unmarshaling hash %s error: %w", filepath, err2) 135 | } 136 | return ch.Hash, nil 137 | } 138 | 139 | func (s *SumFileStore) Save() error { 140 | // Already written in Add 141 | return nil 142 | } 143 | 144 | func (s *SumFileStore) filepath(name string) string { 145 | return filepath.Join(s.path, name, "hash.sum") 146 | } 147 | -------------------------------------------------------------------------------- /hashing_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestNewJSONHashStore(t *testing.T) { 9 | f, err := os.CreateTemp("", "") 10 | if err != nil { 11 | t.Fatal(err) 12 | } 13 | 14 | h, err := NewJSONHashStore(f.Name(), HashStrategyReadWrite) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | 19 | if err := h.Add("foo", "bar"); err != nil { 20 | t.Fatal(err) 21 | } 22 | 23 | if err := h.Save(); err != nil { 24 | t.Fatal(err) 25 | } 26 | 27 | h, err = NewJSONHashStore(f.Name(), HashStrategyRead) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | 32 | hash, err := h.Get("foo") 33 | if err != nil { 34 | t.Fatal(err) 35 | } 36 | 37 | if hash != "bar" { 38 | t.Fatal("Expected hash to match") 39 | } 40 | } 41 | 42 | func TestNewJSONHashStore_InvalidJSON(t *testing.T) { 43 | f, err := os.CreateTemp("", "") 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | _, err = NewJSONHashStore(f.Name(), HashStrategyReadWrite) 48 | if err != nil { 49 | t.Fatal(err) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "flag" 6 | "fmt" 7 | "log" 8 | "os" 9 | "os/exec" 10 | 11 | "path/filepath" 12 | "strings" 13 | "time" 14 | 15 | "github.com/chime/mani-diffy/pkg/helm" 16 | "github.com/chime/mani-diffy/pkg/kustomize" 17 | 18 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 19 | ) 20 | 21 | const InfiniteDepth = -1 22 | 23 | // Renderer is a function that can render an Argo application. 24 | type Renderer func(*v1alpha1.Application, string) error 25 | 26 | // PostRenderer is a function that can be called after an Argo application is rendered. 27 | type PostRenderer func(string) error 28 | 29 | // Walker walks a directory tree looking for Argo applications and renders them 30 | // using a depth first search. 31 | type Walker struct { 32 | // HelmTemplate is a function that can render an Argo application using Helm 33 | HelmTemplate Renderer 34 | 35 | // CopySource is a function that can copy an Argo application to a directory 36 | CopySource Renderer 37 | 38 | // PostRender is a function that can be called after an Argo application is rendered. 39 | PostRender PostRenderer 40 | 41 | // GenerateHash is used to generate a cache key for an Argo application 42 | GenerateHash func(*v1alpha1.Application) (string, error) 43 | 44 | ignoreSuffix string 45 | } 46 | 47 | // Walk walks a directory tree looking for Argo applications and renders them 48 | func (w *Walker) Walk(inputPath, outputPath string, maxDepth int, hashes HashStore) error { 49 | visited := make(map[string]bool) 50 | 51 | if err := w.walk(inputPath, outputPath, 0, maxDepth, visited, hashes); err != nil { 52 | return err 53 | } 54 | 55 | if err := hashes.Save(); err != nil { 56 | return err 57 | } 58 | 59 | if maxDepth == InfiniteDepth { 60 | return pruneUnvisited(visited, outputPath) 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func pruneUnvisited(visited map[string]bool, outputPath string) error { 67 | files, err := os.ReadDir(outputPath) 68 | if err != nil { 69 | return err 70 | } 71 | 72 | for _, f := range files { 73 | if !f.IsDir() { 74 | continue 75 | } 76 | 77 | path := filepath.Join(outputPath, f.Name()) 78 | if visited[path] { 79 | continue 80 | } 81 | if err := os.RemoveAll(path); err != nil { 82 | return err 83 | } 84 | } 85 | 86 | return nil 87 | } 88 | 89 | func (w *Walker) walk(inputPath, outputPath string, depth, maxDepth int, visited map[string]bool, hashes HashStore) error { 90 | if maxDepth != InfiniteDepth { 91 | // If we've reached the max depth, stop walking 92 | if depth > maxDepth { 93 | return nil 94 | } 95 | } 96 | 97 | log.Println("Dropping into", inputPath) 98 | 99 | fi, err := os.ReadDir(inputPath) 100 | if err != nil { 101 | return err 102 | } 103 | for _, file := range fi { 104 | if !strings.Contains(file.Name(), ".yaml") { 105 | continue 106 | } 107 | 108 | crds, err := helm.Read(filepath.Join(inputPath, file.Name())) 109 | if err != nil { 110 | return err 111 | } 112 | for _, crd := range crds { 113 | if crd.Kind != "Application" { 114 | continue 115 | } 116 | 117 | if strings.HasSuffix(crd.ObjectMeta.Name, w.ignoreSuffix) { 118 | continue 119 | } 120 | 121 | path := filepath.Join(outputPath, crd.ObjectMeta.Name) 122 | visited[path] = true 123 | 124 | hash, err := hashes.Get(crd.ObjectMeta.Name) 125 | // COMPARE HASHES HERE. STEP INTO RENDER IF NO MATCH 126 | if err != nil { 127 | return err 128 | } 129 | 130 | hashGenerated, err := w.GenerateHash(crd) 131 | if err != nil { 132 | if errors.Is(err, kustomize.ErrNotSupported) { 133 | continue 134 | } 135 | return err 136 | } 137 | 138 | emptyManifest, err := helm.EmptyManifest(filepath.Join(path, "manifest.yaml")) 139 | if err != nil { 140 | return err 141 | } 142 | 143 | if hashGenerated != hash || emptyManifest { 144 | log.Printf("No match detected. Render: %s\n", crd.ObjectMeta.Name) 145 | if err := w.Render(crd, path); err != nil { 146 | if errors.Is(err, kustomize.ErrNotSupported) { 147 | continue 148 | } 149 | return err 150 | } 151 | 152 | if err := hashes.Add(crd.ObjectMeta.Name, hashGenerated); err != nil { 153 | return err 154 | } 155 | } 156 | 157 | if err := w.walk(path, outputPath, depth+1, maxDepth, visited, hashes); err != nil { 158 | return err 159 | } 160 | } 161 | } 162 | return nil 163 | } 164 | 165 | func (w *Walker) Render(application *v1alpha1.Application, output string) error { 166 | log.Println("Render", application.ObjectMeta.Name) 167 | 168 | var render Renderer 169 | 170 | // Figure out which renderer to use 171 | switch { 172 | case application.Spec.Source.Helm != nil: 173 | render = w.HelmTemplate 174 | case application.Spec.Source.Kustomize != nil: 175 | log.Println("WARNING: kustomize not supported") 176 | return kustomize.ErrNotSupported 177 | default: 178 | render = w.CopySource 179 | } 180 | 181 | // Make sure the directory is empty before rendering. 182 | if err := os.RemoveAll(output); err != nil { 183 | return err 184 | } 185 | 186 | // Render 187 | if err := render(application, output); err != nil { 188 | return err 189 | } 190 | 191 | // Call the post renderer to do any post processing 192 | if w.PostRender != nil { 193 | if err := w.PostRender(output); err != nil { 194 | return fmt.Errorf("post render failed: %w", err) 195 | } 196 | } 197 | 198 | return nil 199 | } 200 | 201 | func HelmTemplate(application *v1alpha1.Application, output string) error { 202 | return helm.Run(application, output, "", "") 203 | } 204 | 205 | func CopySource(application *v1alpha1.Application, output string) error { 206 | cmd := exec.Command("cp", "-r", application.Spec.Source.Path+"/.", output) 207 | return cmd.Run() 208 | } 209 | 210 | func PostRender(command string) PostRenderer { 211 | return func(output string) error { 212 | cmd := exec.Command(command, output) 213 | cmd.Stderr = os.Stderr 214 | return cmd.Run() 215 | } 216 | } 217 | 218 | func main() { 219 | root := flag.String("root", "bootstrap", "Directory to initially look for k8s manifests containing Argo applications. The root of the tree.") 220 | workdir := flag.String("workdir", ".", "Directory to run the command in.") 221 | renderDir := flag.String("output", ".zz.auto-generated", "Path to store the compiled Argo applications.") 222 | maxDepth := flag.Int("max-depth", InfiniteDepth, "Maximum depth for the depth first walk.") 223 | hashStore := flag.String("hash-store", "sumfile", "The hashing backend to use. Can be `sumfile` or `json`.") 224 | hashStrategy := flag.String("hash-strategy", HashStrategyReadWrite, "Whether to read + write, or just read hashes. Can be `readwrite` or `read`.") 225 | ignoreSuffix := flag.String("ignore-suffix", "-ignore", "Suffix used to identify apps to ignore") 226 | skipRenderKey := flag.String("skip-render-key", "do-not-render", "Key to not render") 227 | ignoreValueFile := flag.String("ignore-value-file", "overrides-to-ignore", "Override file to ignore based on filename") 228 | postRenderer := flag.String("post-renderer", "", "When provided, binary will be called after an application is rendered.") 229 | flag.Parse() 230 | 231 | // Runs the command in the specified directory 232 | err := os.Chdir(*workdir) 233 | if err != nil { 234 | log.Fatal("Could not set workdir: ", err) 235 | } 236 | 237 | start := time.Now() 238 | if err := helm.VerifyRenderDir(*renderDir); err != nil { 239 | log.Fatal(err) 240 | } 241 | 242 | h, err := getHashStore(*hashStore, *hashStrategy, *renderDir) 243 | if err != nil { 244 | log.Fatal(err) 245 | } 246 | 247 | w := &Walker{ 248 | CopySource: CopySource, 249 | HelmTemplate: func(application *v1alpha1.Application, output string) error { 250 | return helm.Run(application, output, *skipRenderKey, *ignoreValueFile) 251 | }, 252 | GenerateHash: func(application *v1alpha1.Application) (string, error) { 253 | return helm.GenerateHash(application, *ignoreValueFile) 254 | }, 255 | ignoreSuffix: *ignoreSuffix, 256 | } 257 | 258 | if *postRenderer != "" { 259 | w.PostRender = PostRender(*postRenderer) 260 | } 261 | 262 | if err := w.Walk(*root, *renderDir, *maxDepth, h); err != nil { 263 | log.Fatal(err) 264 | } 265 | log.Printf("mani-diffy took %v to run", time.Since(start)) 266 | } 267 | 268 | var hashStores = map[string]func(string, string) (HashStore, error){ 269 | "sumfile": func(outputPath, hashStrategy string) (HashStore, error) { //nolint:unparam 270 | return NewSumFileStore(outputPath, hashStrategy), nil 271 | }, 272 | "json": func(outputPath, hashStrategy string) (HashStore, error) { 273 | return NewJSONHashStore(filepath.Join(outputPath, "hashes.json"), hashStrategy) 274 | }, 275 | } 276 | 277 | func getHashStore(hashStore, hashStrategy, outputPath string) (HashStore, error) { 278 | if fn, ok := hashStores[hashStore]; ok { 279 | return fn(outputPath, hashStrategy) 280 | } 281 | return nil, fmt.Errorf("Invalid hash store: %v", hashStore) 282 | } 283 | -------------------------------------------------------------------------------- /pkg/helm/helm.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "bytes" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "io/fs" 11 | "log" 12 | "os" 13 | "os/exec" 14 | "path/filepath" 15 | "regexp" 16 | "sort" 17 | "strings" 18 | "sync" 19 | 20 | "github.com/argoproj/argo-cd/v2/pkg/apis/application/v1alpha1" 21 | "github.com/chime/mani-diffy/pkg/kustomize" 22 | yamlutil "k8s.io/apimachinery/pkg/util/yaml" 23 | ) 24 | 25 | func VerifyRenderDir(autoGenerationPath string) error { 26 | if _, err := os.Stat(autoGenerationPath); errors.Is(err, os.ErrNotExist) { 27 | if err := CreateDir(autoGenerationPath); err != nil { 28 | return fmt.Errorf("error creating render directory: %w", err) 29 | } 30 | } 31 | return nil 32 | } 33 | 34 | func CreateDir(dirName string) error { 35 | err := os.MkdirAll(dirName, os.ModePerm) 36 | if err != nil { 37 | return fmt.Errorf("error creating directory: %s %w", dirName, err) 38 | } 39 | return nil 40 | } 41 | 42 | func buildParams(payload *v1alpha1.Application, ignoreValueFile string) (string, string) { 43 | helmParameters := payload.Spec.Source.Helm.Parameters 44 | helmFiles := payload.Spec.Source.Helm.ValueFiles 45 | setValues := "" 46 | fileValues := "" 47 | 48 | for i := 0; i < len(helmParameters); i++ { 49 | setValues += fmt.Sprintf("%s=%s", helmParameters[i].Name, helmParameters[i].Value) 50 | if i != len(helmParameters)-1 { 51 | setValues += "," 52 | } 53 | 54 | } 55 | for i := 0; i < len(helmFiles); i++ { 56 | if ignoreValueFile == "" || !strings.Contains(helmFiles[i], ignoreValueFile) { 57 | fileValues += fmt.Sprintf("%s,", helmFiles[i]) 58 | } 59 | } 60 | fileValues = strings.TrimRight(fileValues, ",") 61 | 62 | return setValues, fileValues 63 | } 64 | 65 | func createTempFile(payload string) (string, error) { 66 | // create a temp file with the results of a yaml block: 67 | tmpYamlFile, err := os.CreateTemp("", "temp.*.yaml") 68 | if err != nil { 69 | return "", fmt.Errorf("error creating temp file: %w", err) 70 | } 71 | 72 | if _, err := tmpYamlFile.Write([]byte(payload)); err != nil { 73 | return "", err 74 | } 75 | if err := tmpYamlFile.Close(); err != nil { 76 | return "", err 77 | } 78 | 79 | return tmpYamlFile.Name(), nil 80 | } 81 | 82 | func IsMissingDependencyErr(err error) bool { 83 | return strings.Contains(err.Error(), "found in requirements.yaml, but missing in charts") || 84 | strings.Contains(err.Error(), "found in Chart.yaml, but missing in charts/ directory") 85 | } 86 | 87 | func installDependencies(chartDirectory string) error { 88 | log.Println("Updating dependencies for " + chartDirectory) 89 | cmd := exec.Command( 90 | "helm", 91 | "dependency", 92 | "update", 93 | ) 94 | cmd.Dir = chartDirectory 95 | err := cmd.Run() 96 | if err != nil { 97 | return fmt.Errorf("error updating dependencies for %s: %w", chartDirectory, err) 98 | } 99 | 100 | return nil 101 | 102 | } 103 | 104 | func template(helmInfo *v1alpha1.Application, skipRenderKey string, ignoreValueFile string) ([]byte, error) { 105 | 106 | chartPath := strings.Split(helmInfo.Spec.Source.Path, "/") 107 | chart := fmt.Sprint("../" + chartPath[len(chartPath)-1]) 108 | 109 | setValues, fileValues := buildParams(helmInfo, ignoreValueFile) 110 | 111 | tmpFile := "" 112 | if helmInfo.Spec.Source.Helm.Values != "" { 113 | dataFile, err := createTempFile(helmInfo.Spec.Source.Helm.Values) 114 | defer os.Remove(dataFile) 115 | if err != nil { 116 | log.Println(err) 117 | } 118 | tmpFile = dataFile 119 | } 120 | 121 | cmd := exec.Command( 122 | "helm", 123 | "template", 124 | chart, 125 | "--set", 126 | setValues, 127 | "-f", 128 | fileValues, 129 | "-f", 130 | tmpFile, 131 | "-n", 132 | helmInfo.Spec.Destination.Namespace, 133 | ) 134 | 135 | if skipRenderKey != "" { 136 | cmd.Args = append(cmd.Args, "--set", fmt.Sprintf("%s=%s", skipRenderKey, "CONSCIOUSLY_NOT_RENDERED")) 137 | } 138 | 139 | cmd.Dir = helmInfo.Spec.Source.Path 140 | 141 | var outb, errb bytes.Buffer 142 | cmd.Stdout = &outb 143 | cmd.Stderr = &errb 144 | 145 | if err := cmd.Run(); err != nil { 146 | if IsMissingDependencyErr(errors.New(errb.String())) { 147 | if err := installDependencies(helmInfo.Spec.Source.Path); err != nil { 148 | return template(helmInfo, skipRenderKey, ignoreValueFile) 149 | } 150 | } else { 151 | return []byte{}, fmt.Errorf("error templating manifest: %w %v", err, errb.String()) 152 | } 153 | } 154 | 155 | return outb.Bytes(), nil 156 | } 157 | 158 | func writeToFile(manifest []byte, location string) error { 159 | if err := CreateDir(location); err != nil { 160 | return err 161 | } 162 | 163 | return os.WriteFile( 164 | fmt.Sprintf( 165 | "%s/%s", 166 | location, 167 | "manifest.yaml", 168 | ), 169 | manifest, 170 | 0664, 171 | ) 172 | } 173 | 174 | func EmptyManifest(manifest string) (bool, error) { 175 | fileInfo, err := os.Stat(manifest) 176 | if err != nil { 177 | if strings.Contains(err.Error(), "manifest.yaml: no such file or directory") { 178 | // the root dirs don't have manifest.yaml files 179 | return false, nil 180 | } 181 | return false, fmt.Errorf("error checking if %s is empty: %w", manifest, err) 182 | } 183 | 184 | if fileInfo.Size() == 0 { 185 | return true, nil 186 | } 187 | 188 | return false, nil 189 | 190 | } 191 | 192 | func GenerateHash(crd *v1alpha1.Application, ignoreValueFile string) (string, error) { 193 | finalHash := sha256.New() 194 | 195 | crdHash, err := generateHashOnCrd(crd) 196 | if err != nil { 197 | return "", err 198 | } 199 | fmt.Fprintf(finalHash, "%x\n", crdHash) 200 | 201 | if crd.Spec.Source.Kustomize != nil { 202 | return "", kustomize.ErrNotSupported 203 | } 204 | 205 | if crd.Spec.Source.Path != "" { 206 | chartHash, err := generalHashFunction(crd.Spec.Source.Path) 207 | if err != nil { 208 | return "", err 209 | } 210 | fmt.Fprintf(finalHash, "%x\n", chartHash) 211 | } 212 | 213 | if crd.Spec.Source.Helm != nil && len(crd.Spec.Source.Helm.ValueFiles) > 0 { 214 | oHash := sha256.New() 215 | overrideFiles := crd.Spec.Source.Helm.ValueFiles 216 | matchDots := regexp.MustCompile(`\.\.\/`) 217 | for i := 0; i < len(overrideFiles); i++ { 218 | if ignoreValueFile == "" || !strings.Contains(overrideFiles[i], ignoreValueFile) { 219 | trimmedFilename := matchDots.ReplaceAllString(overrideFiles[i], "") 220 | oHashReturned, err := generalHashFunction(trimmedFilename) 221 | if err != nil { 222 | return "", err 223 | } 224 | fmt.Fprintf(oHash, "%x\n", oHashReturned) 225 | } 226 | } 227 | overrideHash := oHash.Sum(nil) 228 | fmt.Fprintf(finalHash, "%x\n", overrideHash) 229 | } 230 | 231 | return hex.EncodeToString(finalHash.Sum(nil)), nil 232 | } 233 | 234 | func generalHashFunction(dirFilepath string) ([]byte, error) { 235 | m, err := sha256Dir(dirFilepath) 236 | if err != nil { 237 | log.Println(err) 238 | return []byte{}, err 239 | } 240 | var paths []string 241 | for path := range m { 242 | paths = append(paths, path) 243 | } 244 | // Not sure if needed but I'm sorting for deterministic behavior 245 | sort.Strings(paths) 246 | hash := sha256.New() 247 | for _, path := range paths { 248 | // if a single file, just return the hash 249 | if len(paths) == 1 { 250 | value := m[path] 251 | slice := value[:] 252 | return slice, nil 253 | } 254 | fmt.Fprintf(hash, "%x %s\n", m[path], path) 255 | } 256 | // log.Printf("FINAL HASH: %v\n", hex.EncodeToString(hash.Sum(nil))) 257 | return hash.Sum(nil), nil 258 | } 259 | 260 | // A result is the product of reading and summing a file using MD5. 261 | type result struct { 262 | path string 263 | sum [sha256.Size]byte 264 | err error 265 | } 266 | 267 | type nonRegularFile struct { 268 | fileName string 269 | isDir bool 270 | } 271 | 272 | func resolvesTo(filePath string) (nonRegularFile, error) { 273 | fileData := nonRegularFile{} 274 | info, err := os.Lstat(filePath) 275 | if err != nil { 276 | return fileData, fmt.Errorf("failed to lstat file: %w", err) 277 | } 278 | 279 | if info.IsDir() { 280 | fileData.fileName = filePath 281 | fileData.isDir = true 282 | return fileData, nil 283 | } 284 | 285 | if info.Mode()&fs.ModeSymlink != 0 { 286 | fileName, err := os.Readlink(filePath) 287 | if err != nil { 288 | return fileData, fmt.Errorf("failed to follow symlink: %w", err) 289 | } 290 | fileName = strings.ReplaceAll(filePath, info.Name(), fileName) 291 | fileData.fileName = fileName 292 | fileInfo, err := os.Lstat(fileName) 293 | if err != nil { 294 | return fileData, fmt.Errorf("failed to lstat file: %w", err) 295 | } 296 | if fileInfo.IsDir() { 297 | fileData.isDir = true 298 | return fileData, nil 299 | } 300 | } 301 | return fileData, nil 302 | } 303 | 304 | // sumFiles starts goroutines to walk the directory tree at root and digest each 305 | // regular file. These goroutines send the results of the digests on the result 306 | // channel and send the result of the walk on the error channel. If done is 307 | // closed, sumFiles abandons its work. 308 | func sumFiles(done <-chan struct{}, root string) (<-chan result, <-chan error) { 309 | // For each regular file, start a goroutine that sums the file and sends 310 | // the result on c. Send the result of the walk on errc. 311 | c := make(chan result) 312 | errc := make(chan error, 1) 313 | go func() { // HL 314 | var wg sync.WaitGroup 315 | err := filepath.Walk(root, func(path string, info os.FileInfo, err error) error { 316 | if err != nil { 317 | return fmt.Errorf("error walking the file path %s: %w", root, err) 318 | } 319 | if !info.Mode().IsRegular() { 320 | resolvedInfo, err := resolvesTo(root) 321 | if err != nil { 322 | return err 323 | } 324 | if resolvedInfo.isDir { 325 | // TODO: figure out how to handle dirs and 326 | // symlinked dirs 327 | return nil 328 | } 329 | path = resolvedInfo.fileName 330 | } 331 | wg.Add(1) 332 | go func() { // HL 333 | data, err := os.ReadFile(path) 334 | select { 335 | case c <- result{path, sha256.Sum256(data), err}: // HL 336 | case <-done: // HL 337 | } 338 | wg.Done() 339 | }() 340 | // Abort the walk if done is closed. 341 | select { 342 | case <-done: // HL 343 | return errors.New("walk canceled") 344 | default: 345 | return nil 346 | } 347 | }) 348 | // Walk has returned, so all calls to wg.Add are done. Start a 349 | // goroutine to close c once all the sends are done. 350 | go func() { // HL 351 | wg.Wait() 352 | close(c) // HL 353 | }() 354 | // No select needed here, since errc is buffered. 355 | errc <- err // HL 356 | }() 357 | return c, errc 358 | } 359 | 360 | // sha256Dir reads all the files in the file tree rooted at root and returns a map 361 | // from file path to the sha256 sum of the file's contents. If the directory walk 362 | // fails or any read operation fails, sha256Dir returns an error. In that case, 363 | // sha256Dir does not wait for inflight read operations to complete. 364 | func sha256Dir(root string) (map[string][sha256.Size]byte, error) { 365 | // sha256Dir closes the done channel when it returns; it may do so before 366 | // receiving all the values from c and errc. 367 | done := make(chan struct{}) // HLdone 368 | defer close(done) // HLdone 369 | 370 | c, errc := sumFiles(done, root) // HLdone 371 | 372 | m := make(map[string][sha256.Size]byte) 373 | for r := range c { // HLrange 374 | if r.err != nil { 375 | return nil, r.err 376 | } 377 | m[r.path] = r.sum 378 | } 379 | if err := <-errc; err != nil { 380 | return nil, err 381 | } 382 | return m, nil 383 | } 384 | 385 | func generateHashOnCrd(crd *v1alpha1.Application) (string, error) { 386 | hash := sha256.New() 387 | crdString := crd.String() 388 | crdByte := []byte(crdString) 389 | if _, err := hash.Write(crdByte); err != nil { 390 | return "", fmt.Errorf("error generating hash for the %s crd: %w", crd.ObjectMeta.Name, err) 391 | } 392 | sum := hash.Sum(nil) 393 | return hex.EncodeToString(sum), nil 394 | } 395 | 396 | func Run(crd *v1alpha1.Application, output string, skipRenderKey string, ignoreValueFile string) error { 397 | manifest, err := template(crd, skipRenderKey, ignoreValueFile) 398 | if err != nil { 399 | log.Printf( 400 | "error generating manifest for %s error: %v\n", 401 | crd.ObjectMeta.Name, 402 | string(manifest), 403 | ) 404 | return err 405 | } 406 | err = writeToFile(manifest, output) 407 | return err 408 | } 409 | 410 | func Read(inputCRD string) ([]*v1alpha1.Application, error) { 411 | crdSpecs := make([]*v1alpha1.Application, 0) 412 | yamlFile, err := os.ReadFile(inputCRD) 413 | if err != nil { 414 | // log.Fatalf("Error reading crd: %s %v", inputCRD, err) 415 | return crdSpecs, fmt.Errorf("error reading crd: %s %w", inputCRD, err) 416 | } 417 | 418 | dec := yamlutil.NewYAMLOrJSONDecoder(bytes.NewReader(yamlFile), 1000) 419 | for { 420 | app := v1alpha1.Application{} 421 | if err := dec.Decode(&app); err != nil { 422 | if errors.Is(err, io.EOF) { 423 | break 424 | } 425 | // panic(fmt.Errorf("document decode failed: %w", err)) 426 | return crdSpecs, fmt.Errorf("document decode failed: %w", err) 427 | } 428 | crdSpecs = append(crdSpecs, &app) 429 | } 430 | 431 | return crdSpecs, nil 432 | } 433 | -------------------------------------------------------------------------------- /pkg/helm/helm_test.go: -------------------------------------------------------------------------------- 1 | package helm 2 | 3 | import ( 4 | "encoding/hex" 5 | "errors" 6 | "log" 7 | "os" 8 | "strings" 9 | "testing" 10 | ) 11 | 12 | func TestRead(t *testing.T) { 13 | data, err := Read("test_files/crdData_testfile.yaml") 14 | if err != nil { 15 | t.Error(err) 16 | } 17 | 18 | for _, crd := range data { 19 | 20 | if crd.Kind != "Application" { 21 | t.Error("Kind attribute did not match Application") 22 | } 23 | 24 | if crd.Spec.Source.Helm.ValueFiles[0] != "../../overrides/bootstrap/prod-cluster.yaml" { 25 | t.Error(("Failed to parse ValuesFiles from yaml")) 26 | } 27 | } 28 | } 29 | 30 | func TestReadMultipleCrds(t *testing.T) { 31 | data, err := Read("test_files/crdData_multiple_crd_testfile.yaml") 32 | if err != nil { 33 | t.Error(err) 34 | } 35 | 36 | if len(data) != 2 { 37 | t.Error("Failed to get correct number of crds") 38 | t.Errorf("%s", data) 39 | } 40 | } 41 | 42 | func TestBuildParameters(t *testing.T) { 43 | data, err := Read("test_files/crdData_testfile.yaml") 44 | if err != nil { 45 | t.Error(err) 46 | } 47 | crd := data[0] 48 | setValues, fileValues := buildParams(crd, "") 49 | 50 | if setValues != "region=us-east-1" { 51 | t.Error("setValues is not correct") 52 | } 53 | 54 | if fileValues != "../../overrides/bootstrap/prod-cluster.yaml" { 55 | t.Error("fileValues is not correct") 56 | } 57 | 58 | } 59 | 60 | func TestBuildParameters2(t *testing.T) { 61 | data, err := Read("test_files/crdData_testfile_2.yaml") 62 | if err != nil { 63 | t.Error(err) 64 | } 65 | crd := data[0] 66 | setValues, fileValues := buildParams(crd, "") 67 | 68 | if setValues != "region=us-east-1,testName=testValue" { 69 | t.Error("setValues is not correct") 70 | } 71 | 72 | if fileValues != "../../overrides/bootstrap/prod-cluster.yaml,../../overrides/bootstrap/fake_file.yaml" { 73 | t.Error("fileValues is not correct") 74 | } 75 | 76 | } 77 | 78 | func TestBuildParametersIgnoreValueFile(t *testing.T) { 79 | data, err := Read("test_files/crdData_testfile_3.yaml") 80 | if err != nil { 81 | t.Error(err) 82 | } 83 | crd := data[0] 84 | setValues, fileValues := buildParams(crd, "overrides/service/bar/test.yaml") 85 | 86 | if setValues != "env=test" { 87 | t.Error("setValues is not correct") 88 | } 89 | 90 | if fileValues != "../../overrides/service/bar/base.yaml" { 91 | t.Error("fileValues is not correct") 92 | } 93 | 94 | } 95 | 96 | func TestCreateTempFile(t *testing.T) { 97 | 98 | fileContent := ` 99 | apiVersion: argoproj.io/v1alpha1 100 | kind: Application 101 | metadata: 102 | name: prod-cluster 103 | namespace: argocd 104 | ` 105 | fileName, err := createTempFile(fileContent) 106 | if err != nil { 107 | t.Errorf("failure during file creation: %v", err) 108 | } 109 | 110 | t.Run("Test creating a file from values.", func(t *testing.T) { 111 | _, err = os.Stat(fileName) 112 | if err != nil { 113 | t.Errorf("failed to create a temporary file: %v", err) 114 | } 115 | }) 116 | 117 | t.Run("Verify the content of the temp file", func(t *testing.T) { 118 | got, _ := os.ReadFile(fileName) 119 | want := fileContent 120 | if string(got) != want { 121 | t.Errorf("file contents didn't match got %s wanted %s", got, want) 122 | } 123 | }) 124 | 125 | defer os.Remove(fileName) 126 | 127 | t.Run("Verify the file is cleaned up", func(t *testing.T) { 128 | _, err = os.Stat(fileName) 129 | if os.IsNotExist(err) { 130 | t.Errorf("failed to clean up the temp file: %v", err) 131 | } 132 | 133 | }) 134 | } 135 | 136 | func TestTemplate(t *testing.T) { 137 | data, err := Read("./test_files/crdData_testfile.yaml") 138 | if err != nil { 139 | t.Error(err) 140 | } 141 | crdSpec := data[0] 142 | if err := os.Chdir("../../"); err != nil { 143 | t.Error(err) 144 | } 145 | _, err = template(crdSpec, "", "") 146 | if err != nil { 147 | log.Println(err) 148 | t.Error("Template failed to render a template") 149 | } 150 | } 151 | 152 | func TestTemplateContent(t *testing.T) { 153 | data, err := Read("pkg/helm/test_files/crdData_testfile.yaml") 154 | if err != nil { 155 | t.Error(err) 156 | } 157 | crdSpec := data[0] 158 | 159 | var comparisonString = `--- 160 | # Source: app-of-apps/templates/apps.yaml 161 | apiVersion: argoproj.io/v1alpha1 162 | kind: Application 163 | ` 164 | 165 | manifest, _ := template(crdSpec, "", "") 166 | if strings.Contains(string(manifest), comparisonString) != true { 167 | t.Error("Template failed to render a template with expected content") 168 | } 169 | } 170 | 171 | func TestTemplateContentSkipRenderKey(t *testing.T) { 172 | data, err := Read("pkg/helm/test_files/crdData_testfile_3.yaml") 173 | if err != nil { 174 | t.Error(err) 175 | } 176 | app := data[0] 177 | 178 | // Call template with a key to override 179 | manifest, _ := template(app, "appTag", "") 180 | 181 | // Verify the rendered manifest contains the override 182 | if !strings.Contains(string(manifest), "appTag: CONSCIOUSLY_NOT_RENDERED") { 183 | t.Errorf("Expected override not found in rendered manifest") 184 | 185 | } 186 | } 187 | 188 | func TestGeneralHashFunction(t *testing.T) { 189 | testFiles := []struct { 190 | name string 191 | file string 192 | hash string 193 | }{ 194 | { 195 | name: "Generating hash on symlinked files", 196 | file: "pkg/helm/test_files/crdData_override_testfile_sym_link.yaml", 197 | hash: "a1d62704739d8af3fcaca8f8b13602fc4d4e656b87d773089df3c626c2f37b5d", 198 | }, 199 | { 200 | name: "Generate hash on non symlinked file", 201 | file: "pkg/helm/test_files/crdData_override_testfile.yaml", 202 | hash: "a1d62704739d8af3fcaca8f8b13602fc4d4e656b87d773089df3c626c2f37b5d", 203 | }, 204 | } 205 | 206 | for _, tt := range testFiles { 207 | t.Run(tt.name, func(t *testing.T) { 208 | t.Parallel() 209 | hash, err := generalHashFunction(tt.file) //nolint:govet 210 | h := hex.EncodeToString(hash) 211 | expectedHash := tt.hash //nolint:govet 212 | if err != nil || h != expectedHash { 213 | t.Errorf("Failed to generate a correct hash on an overrides. got: %s wanted %s", h, expectedHash) 214 | } 215 | }) 216 | } 217 | } 218 | 219 | func TestGenerateHashOnCrd(t *testing.T) { 220 | data, err := Read("pkg/helm/test_files/crdData_testfile.yaml") 221 | if err != nil { 222 | t.Error(err) 223 | } 224 | crd := data[0] 225 | 226 | hash, err := generateHashOnCrd(crd) 227 | if err != nil || hash != "7bfd65e963e76680dc5160b6a55c04c3d9780c84aee1413ae710e4b5279cfe14" { 228 | t.Errorf("Failed to generate correctly, got %s", hash) 229 | } 230 | } 231 | 232 | func TestResolvesTo(t *testing.T) { 233 | scenarios := []struct { 234 | name string 235 | expected string 236 | file string 237 | isDirectory bool 238 | }{ 239 | { 240 | name: "symlinked file resolves to its target", 241 | expected: "pkg/helm/test_files/crdData_override_testfile.yaml", 242 | file: "pkg/helm/test_files/crdData_override_testfile_sym_link.yaml", 243 | isDirectory: false, 244 | }, 245 | { 246 | name: "regular directory returns own name", 247 | expected: "pkg/helm/test_files/nonSymDir", 248 | file: "pkg/helm/test_files/nonSymDir", 249 | isDirectory: true, 250 | }, 251 | { 252 | name: "symlinked directory returns its target", 253 | expected: "pkg/helm/test_files/nonSymDir", 254 | file: "pkg/helm/test_files/SymDir", 255 | isDirectory: true, 256 | }, 257 | /* 258 | { 259 | name: "fail to find", 260 | expected: "pkg/helm/test_files/nonSymDir", 261 | file: "pkg/helm/test_files/phantom", 262 | isDirectory: true, 263 | }, 264 | */ 265 | } 266 | 267 | for _, tt := range scenarios { 268 | t.Run(tt.name, func(t *testing.T) { 269 | dataGot, err := resolvesTo(tt.file) 270 | if err != nil { 271 | t.Errorf("failed to resolve file err: %v", err) 272 | } 273 | if dataGot.fileName != tt.expected { 274 | t.Errorf("resolved files do not match. got: %s wanted: %s", dataGot.fileName, tt.expected) 275 | } 276 | if dataGot.isDir != tt.isDirectory { 277 | t.Errorf("failed checking directory status. got: %t wanted: %t", dataGot.isDir, tt.isDirectory) 278 | } 279 | }) 280 | } 281 | } 282 | 283 | func TestDifferenceInTwoDifferentFiles(t *testing.T) { 284 | data, err := Read("pkg/helm/test_files/crdData_testfile.yaml") 285 | if err != nil { 286 | t.Error(err) 287 | } 288 | data2, err2 := Read("pkg/helm/test_files/crdData_testfile_2.yaml") 289 | if err2 != nil { 290 | t.Error(err2) 291 | } 292 | 293 | crd1Hash, _ := generateHashOnCrd(data[0]) 294 | crd2Hash, _ := generateHashOnCrd(data2[0]) 295 | if crd1Hash == crd2Hash { 296 | t.Error("Failed to generate two different hashes") 297 | } 298 | } 299 | 300 | func TestGenerateHashOnChart(t *testing.T) { 301 | hash, _ := generalHashFunction("demo/charts/app-of-apps") 302 | h := hex.EncodeToString(hash) 303 | actualHash := "13aa148adefa3d633e5ce95584d3c95297a4417977837040cd67f0afbca17b5a" 304 | if h != actualHash { 305 | t.Errorf("Failed to generate a generic hash on a chart. got: %s wanted: %s", h, actualHash) 306 | } 307 | } 308 | 309 | func TestIsMissingDependencyErr(t *testing.T) { 310 | 311 | templateErrors := []struct { 312 | name string 313 | err error 314 | dependency bool 315 | }{ 316 | { 317 | name: "Missing charts", 318 | err: errors.New( 319 | "Error: found in Chart.yaml, but missing in charts/ directory: postgresql", 320 | ), 321 | dependency: true, 322 | }, 323 | { 324 | name: "Missing requirements", 325 | err: errors.New( 326 | "Error: found in requirements.yaml, but missing in charts", 327 | ), 328 | dependency: true, 329 | }, 330 | { 331 | name: "Chart error", 332 | err: errors.New( 333 | "no such file or directory", 334 | ), 335 | dependency: false, 336 | }, 337 | } 338 | 339 | for _, tt := range templateErrors { 340 | t.Run(tt.name, func(t *testing.T) { 341 | got := IsMissingDependencyErr(tt.err) 342 | if got != tt.dependency { 343 | t.Errorf("%v got %t wanted %t", tt.name, got, tt.dependency) 344 | } 345 | }) 346 | } 347 | 348 | } 349 | 350 | func TestEmptyManifest(t *testing.T) { 351 | 352 | manifestErrors := []struct { 353 | manifest string 354 | name string 355 | expected bool 356 | err error 357 | }{ 358 | { 359 | name: "Check known empty file", 360 | manifest: "pkg/helm/test_files/empty_manifest.yaml", 361 | expected: true, 362 | err: nil, 363 | }, 364 | { 365 | name: "Check known non empty file", 366 | manifest: "pkg/helm/test_files/crdData_multiple_crd_testfile.yaml", 367 | expected: false, 368 | err: nil, 369 | }, 370 | { 371 | name: "Check missing file", 372 | manifest: "pkg/helm/test_files/i_dont_exist.yaml", 373 | expected: false, 374 | err: errors.New("stat pkg/helm/test_files/i_dont_exist.yaml: no such file or directory"), 375 | }, 376 | } 377 | 378 | for _, tt := range manifestErrors { 379 | t.Run(tt.name, func(t *testing.T) { 380 | got, err := EmptyManifest(tt.manifest) 381 | if !errors.Is(err, tt.err) { 382 | if !strings.Contains(err.Error(), "no such file or directory") { 383 | t.Errorf("unexpected error got: %v wanted: %v", tt.err, err) 384 | } 385 | } 386 | if got != tt.expected { 387 | t.Errorf("got: %t wanted: %t", got, tt.expected) 388 | } 389 | }) 390 | } 391 | 392 | } 393 | -------------------------------------------------------------------------------- /pkg/helm/test_files/SymDir: -------------------------------------------------------------------------------- 1 | nonSymDir -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_multiple_crd_testfile.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: prod-cluster 6 | namespace: argocd 7 | spec: 8 | destination: 9 | namespace: argocd 10 | server: https://kubernetes.default.svc 11 | project: default 12 | source: 13 | helm: 14 | parameters: 15 | - name: region 16 | value: us-east-1 17 | - name: renderBaseDir 18 | value: /zz.auto-generated/root 19 | valueFiles: 20 | - ../../overrides/bootstrap/prod-cluster.yaml 21 | path: charts/app-of-apps 22 | repoURL: https://github.com/chime/mani-diffy 23 | targetRevision: HEAD 24 | syncPolicy: 25 | syncOptions: 26 | - CreateNamespace=true 27 | --- 28 | apiVersion: argoproj.io/v1alpha1 29 | kind: Application 30 | metadata: 31 | name: test-cluster 32 | namespace: argocd 33 | spec: 34 | destination: 35 | namespace: argocd 36 | server: https://kubernetes.default.svc 37 | project: default 38 | source: 39 | helm: 40 | parameters: 41 | - name: region 42 | value: us-east-1 43 | - name: renderBaseDir 44 | value: /zz.auto-generated/root 45 | valueFiles: 46 | - ../../overrides/bootstrap/test-cluster.yaml 47 | path: charts/app-of-apps 48 | repoURL: https://github.com/chime/mani-diffy 49 | targetRevision: HEAD 50 | syncPolicy: 51 | syncOptions: 52 | - CreateNamespace=true 53 | -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_override_testfile.yaml: -------------------------------------------------------------------------------- 1 | children: 2 | infra: 3 | chart: project 4 | destinations: 5 | - namespace: "*" 6 | server: "*" 7 | -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_override_testfile_sym_link.yaml: -------------------------------------------------------------------------------- 1 | crdData_override_testfile.yaml -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_testfile.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: prod-cluster 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: argocd 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | helm: 13 | parameters: 14 | - name: region 15 | value: us-east-1 16 | valueFiles: 17 | - ../../overrides/bootstrap/prod-cluster.yaml 18 | path: demo/charts/app-of-apps 19 | repoURL: https://github.com/chime/mani-diffy 20 | targetRevision: HEAD 21 | syncPolicy: 22 | syncOptions: 23 | - CreateNamespace=true 24 | -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_testfile_2.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: argoproj.io/v1alpha1 2 | kind: Application 3 | metadata: 4 | name: prod-cluster 5 | namespace: argocd 6 | spec: 7 | destination: 8 | namespace: argocd 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | helm: 13 | parameters: 14 | - name: region 15 | value: us-east-1 16 | - name: testName 17 | value: testValue 18 | valueFiles: 19 | - ../../overrides/bootstrap/prod-cluster.yaml 20 | - ../../overrides/bootstrap/fake_file.yaml 21 | path: charts/app-of-apps 22 | repoURL: https://github.com/chime/mani-diffy 23 | targetRevision: HEAD 24 | syncPolicy: 25 | syncOptions: 26 | - CreateNamespace=true 27 | -------------------------------------------------------------------------------- /pkg/helm/test_files/crdData_testfile_3.yaml: -------------------------------------------------------------------------------- 1 | # Source: app-of-apps/templates/apps.yaml 2 | apiVersion: argoproj.io/v1alpha1 3 | kind: Application 4 | metadata: 5 | name: test-service-bar 6 | spec: 7 | destination: 8 | namespace: argocd 9 | server: https://kubernetes.default.svc 10 | project: default 11 | source: 12 | repoURL: https://github.com/my-org/my-repo.git 13 | path: demo/charts/service 14 | helm: 15 | version: v3 16 | parameters: 17 | - name: env 18 | value: test 19 | valueFiles: 20 | - ../../overrides/service/bar/base.yaml 21 | - ../../overrides/service/bar/test.yaml 22 | syncPolicy: 23 | automated: {} -------------------------------------------------------------------------------- /pkg/helm/test_files/empty_manifest.yaml: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chime/mani-diffy/cbbd15440daf1a16d0f45f2208dab17b28d224c8/pkg/helm/test_files/empty_manifest.yaml -------------------------------------------------------------------------------- /pkg/helm/test_files/hash_testfile.sum: -------------------------------------------------------------------------------- 1 | hash: 2eeaf6db950a0fbd5462bc2950a31f687f05c7c99c048eee1a8dd7ec6b513ac4 2 | -------------------------------------------------------------------------------- /pkg/helm/test_files/nonSymDir/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chime/mani-diffy/cbbd15440daf1a16d0f45f2208dab17b28d224c8/pkg/helm/test_files/nonSymDir/.gitkeep -------------------------------------------------------------------------------- /pkg/kustomize/kustomize.go: -------------------------------------------------------------------------------- 1 | package kustomize 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | var ErrNotSupported = errors.New("kustomize not supported") 8 | --------------------------------------------------------------------------------