├── .github ├── dependabot.yml └── workflows │ ├── ci.yaml │ ├── codeql-analysis.yml │ ├── pull.yaml │ ├── release-helm.yml │ └── release.yaml ├── .gitignore ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── assets └── images │ ├── argocd-vault-replacer-diagram.drawio │ └── argocd-vault-replacer-diagram.png ├── charts └── argocd-vault-replacer-example-chart │ ├── Chart.yaml │ ├── README.md │ ├── templates │ ├── configmap.yaml │ └── secret.yaml │ └── values.yaml ├── docs └── modifiers.md ├── examples ├── example-manifests │ ├── configmap-multi.yaml │ ├── configmap.yaml │ └── secret.yaml ├── example-third-party-helm-chart │ ├── example │ │ ├── Chart.yaml │ │ └── values.yaml │ └── readme.md └── kustomize │ └── argocd │ ├── argo-cm.yaml │ ├── argocd-vault-replacer-secret.yaml │ ├── argocd-vault-replacer.yaml │ ├── kustomization.yaml │ ├── patch-serviceAccount.yaml │ └── serviceaccount.yaml ├── go.mod ├── go.sum ├── main.go ├── main_test.go ├── renovate.json ├── src ├── bwvaluesource │ └── bwValueSource.go ├── modifier │ ├── base64.go │ ├── htaccess.go │ ├── jsonKeyedObject.go │ ├── jsonKeyedObject_test.go │ ├── jsonList.go │ ├── jsonList_test.go │ ├── jsonObjectToList.go │ ├── jsonObjectToList_test.go │ ├── jsonPairedObject.go │ ├── jsonPairedObject_test.go │ ├── modifier.go │ ├── modify.go │ ├── modify_test.go │ └── valuesText.go ├── substitution │ ├── mockValueSource.go │ ├── mockValueSource_test.go │ ├── substitute.go │ ├── substituteValue.go │ ├── substituteValue_test.go │ ├── substitute_test.go │ └── valueSource.go └── vaultvaluesource │ ├── kubernetes.go │ ├── vaultValueSource.go │ └── vaultValueSource_test.go ├── test └── plain │ ├── configmap.yaml │ ├── expected.txt │ └── secret.yaml └── testvalues ├── README.md ├── foo ├── lemon └── fig ├── test.json ├── test.yaml └── test.yml /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for Golang 5 | - package-ecosystem: "gomod" 6 | directory: "/" 7 | schedule: 8 | interval: "daily" 9 | commit-message: 10 | # Prefix all commit messages with "go" 11 | prefix: "go" 12 | labels: 13 | - "golang" 14 | - "dependencies" 15 | reviewers: 16 | - "Joibel" 17 | - "tico24" 18 | 19 | # Maintain dependencies for GitHub Actions 20 | - package-ecosystem: "github-actions" 21 | directory: "/" 22 | schedule: 23 | interval: "daily" 24 | commit-message: 25 | # Prefix all commit messages with "gh-actions" 26 | prefix: "gh-actions" 27 | labels: 28 | - "gh-actions" 29 | - "dependencies" 30 | reviewers: 31 | - "Joibel" 32 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | 7 | jobs: 8 | gogitops: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: GoGitOps Step 13 | id: gogitops 14 | uses: beaujr/gogitops-action@v0.2 15 | with: 16 | github-actions-token: ${{secrets.GITHUB_TOKEN}} 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: 'go.mod' 26 | 27 | - name: Build 28 | run: go build -v ./... 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | push_to_registry: 33 | name: Push Docker image to GitHub Packages 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Check out the repo 37 | uses: actions/checkout@v4 38 | - name: Set up QEMU 39 | uses: docker/setup-qemu-action@v3 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | - name: Login to GitHub Container Registry 43 | uses: docker/login-action@v3 44 | with: 45 | registry: ghcr.io 46 | username: ${{ github.repository_owner }} 47 | password: ${{ secrets.CR_PAT }} 48 | - name: Push to GitHub Packages 49 | uses: docker/build-push-action@v6 50 | with: 51 | context: . 52 | file: ./Dockerfile 53 | push: true 54 | platforms: linux/amd64, linux/arm64 55 | tags: ghcr.io/crumbhole/argocd-vault-replacer:latest 56 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ main ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ main ] 20 | schedule: 21 | - cron: '38 13 * * 4' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | 28 | strategy: 29 | fail-fast: false 30 | matrix: 31 | language: [ 'go' ] 32 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 33 | # Learn more: 34 | # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed 35 | 36 | steps: 37 | - name: Checkout repository 38 | uses: actions/checkout@v4 39 | 40 | - name: Set up Go 41 | uses: actions/setup-go@v5 42 | with: 43 | go-version: 1.17 44 | 45 | # Initializes the CodeQL tools for scanning. 46 | - name: Initialize CodeQL 47 | uses: github/codeql-action/init@v3 48 | with: 49 | languages: ${{ matrix.language }} 50 | # If you wish to specify custom queries, you can do so here or in a config file. 51 | # By default, queries listed here will override any specified in a config file. 52 | # Prefix the list here with "+" to use these queries and those in the config file. 53 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 54 | 55 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 56 | # If this step fails, then you should remove it and run the build manually (see below) 57 | - name: Autobuild 58 | uses: github/codeql-action/autobuild@v3 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/pull.yaml: -------------------------------------------------------------------------------- 1 | name: Build and test 2 | 3 | on: 4 | pull_request: 5 | branches: [ main ] 6 | 7 | jobs: 8 | gogitops: 9 | runs-on: ubuntu-latest 10 | steps: 11 | - uses: actions/checkout@v4 12 | - name: GoGitOps Step 13 | id: gogitops 14 | uses: beaujr/gogitops-action@v0.2 15 | with: 16 | github-actions-token: ${{secrets.GITHUB_TOKEN}} 17 | build: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Set up Go 23 | uses: actions/setup-go@v5 24 | with: 25 | go-version-file: 'go.mod' 26 | 27 | - name: Build 28 | run: go build -v ./... 29 | 30 | - name: Test 31 | run: go test -v ./... 32 | push_to_registry: 33 | name: Push Docker image to GitHub Packages 34 | runs-on: ubuntu-latest 35 | steps: 36 | - name: Check out the repo 37 | uses: actions/checkout@v4 38 | - name: Set up QEMU 39 | uses: docker/setup-qemu-action@v3 40 | - name: Set up Docker Buildx 41 | uses: docker/setup-buildx-action@v3 42 | - name: Push to GitHub Packages 43 | uses: docker/build-push-action@v6 44 | with: 45 | context: . 46 | file: ./Dockerfile 47 | push: false 48 | platforms: linux/amd64, linux/arm64 49 | tags: ghcr.io/crumbhole/argocd-vault-replacer:pull 50 | -------------------------------------------------------------------------------- /.github/workflows/release-helm.yml: -------------------------------------------------------------------------------- 1 | name: Release Helm Chart 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | paths: 8 | - 'charts/argocd-vault-replacer-example-chart/Chart.yaml' 9 | 10 | jobs: 11 | release: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Configure Git 20 | run: | 21 | git config user.name "$GITHUB_ACTOR" 22 | git config user.email "$GITHUB_ACTOR@users.noreply.github.com" 23 | 24 | - name: Install Helm 25 | uses: azure/setup-helm@v4 26 | with: 27 | version: v3.4.0 28 | 29 | - name: Run chart-releaser 30 | uses: helm/chart-releaser-action@v1.7.0 31 | with: 32 | charts_dir: charts 33 | env: 34 | CR_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 35 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | # .github/workflows/release.yaml 2 | name: Release 3 | 4 | on: 5 | release: 6 | types: [created] 7 | 8 | jobs: 9 | releases-matrix: 10 | name: Release Go Binary 11 | runs-on: ubuntu-latest 12 | strategy: 13 | matrix: 14 | # build and publish in parallel: linux/386, linux/amd64, windows/386, windows/amd64, darwin/386, darwin/amd64 15 | goos: [linux, darwin] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - uses: wangyoucao577/go-release-action@v1.53 19 | with: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | goos: ${{ matrix.goos }} 22 | goarch: amd64 23 | goversion: "https://golang.org/dl/go1.20.1.linux-amd64.tar.gz" 24 | binary_name: "vault-replacer" 25 | extra_files: LICENSE README.md 26 | push_to_registry: 27 | name: Push Docker image to GitHub Packages 28 | runs-on: ubuntu-latest 29 | steps: 30 | - name: Check out the repo 31 | uses: actions/checkout@v4 32 | - name: Get git tag 33 | uses: little-core-labs/get-git-tag@v3.0.2 34 | - name: Set up QEMU 35 | uses: docker/setup-qemu-action@v3 36 | - name: Set up Docker Buildx 37 | uses: docker/setup-buildx-action@v3 38 | - name: Login to GitHub Container Registry 39 | uses: docker/login-action@v3 40 | with: 41 | registry: ghcr.io 42 | username: ${{ github.repository_owner }} 43 | password: ${{ secrets.CR_PAT }} 44 | - name: Push to GitHub Packages 45 | uses: docker/build-push-action@v6 46 | with: 47 | context: . 48 | file: ./Dockerfile 49 | push: true 50 | platforms: linux/amd64, linux/arm64 51 | tags: | 52 | ghcr.io/crumbhole/argocd-vault-replacer:${{ env.GIT_TAG_NAME }} 53 | ghcr.io/crumbhole/argocd-vault-replacer:stable 54 | ghcr.io/crumbhole/argocd-vault-replacer:latest 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/go,emacs,vim 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=go,emacs,vim 3 | 4 | ### Emacs ### 5 | # -*- mode: gitignore; -*- 6 | *~ 7 | \#*\# 8 | /.emacs.desktop 9 | /.emacs.desktop.lock 10 | *.elc 11 | auto-save-list 12 | tramp 13 | .\#* 14 | 15 | # Org-mode 16 | .org-id-locations 17 | *_archive 18 | ltximg/** 19 | 20 | # flymake-mode 21 | *_flymake.* 22 | 23 | # eshell files 24 | /eshell/history 25 | /eshell/lastdir 26 | 27 | # elpa packages 28 | /elpa/ 29 | 30 | # reftex files 31 | *.rel 32 | 33 | # AUCTeX auto folder 34 | /auto/ 35 | 36 | # cask packages 37 | .cask/ 38 | dist/ 39 | 40 | # Flycheck 41 | flycheck_*.el 42 | 43 | # server auth directory 44 | /server/ 45 | 46 | # projectiles files 47 | .projectile 48 | 49 | # directory configuration 50 | .dir-locals.el 51 | 52 | # network security 53 | /network-security.data 54 | 55 | 56 | ### Go ### 57 | # Binaries for programs and plugins 58 | *.exe 59 | *.exe~ 60 | *.dll 61 | *.so 62 | *.dylib 63 | 64 | # Test binary, built with `go test -c` 65 | *.test 66 | 67 | # Output of the go coverage tool, specifically when used with LiteIDE 68 | *.out 69 | 70 | # Dependency directories (remove the comment below to include it) 71 | # vendor/ 72 | 73 | ### Go Patch ### 74 | /vendor/ 75 | /Godeps/ 76 | 77 | ### Vim ### 78 | # Swap 79 | [._]*.s[a-v][a-z] 80 | !*.svg # comment out if you don't need vector files 81 | [._]*.sw[a-p] 82 | [._]s[a-rt-v][a-z] 83 | [._]ss[a-gi-z] 84 | [._]sw[a-p] 85 | 86 | # Session 87 | Session.vim 88 | Sessionx.vim 89 | 90 | # Temporary 91 | .netrwhist 92 | # Auto-generated tag files 93 | tags 94 | # Persistent undo 95 | [._]*.un~ 96 | 97 | # End of https://www.toptal.com/developers/gitignore/api/go,emacs,vim 98 | build/ 99 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.24.3 as builder 2 | ADD . /build 3 | WORKDIR /build 4 | RUN go vet ./... 5 | RUN go test ./... 6 | RUN CGO_ENABLED=0 go build -buildvcs=false -o build/argocd-vault-replacer 7 | 8 | FROM alpine:3.21.3 as putter 9 | COPY --from=builder /build/build/argocd-vault-replacer . 10 | USER 999 11 | ENTRYPOINT [ "cp", "argocd-vault-replacer", "/custom-tools/" ] 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2021, Alan Clucas 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: all clean code-vet code-fmt test get 2 | 3 | DEPS := $(shell find . -type f -name "*.go" -printf "%p ") 4 | 5 | all: code-vet code-fmt test build/argocd-vault-replacer 6 | 7 | clean: 8 | $(RM) -rf build 9 | 10 | get: $(DEPS) 11 | go get ./... 12 | 13 | test: get 14 | go test ./... 15 | 16 | build/argocd-vault-replacer: $(DEPS) get 17 | mkdir -p build 18 | CGO_ENABLED=0 go build -o build ./... 19 | 20 | code-vet: $(DEPS) get 21 | ## Run go vet for this project. More info: https://golang.org/cmd/vet/ 22 | @echo go vet 23 | go vet $$(go list ./... ) 24 | 25 | code-fmt: $(DEPS) get 26 | ## Run go fmt for this project 27 | @echo go fmt 28 | go fmt $$(go list ./... ) 29 | 30 | lint: $(DEPS) get 31 | ## Run golint for this project 32 | @echo golint 33 | golint $$(go list ./... ) 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # argocd-vault-replacer 2 | A plugin for [ArgoCD lovely plugin](https://github.com/crumbhole/argocd-lovely-plugin) to replace placeholders in Kubernetes manifests with secrets stored in [Hashicorp Vault](https://www.vaultproject.io/). The binary will scan the current directory recursively for any .yaml (or .yml if you're so inclined) files, or take yaml from stdin, and attempt to replace strings of the form `` with those obtained from a Vault kv2 store. 3 | 4 | If you use it as the reader in a unix pipe, it will instead read from stdin. In this scenario it can post-process the output of another tool, such as Kustomize or Helm. 5 | 6 | This plugin used to be available as a direct plugin to ArgoCD, but has not been adapted for ArgoCD 2.7's need for running as a sidecar. If this need is one you have, please raise an issue or ideally a PR. The authors now only use this through lovely plugin. 7 | 8 | Note: This and previous versions of this plugin only talk to vault, and hence can also be specified as . Future plans may include other secret providers. 9 | 10 | 11 | 12 | ## Why? 13 | - Allows you to invest in Git Ops without compromising secret security. 14 | - Configuration goes into Git. 15 | - Secrets go into Vault. 16 | - yaml-agnostic. Supports any Kubernetes resource type as long as it can be expressed in .yaml (or .yml). 17 | - Also supports Argo CD-managed Kustomize and Helm charts 18 | - Native Vault-Kubernetes authentication means you don't have to renew tokens or store/passthrough approle role-ids and secret-ids. 19 | 20 | #Installing 21 | 22 | ## As a lovely plugin 23 | 24 | Install [ArgoCD lovely plugin](https://github.com/crumbhole/argocd-lovely-plugin) using the ghcr.io/crumbhole/argocd-lovely-plugin-cmp-vault image. Setup your vault-replacer environment variables in that sidecar. 25 | 26 | ## Installing as an Argo CD Plugin (deprecated) 27 | You can use [our Kustomization example](https://github.com/crumbhole/argocd-vault-replacer/tree/main/examples/kustomize/argocd) to install Argo CD and to bootstrap the installation of the plugin at the same time. However the steps below will detail what is required should you wish to do things more manually. The Vault authentication setup cannot be done with Kustomize and must be done manually. 28 | 29 | ## Vault Kubernetes Authentication 30 | You will need to set up the Vault Kubernetes authentication method for your cluster. 31 | 32 | You will need to create a service account. In this example, our service account will be called 'argocd'. Our example creates the serviceAccount in the argocd namespace: 33 | 34 | ```YAML 35 | apiVersion: v1 36 | kind: ServiceAccount 37 | metadata: 38 | name: argocd 39 | namespace: argocd 40 | ``` 41 | 42 | For Kubernetes [version 1.24](https://github.com/kubernetes/kubernetes/blob/master/CHANGELOG/CHANGELOG-1.24.md#urgent-upgrade-notes) and newer there is no automatic service account token generated. This must be generated as a secret and the service account must refer to said secret: 43 | 44 | ```YAML 45 | apiVersion: v1 46 | kind: ServiceAccount 47 | metadata: 48 | namespace: argocd 49 | name: argocd 50 | secrets: 51 | - name: argocd-sa-token 52 | --- 53 | apiVersion: v1 54 | kind: Secret 55 | metadata: 56 | namespace: argocd 57 | name: argocd-sa-token 58 | annotations: 59 | kubernetes.io/service-account.name: argocd 60 | type: kubernetes.io/service-account-token 61 | ``` 62 | 63 | You will need to tell Vault about this Service Account and what policy/policies it maps to: 64 | 65 | ``` 66 | vault write auth/kubernetes/role/argocd \ 67 | bound_service_account_names=argocd \ 68 | bound_service_account_namespaces=argocd \ 69 | policies=argocd \ 70 | ttl=1h 71 | ``` 72 | This is better documented by Hashicorp themselves, do please refer to [their documentation](https://www.vaultproject.io/docs/auth/kubernetes). 73 | 74 | Lastly, you will need to modify the argocd-repo-server deployment to use your new serviceAccount, and to allow the serviceAccountToken to automount when the pod starts up. You must patch the deployment with: 75 | ```YAML 76 | apiVersion: apps/v1 77 | kind: Deployment 78 | metadata: 79 | name: patch-serviceAccount 80 | spec: 81 | template: 82 | spec: 83 | serviceAccount: argocd 84 | automountServiceAccountToken: true 85 | ``` 86 | ## Plugin Installation 87 | In order to install the plugin into Argo CD, you can either build your own Argo CD image with the plugin already inside, or make use of an Init Container to pull the binary. Argo CD's documentation provides further information how to do this: https://argoproj.github.io/argo-cd/operator-manual/custom_tools/ 88 | 89 | We offer a pre-built init container that moves the binary into /custom-tools on startup, so an init container manifest will look something like this: 90 | ```YAML 91 | containers: 92 | - name: argocd-repo-server 93 | volumeMounts: 94 | - name: custom-tools 95 | mountPath: /usr/local/bin/argocd-vault-replacer 96 | subPath: argocd-vault-replacer 97 | envFrom: 98 | - secretRef: 99 | name: argocd-vault-replacer-credentials 100 | volumes: 101 | - name: custom-tools 102 | emptyDir: {} 103 | initContainers: 104 | - name: argocd-vault-replacer-install 105 | image: ghcr.io/crumbhole/argocd-vault-replacer 106 | imagePullPolicy: Always 107 | volumeMounts: 108 | - mountPath: /custom-tools 109 | name: custom-tools 110 | ``` 111 | The above references a Kubernetes secret called "argocd-vault-replacer-credentials". We use this to pass through the mandatory ARGOCD_ENV_VAULT_ADDR environment variable. We could also use it to pass through optional variables too 112 | ```YAML 113 | apiVersion: v1 114 | data: 115 | ARGOCD_ENV_VAULT_ADDR: aHR0cHM6Ly92YXVsdC5leGFtcGxlLmJpeg== 116 | kind: Secret 117 | metadata: 118 | name: argocd-vault-replacer-credentials 119 | namespace: argocd 120 | type: Opaque 121 | ``` 122 | 123 | Environment Variables: 124 | 125 | | Environment Variable Name | Purpose | Example | Mandatory? | 126 | |-------------------------- |-------------------------------------------------------------------------------------------------------------------------------------- |---------------------------------- |----------- | 127 | | ARGOCD_ENV_VAULT_ADDR | Provides argocd-vault-replacer with the URL to your Hashicorp Vault instance. | https://vault.examplecompany.biz | Y 128 | | ARGOCD_ENV_VAULT_TOKEN | A valid Vault authentication token. This should only be used for debugging. This won't work inside kubernetes if you have a service account token available, as the tool considers a service account token that fails to authenticate a complete failure. You'll have to run a pod without a service account if you want to use this. The token cannot be renewed by the tool so if it expires, the tool will stop. | s.LLijB190n3c8s4fiSuvTdVNM | N 129 | | ARGOCD_ENV_VAULT_ROLE | The name of the role for the VAULT_TOKEN. This defaults to 'argocd'. | argocd-role | N 130 | | ARGOCD_ENV_VAULT_AUTH_PATH | Determines the authorization path for Kubernetes authentication. This defaults to 'kubernetes' so will probably not need configuring. | kubernetes | N 131 | 132 | Before Argo CD 2.4 these did not need to be prefixed with ARGOCD_ENV_, and the current version will accept either type, with precedence given to the ARGOCD_ENV_ version. 133 | 134 | If you are passing the configuration in as application environment variables in Argo CD 2.4 or higher you must not put the ARGOCD_ENV_ prefix on them, as Argo CD does that for you. 135 | 136 | ## Plugin Configuration 137 | After installing the plugin into the /custom-tools/ directory, you need to register it inside the Argo CD config. Declaratively, you can add this to your argocd-cm configmap file: 138 | 139 | ```YAML 140 | configManagementPlugins: |- 141 | - name: argocd-vault-replacer 142 | generate: 143 | command: ["argocd-vault-replacer"] 144 | - name: kustomize-argocd-vault-replacer 145 | generate: 146 | command: ["sh", "-c"] 147 | args: ["kustomize build . | argocd-vault-replacer"] 148 | - name: helm-argocd-vault-replacer 149 | init: 150 | command: ["/bin/sh", "-c"] 151 | args: ["helm dependency build"] 152 | generate: 153 | command: [sh, -c] 154 | args: ["helm template -n $ARGOCD_APP_NAMESPACE $ARGOCD_APP_NAME . | argocd-vault-replacer"] 155 | ``` 156 | 157 | This is documented further in Argo CD's documentation: https://argoproj.github.io/argo-cd/user-guide/config-management-plugins/ 158 | 159 | * argo-vault-replacer as a plugin will only work on a directory full of straight .yaml files. 160 | * kustomize-argo-vault-replacer as a plugin will take the output of kustomize and then do vault-replacement on those files. Note: This won't allow you to use the argo application kustomization options, it just runs a straight kustomize. 161 | * helm-argo-vault-replacer as a plugin will take the output of Helm and then do vault-replacement on those files. Note: This won't allow you to use the argo application Helm options, it just runs a straight Helm with the default argo name. 162 | ## Testing 163 | 164 | Create a test yaml file that will be used to pull a secret from Vault. The below will look in Vault for /path/to/your/secret and will return the key 'secretkey', it will then base64 encode that value. As we are using a Vault kv2 store, we must include ..`/data/`.. in our path: 165 | 166 | ```YAML 167 | apiVersion: v1 168 | kind: Secret 169 | metadata: 170 | name: argocd-vault-replacer-secret 171 | data: 172 | sample-secret: 173 | type: Opaque 174 | ``` 175 | In this example, we pushed the above to `https://github.com/replace-me/argocd-vault-replacer-test/argocd-vault-replacer-secret.yaml` 176 | 177 | We then deploy this as an Argo CD application, making sure we tell the application to use the argocd-vault-replacer plugin: 178 | 179 | ```YAML 180 | apiVersion: argoproj.io/v1alpha1 181 | kind: Application 182 | metadata: 183 | name: argocd-vault-replacer-test 184 | spec: 185 | destination: 186 | server: 'https://kubernetes.default.svc' 187 | namespace: argocd-vault-replacer-test 188 | syncPolicy: 189 | automated: 190 | prune: true 191 | selfHeal: true 192 | syncOptions: 193 | - CreateNamespace=true 194 | source: 195 | repoURL: 'https://github.com/replace-me' 196 | path: argocd-vault-replacer-test 197 | plugin: 198 | name: argocd-vault-replacer 199 | targetRevision: HEAD 200 | ``` 201 | 202 | There are further examples to use for testing in the [examples directory](https://github.com/crumbhole/argocd-vault-replacer/tree/main/examples/). 203 | ## A deep-dive on authentication 204 | 205 | The tool only has two methods of authenticating with Vault: 206 | * Using kubernetes authentication method https://github.com/hashicorp/vault/blob/master/website/content/docs/auth/kubernetes.mdx 207 | * Using a token, which is only intended for debugging 208 | 209 | Both methods expect the environment variable ARGOCD_ENV_VAULT_ADDR to be set. 210 | 211 | It will attempt to use kubernetes authentication through an appropriate service account first, and complain if that doesn't work. It will then use VAULT_TOKEN which should be a valid token. This tool has no way of renewing a token or obtaining one other than through a kubernetes service account. 212 | 213 | To use the kubernetes service account your pod should be running with the appropriate service account, and will try to obtain the JWT token from /var/run/secrets/kubernetes.io/serviceaccount/token which is the default location. 214 | 215 | It will use the environment variable ARGOCD_ENV_VAULT_ROLE as the name of the role for that token, defaulting to "argocd". 216 | It will use the environment variable ARGOCD_ENV_VAULT_AUTH_PATH to determine the authorization path for kubernetes authentication. This defaults in this tool and in vault to "kubernetes" so will probably not need configuring. 217 | 218 | The vault authentication token that the tool gets will not be cached, nor will it be renewed. It is expected that the token will last for the length of the tool's invokation, which is usually a reasonable assumption in the use case for which it was designed. 219 | 220 | ## Valid vault paths 221 | 222 | Currently the only valid 'URL style' to a path is 223 | 224 | `` 225 | 226 | You must put ..`/data/`.. into the path. If your path or key contains `~`, `<`, `>` or `|` you must URL escape it. If your path or key has one or more leading or trailing spaces or tabs you must URL escape them you weirdo. 227 | 228 | Any base64 encoded strings will be decoded and subsitution will happen within them (and end up being base64 encoded afterwards). These base64 strings require some form of whitespace (any non-base64 valid character) around them in order to be detected. 229 | 230 | ## Modifiers 231 | 232 | You can modify the resulting output with the following modifiers: 233 | 234 | * base64: Will base64 encode the secret. Use for data: sections in kubernetes secrets. 235 | * Other modifiers are documented in [docs/modifiers](docs/modifiers.md) 236 | 237 | ## Rotating secrets in Vault 238 | Currently, because Argo CD cannot monitor Vault for changes, when you change a secret in Vault, Argo CD will not automatically update your Kubernetes resources with the new value. You will have to either push a change to git, use the Hard Refresh option in Argo CD, or force Argo CD to heal by deleting the Kubernetes resource in question. 239 | 240 | ## Development 241 | 242 | This project only builds with go 1.18 (and presumably later when later happens). 243 | 244 | You can build in the docker container, or look at the [Makefile] to build and test conventionally. 245 | 246 | Please add tests for any new/changed functionality. 247 | -------------------------------------------------------------------------------- /assets/images/argocd-vault-replacer-diagram.drawio: -------------------------------------------------------------------------------- 1 | 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 -------------------------------------------------------------------------------- /assets/images/argocd-vault-replacer-diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crumbhole/argocd-vault-replacer/b437b312c20b8f9c5683380ff116fe48d4fa8132/assets/images/argocd-vault-replacer-diagram.png -------------------------------------------------------------------------------- /charts/argocd-vault-replacer-example-chart/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: argocd-vault-replacer-example-chart 3 | description: A Helm chart to demonstrate how argocd-vault-replacer can work with Helm 4 | type: application 5 | version: 0.0.7 6 | appVersion: 0.0.0 7 | -------------------------------------------------------------------------------- /charts/argocd-vault-replacer-example-chart/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crumbhole/argocd-vault-replacer/b437b312c20b8f9c5683380ff116fe48d4fa8132/charts/argocd-vault-replacer-example-chart/README.md -------------------------------------------------------------------------------- /charts/argocd-vault-replacer-example-chart/templates/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-vault-replacer-example-configmap 5 | data: 6 | sample-secret: {{ .Values.configMap }} -------------------------------------------------------------------------------- /charts/argocd-vault-replacer-example-chart/templates/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: argocd-vault-replacer-example-secret 5 | data: 6 | sample-secret: {{ .Values.secret.sampleSecret | b64enc }} 7 | sample-secret-quoted: {{ .Values.secret.sampleSecretQuoted | b64enc | quote }} 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /charts/argocd-vault-replacer-example-chart/values.yaml: -------------------------------------------------------------------------------- 1 | # You can inject Vault secrets into any valid Kubernetes resource. 2 | # Common types are 'secret' and 'configMaps'. 3 | # This helm chart deploys a secret and a config map and populates them with Value values defined on a given path. 4 | 5 | secret: 6 | # The 'sample-secret' will be pulled from vault and populated into a secret called 'argocd-vault-replacer-example-secret' 7 | sampleSecret: 8 | # 'quoted-secret' is as above but will wrap the value in quotes. 9 | sampleSecretQuoted: 10 | 11 | # This will populate a configMap with the value from Vault on the given path. 12 | # The config map is called 'argocd-vault-replacer-example-configmap' 13 | configMap: 14 | 15 | # Once deployed, you can read the secret and configMap to confirm that your Vault values are present: 16 | 17 | # kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.sample-secret}" | base64 --decode 18 | # kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.quoted-secret}" | base64 --decode 19 | # kubectl get configmaps argocd-vault-replacer-example-configmap -o jsonpath="{.data.sample-secret}" 20 | -------------------------------------------------------------------------------- /docs/modifiers.md: -------------------------------------------------------------------------------- 1 | # Modifiers 2 | 3 | Given the path contains 4 | 5 | |Key | Value| 6 | ---|--- 7 | |a|x| 8 | |b|y| 9 | |c|z| 10 | |d|j| 11 | 12 | ## valuesText 13 | 14 | This gets implicitly invoked, but can be explicitly called. 15 | 16 | ``` 17 | ~a~b~c -> xyz 18 | ~a~b~c|valuesText ->xyz 19 | ``` 20 | 21 | ## base64 22 | 23 | Base64 encodes the text 24 | 25 | ## jsonlist 26 | Just takes the values and makes a list 27 | ``` 28 | ~a~b~c|jsonlist -> ['x', 'y', 'z'] 29 | ``` 30 | ## jsonkeyedobject 31 | Uses the keys you specfied into vault as the keys to their values in the object 32 | ``` 33 | ~a~b~c|jsonkeyedobject -> {'a':'x', 'b':'y', 'c':'z'} 34 | ``` 35 | ## jsonpairedobject 36 | Takes pairs of values and makes the first into a key, second into a value. An odd number of keys is an error. 37 | ``` 38 | ~a~b~c~d|jsonpairedobject -> {'x':'y', 'z':'j'} 39 | ``` 40 | ## jsonobject2list(name,value) 41 | Takes a json object and splits it into a list of objects with the keys coming from the call, and the values from the object. 42 | ``` 43 | ~a~b~c|jsonkeyedobject|jsonobject2list(name, value)[{'name':'a', 'value':'x'}, {'name':'b', 'value':'y'}, {'name':'c', 'value':'z'}] 44 | ~a~b|jsonpairedobject|jsonobject2list(user, password)[{'user':'x', 'password','y'}] 45 | ~a~b~c~d|jsonpairedobject|jsonobject2list(user, password)[{'user':'x', 'password','y'}, {'user':'z', 'password','j'}] 46 | ``` 47 | 48 | ## json2htaccess 49 | _Don't use this_, see [issue #6](https://github.com/crumbhole/argocd-vault-replacer/issues/6) 50 | Takes an object list (jsonobject2list) and uses the key called user and the key called password from each object to make an htaccess file. Any object in the list without these keys is an error. 51 | 52 | 53 | -------------------------------------------------------------------------------- /examples/example-manifests/configmap-multi.yaml: -------------------------------------------------------------------------------- 1 | # An example demonstrating how Vault paths can be completely different in the same configmap. 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: argocd-vault-replacer-example-configmap-multi 6 | data: 7 | sample-secret-one: 8 | sample-secret-two: 9 | -------------------------------------------------------------------------------- /examples/example-manifests/configmap.yaml: -------------------------------------------------------------------------------- 1 | # The below will look in Vault for /path/to/your/secret and will return the key 'secretkey'. 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: argocd-vault-replacer-example-configmap 6 | data: 7 | sample-secret: 8 | -------------------------------------------------------------------------------- /examples/example-manifests/secret.yaml: -------------------------------------------------------------------------------- 1 | # The below will look in Vault for /path/to/your/secret and will return the key 'secretkey', it will then base64 encode that value. 2 | apiVersion: v1 3 | kind: Secret 4 | metadata: 5 | name: argocd-vault-replacer-example-secret 6 | data: 7 | sample-secret: 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /examples/example-third-party-helm-chart/example/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v2 2 | name: argocd-vault-replacer-example 3 | version: 0.0.1 4 | 5 | dependencies: 6 | - name: argocd-vault-replacer-example-chart 7 | version: 0.0.7 8 | repository: https://crumbhole.github.io/argocd-vault-replacer/ 9 | -------------------------------------------------------------------------------- /examples/example-third-party-helm-chart/example/values.yaml: -------------------------------------------------------------------------------- 1 | argocd-vault-replacer-example-chart: 2 | # You can inject Vault secrets into any valid Kubernetes resource. 3 | # Common types are 'secret' and 'configMaps'. 4 | # This helm chart deploys a secret and a config map and populates them with Value values defined on a given path. 5 | 6 | secret: 7 | # The 'sample-secret' will be pulled from vault and populated into a secret called 'argocd-vault-replacer-example-secret' 8 | sampleSecret: 9 | # 'quoted-secret' is as above but will wrap the value in quotes. 10 | sampleSecretQuoted: 11 | 12 | # This will populate a configMap with the value from Vault on the given path. 13 | # The config map is called 'argocd-vault-replacer-example-configmap' 14 | configMap: 15 | 16 | # Once deployed, you can read the secret and configMap to confirm that your Vault values are present: 17 | 18 | # kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.sample-secret}" | base64 --decode 19 | # kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.quoted-secret}" | base64 --decode 20 | # kubectl get configmaps argocd-vault-replacer-example-configmap -o jsonpath="{.data.sample-secret}" 21 | -------------------------------------------------------------------------------- /examples/example-third-party-helm-chart/readme.md: -------------------------------------------------------------------------------- 1 | We have written a very simple Helm Chart to help you understand what you can and can't do with argocd-vault-replacer without breaking something in production. 2 | 3 | You will need to deploy our third party Helm chart into your cluster using argocd (via git): 4 | 5 | - Clone/copy the 'example' directory into your own git repo. 6 | - Modify values.yaml so that the vault paths and secrets are valid for your environment. 7 | 8 | Create a new Argo CD application to point to your repo: 9 | 10 | 11 | You will need to change: 12 | - spec.source.repoURL 13 | - spec.source.path 14 | 15 | ```YAML 16 | apiVersion: argoproj.io/v1alpha1 17 | kind: Application 18 | metadata: 19 | name: argocd-vault-replacer-example 20 | spec: 21 | destination: 22 | server: 'https://kubernetes.default.svc' 23 | namespace: example 24 | project: default 25 | syncPolicy: 26 | automated: 27 | prune: true 28 | selfHeal: true 29 | syncOptions: 30 | - CreateNamespace=true 31 | source: 32 | repoURL: 'https://github.com/crumbhole/argocd-vault-replacer/' 33 | path: examples/example-third-party-helm-chart/example 34 | targetRevision: HEAD 35 | plugin: 36 | name: helm-argocd-vault-replacer 37 | ``` 38 | 39 | Apply your application to Argo CD in the usual way. 40 | 41 | Argocd should then show a successful installation of the Helm chart into your cluster. You can then go and view the deployed artefacts and check whether the Vault secrets are as you expect them to be. You can read the secret and configmap to confirm that your Vault values are present: 42 | 43 | ``` 44 | kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.sample-secret}" | base64 --decode 45 | kubectl get secret argocd-vault-replacer-example-secret -o jsonpath="{.data.quoted-secret}" | base64 --decode 46 | kubectl get configmaps argocd-vault-replacer-example-configmap -o jsonpath="{.data.sample-secret}" 47 | ``` -------------------------------------------------------------------------------- /examples/kustomize/argocd/argo-cm.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | data: 3 | configManagementPlugins: |- 4 | - name: argocd-vault-replacer 5 | generate: 6 | command: ["argocd-vault-replacer"] 7 | - name: kustomize-argocd-vault-replacer 8 | generate: 9 | command: ["sh", "-c"] 10 | args: ["kustomize build . | argocd-vault-replacer"] 11 | - name: helm-argocd-vault-replacer 12 | init: 13 | command: ["/bin/sh", "-c"] 14 | args: ["helm dependency build"] 15 | generate: 16 | command: [sh, -c] 17 | args: ["helm template -n $ARGOCD_APP_NAMESPACE $ARGOCD_APP_NAME . | argocd-vault-replacer"] 18 | kind: ConfigMap 19 | metadata: 20 | labels: 21 | app.kubernetes.io/name: argocd-cm 22 | app.kubernetes.io/part-of: argocd 23 | name: argocd-cm 24 | -------------------------------------------------------------------------------- /examples/kustomize/argocd/argocd-vault-replacer-secret.yaml: -------------------------------------------------------------------------------- 1 | # Change the Vault Address to match yours (base64 encoded) 2 | apiVersion: v1 3 | data: 4 | ARGOCD_ENV_VAULT_ADDR: aHR0cHM6Ly92YXVsdC5leGFtcGxlLmJpeg== 5 | kind: Secret 6 | metadata: 7 | name: argocd-vault-replacer-secret 8 | type: Opaque 9 | -------------------------------------------------------------------------------- /examples/kustomize/argocd/argocd-vault-replacer.yaml: -------------------------------------------------------------------------------- 1 | # Downloads the plugin and moves it to /custom-tools, which is then mounted on the argocd-repo-server 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: argocd-vault-replacer 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: argocd-repo-server 11 | volumeMounts: 12 | - name: custom-tools 13 | mountPath: /usr/local/bin/argocd-vault-replacer 14 | subPath: argocd-vault-replacer 15 | envFrom: 16 | - secretRef: 17 | name: argocd-vault-replacer-secret 18 | volumes: 19 | - name: custom-tools 20 | emptyDir: {} 21 | initContainers: 22 | - name: argocd-vault-replacer-install 23 | image: ghcr.io/crumbhole/argocd-vault-replacer:stable 24 | imagePullPolicy: Always 25 | volumeMounts: 26 | - mountPath: /custom-tools 27 | name: custom-tools 28 | -------------------------------------------------------------------------------- /examples/kustomize/argocd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # kubectl kustomize . > argocd-deploy.yaml 2 | # 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | namespace: argocd 6 | 7 | resources: 8 | - github.com/argoproj/argo-cd/manifests/ha/cluster-install?ref=v2.5.5 9 | - argocd-vault-replacer-secret.yaml 10 | - serviceaccount.yaml 11 | 12 | patches: 13 | - path: argo-cm.yaml 14 | target: 15 | kind: ConfigMap 16 | name: argocd-cm 17 | - path: argocd-vault-replacer.yaml 18 | target: 19 | kind: Deployment 20 | name: argocd-repo-server 21 | - path: patch-serviceAccount.yaml 22 | target: 23 | kind: Deployment 24 | name: argocd-repo-server 25 | -------------------------------------------------------------------------------- /examples/kustomize/argocd/patch-serviceAccount.yaml: -------------------------------------------------------------------------------- 1 | # Enter the name of your chosen serviceAccount, and enable automounting 2 | apiVersion: apps/v1 3 | kind: Deployment 4 | metadata: 5 | name: patch-serviceAccount 6 | spec: 7 | template: 8 | spec: 9 | serviceAccount: argocd 10 | automountServiceAccountToken: true 11 | -------------------------------------------------------------------------------- /examples/kustomize/argocd/serviceaccount.yaml: -------------------------------------------------------------------------------- 1 | # Choose the name and namespace for your serviceAccount 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: argocd 6 | namespace: argocd 7 | secrets: 8 | - name: argocd-sa-token 9 | --- 10 | apiVersion: v1 11 | kind: Secret 12 | metadata: 13 | namespace: argocd 14 | name: argocd-sa-token 15 | annotations: 16 | kubernetes.io/service-account.name: argocd 17 | type: kubernetes.io/service-account-token 18 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/crumbhole/argocd-vault-replacer 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/crumbhole/bitwardenwrapper v0.0.0-20230218201331-228a231a3fa2 7 | github.com/hashicorp/vault v1.14.1 8 | github.com/hashicorp/vault/api v1.9.2 9 | golang.org/x/crypto v0.23.0 10 | ) 11 | 12 | require ( 13 | cloud.google.com/go/compute v1.19.3 // indirect 14 | cloud.google.com/go/compute/metadata v0.2.3 // indirect 15 | cloud.google.com/go/iam v1.0.1 // indirect 16 | cloud.google.com/go/kms v1.10.2 // indirect 17 | cloud.google.com/go/monitoring v1.13.0 // indirect 18 | github.com/Azure/azure-sdk-for-go v67.2.0+incompatible // indirect 19 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 20 | github.com/Azure/go-autorest/autorest v0.11.29 // indirect 21 | github.com/Azure/go-autorest/autorest/adal v0.9.22 // indirect 22 | github.com/Azure/go-autorest/autorest/azure/auth v0.5.12 // indirect 23 | github.com/Azure/go-autorest/autorest/azure/cli v0.4.5 // indirect 24 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 25 | github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect 26 | github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect 27 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 28 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 29 | github.com/BurntSushi/toml v1.2.1 // indirect 30 | github.com/DataDog/datadog-go v3.2.0+incompatible // indirect 31 | github.com/Jeffail/gabs v1.1.1 // indirect 32 | github.com/Masterminds/goutils v1.1.1 // indirect 33 | github.com/Masterminds/semver v1.5.0 // indirect 34 | github.com/Masterminds/sprig v2.22.0+incompatible // indirect 35 | github.com/NYTimes/gziphandler v1.1.1 // indirect 36 | github.com/ProtonMail/go-crypto v0.0.0-20230626094100-7e9e0395ebec // indirect 37 | github.com/aliyun/alibaba-cloud-sdk-go v1.62.301 // indirect 38 | github.com/armon/go-metrics v0.4.1 // indirect 39 | github.com/armon/go-radix v1.0.0 // indirect 40 | github.com/asaskevich/govalidator v0.0.0-20200907205600-7a23bdc65eef // indirect 41 | github.com/aws/aws-sdk-go v1.44.268 // indirect 42 | github.com/axiomhq/hyperloglog v0.0.0-20220105174342-98591331716a // indirect 43 | github.com/beorn7/perks v1.0.1 // indirect 44 | github.com/bgentry/speakeasy v0.1.0 // indirect 45 | github.com/boombuler/barcode v1.0.1 // indirect 46 | github.com/cenkalti/backoff/v3 v3.2.2 // indirect 47 | github.com/cenkalti/backoff/v4 v4.2.0 // indirect 48 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 49 | github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible // indirect 50 | github.com/circonus-labs/circonusllhist v0.1.3 // indirect 51 | github.com/cloudflare/circl v1.3.7 // indirect 52 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 53 | github.com/denverdino/aliyungo v0.0.0-20190125010748-a747050bb1ba // indirect 54 | github.com/dgryski/go-metro v0.0.0-20180109044635-280f6062b5bc // indirect 55 | github.com/digitalocean/godo v1.7.5 // indirect 56 | github.com/dimchansky/utfbom v1.1.1 // indirect 57 | github.com/dnaeon/go-vcr v1.2.0 // indirect 58 | github.com/duosecurity/duo_api_golang v0.0.0-20190308151101-6c680f768e74 // indirect 59 | github.com/emicklei/go-restful/v3 v3.10.1 // indirect 60 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 61 | github.com/fatih/color v1.16.0 // indirect 62 | github.com/frankban/quicktest v1.14.4 // indirect 63 | github.com/go-jose/go-jose/v3 v3.0.1 // indirect 64 | github.com/go-logr/logr v1.2.3 // indirect 65 | github.com/go-ole/go-ole v1.2.6 // indirect 66 | github.com/go-openapi/analysis v0.20.0 // indirect 67 | github.com/go-openapi/errors v0.20.1 // indirect 68 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 69 | github.com/go-openapi/jsonreference v0.20.1 // indirect 70 | github.com/go-openapi/loads v0.20.2 // indirect 71 | github.com/go-openapi/runtime v0.19.24 // indirect 72 | github.com/go-openapi/spec v0.20.3 // indirect 73 | github.com/go-openapi/strfmt v0.20.0 // indirect 74 | github.com/go-openapi/swag v0.22.3 // indirect 75 | github.com/go-openapi/validate v0.20.2 // indirect 76 | github.com/go-ozzo/ozzo-validation v3.6.0+incompatible // indirect 77 | github.com/go-sql-driver/mysql v1.6.0 // indirect 78 | github.com/go-test/deep v1.1.0 // indirect 79 | github.com/gogo/protobuf v1.3.2 // indirect 80 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 81 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 82 | github.com/golang/protobuf v1.5.3 // indirect 83 | github.com/golang/snappy v0.0.4 // indirect 84 | github.com/google/gnostic v0.5.7-v3refs // indirect 85 | github.com/google/go-cmp v0.5.9 // indirect 86 | github.com/google/go-metrics-stackdriver v0.2.0 // indirect 87 | github.com/google/go-querystring v1.1.0 // indirect 88 | github.com/google/gofuzz v1.2.0 // indirect 89 | github.com/google/s2a-go v0.1.4 // indirect 90 | github.com/google/uuid v1.3.0 // indirect 91 | github.com/googleapis/enterprise-certificate-proxy v0.2.3 // indirect 92 | github.com/googleapis/gax-go/v2 v2.9.1 // indirect 93 | github.com/gophercloud/gophercloud v0.1.0 // indirect 94 | github.com/hashicorp/consul/sdk v0.13.1 // indirect 95 | github.com/hashicorp/errwrap v1.1.0 // indirect 96 | github.com/hashicorp/eventlogger v0.2.1 // indirect 97 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 98 | github.com/hashicorp/go-discover v0.0.0-20210818145131-c573d69da192 // indirect 99 | github.com/hashicorp/go-hclog v1.6.3 // indirect 100 | github.com/hashicorp/go-immutable-radix v1.3.1 // indirect 101 | github.com/hashicorp/go-kms-wrapping/entropy/v2 v2.0.0 // indirect 102 | github.com/hashicorp/go-kms-wrapping/v2 v2.0.9 // indirect 103 | github.com/hashicorp/go-kms-wrapping/wrappers/aead/v2 v2.0.7-1 // indirect 104 | github.com/hashicorp/go-kms-wrapping/wrappers/alicloudkms/v2 v2.0.1 // indirect 105 | github.com/hashicorp/go-kms-wrapping/wrappers/awskms/v2 v2.0.7 // indirect 106 | github.com/hashicorp/go-kms-wrapping/wrappers/azurekeyvault/v2 v2.0.7 // indirect 107 | github.com/hashicorp/go-kms-wrapping/wrappers/gcpckms/v2 v2.0.8 // indirect 108 | github.com/hashicorp/go-kms-wrapping/wrappers/ocikms/v2 v2.0.7 // indirect 109 | github.com/hashicorp/go-kms-wrapping/wrappers/transit/v2 v2.0.7 // indirect 110 | github.com/hashicorp/go-memdb v1.3.3 // indirect 111 | github.com/hashicorp/go-msgpack v1.1.5 // indirect 112 | github.com/hashicorp/go-multierror v1.1.1 // indirect 113 | github.com/hashicorp/go-plugin v1.4.9 // indirect 114 | github.com/hashicorp/go-raftchunking v0.6.3-0.20191002164813-7e9e8525653a // indirect 115 | github.com/hashicorp/go-retryablehttp v0.7.7 // indirect 116 | github.com/hashicorp/go-rootcerts v1.0.2 // indirect 117 | github.com/hashicorp/go-secure-stdlib/awsutil v0.2.3 // indirect 118 | github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect 119 | github.com/hashicorp/go-secure-stdlib/mlock v0.1.3 // indirect 120 | github.com/hashicorp/go-secure-stdlib/parseutil v0.1.7 // indirect 121 | github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 // indirect 122 | github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect 123 | github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 // indirect 124 | github.com/hashicorp/go-sockaddr v1.0.2 // indirect 125 | github.com/hashicorp/go-uuid v1.0.3 // indirect 126 | github.com/hashicorp/go-version v1.6.0 // indirect 127 | github.com/hashicorp/golang-lru v0.5.4 // indirect 128 | github.com/hashicorp/hcl v1.0.1-vault-5 // indirect 129 | github.com/hashicorp/hcp-sdk-go v0.23.0 // indirect 130 | github.com/hashicorp/mdns v1.0.4 // indirect 131 | github.com/hashicorp/raft v1.3.10 // indirect 132 | github.com/hashicorp/raft-autopilot v0.2.0 // indirect 133 | github.com/hashicorp/raft-boltdb/v2 v2.0.0-20210421194847-a7e34179d62c // indirect 134 | github.com/hashicorp/raft-snapshot v1.0.4 // indirect 135 | github.com/hashicorp/vault/sdk v0.9.2-0.20230530190758-08ee474850e0 // indirect 136 | github.com/hashicorp/vic v1.5.1-0.20190403131502-bbfe86ec9443 // indirect 137 | github.com/hashicorp/yamux v0.1.1 // indirect 138 | github.com/huandu/xstrings v1.4.0 // indirect 139 | github.com/imdario/mergo v0.3.15 // indirect 140 | github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect 141 | github.com/jefferai/jsonx v1.0.0 // indirect 142 | github.com/jmespath/go-jmespath v0.4.0 // indirect 143 | github.com/josharian/intern v1.0.0 // indirect 144 | github.com/joyent/triton-go v1.7.1-0.20200416154420-6801d15b779f // indirect 145 | github.com/json-iterator/go v1.1.12 // indirect 146 | github.com/kelseyhightower/envconfig v1.4.0 // indirect 147 | github.com/klauspost/compress v1.16.5 // indirect 148 | github.com/linode/linodego v0.7.1 // indirect 149 | github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect 150 | github.com/mailru/easyjson v0.7.7 // indirect 151 | github.com/mattn/go-colorable v0.1.13 // indirect 152 | github.com/mattn/go-isatty v0.0.20 // indirect 153 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 154 | github.com/miekg/dns v1.1.43 // indirect 155 | github.com/mitchellh/cli v1.1.2 // indirect 156 | github.com/mitchellh/copystructure v1.2.0 // indirect 157 | github.com/mitchellh/go-homedir v1.1.0 // indirect 158 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 159 | github.com/mitchellh/mapstructure v1.5.0 // indirect 160 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 161 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 162 | github.com/modern-go/reflect2 v1.0.2 // indirect 163 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 164 | github.com/nicolai86/scaleway-sdk v1.10.2-0.20180628010248-798f60e20bb2 // indirect 165 | github.com/oklog/run v1.1.0 // indirect 166 | github.com/okta/okta-sdk-golang/v2 v2.12.1 // indirect 167 | github.com/opentracing/opentracing-go v1.2.1-0.20220228012449-10b1cf09e00b // indirect 168 | github.com/oracle/oci-go-sdk/v60 v60.0.0 // indirect 169 | github.com/packethost/packngo v0.1.1-0.20180711074735-b9cb5096f54c // indirect 170 | github.com/patrickmn/go-cache v2.1.0+incompatible // indirect 171 | github.com/petermattis/goid v0.0.0-20180202154549-b0b1615b78e5 // indirect 172 | github.com/pierrec/lz4 v2.6.1+incompatible // indirect 173 | github.com/pires/go-proxyproto v0.6.1 // indirect 174 | github.com/pkg/errors v0.9.1 // indirect 175 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 176 | github.com/posener/complete v1.2.3 // indirect 177 | github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect 178 | github.com/pquerna/otp v1.2.1-0.20191009055518-468c2dd2b58d // indirect 179 | github.com/prometheus/client_golang v1.14.0 // indirect 180 | github.com/prometheus/client_model v0.3.0 // indirect 181 | github.com/prometheus/common v0.37.0 // indirect 182 | github.com/prometheus/procfs v0.8.0 // indirect 183 | github.com/rboyer/safeio v0.2.1 // indirect 184 | github.com/renier/xmlrpc v0.0.0-20170708154548-ce4a1a486c03 // indirect 185 | github.com/ryanuber/go-glob v1.0.0 // indirect 186 | github.com/sasha-s/go-deadlock v0.2.0 // indirect 187 | github.com/sethvargo/go-limiter v0.7.1 // indirect 188 | github.com/shirou/gopsutil/v3 v3.22.6 // indirect 189 | github.com/sirupsen/logrus v1.9.0 // indirect 190 | github.com/softlayer/softlayer-go v0.0.0-20180806151055-260589d94c7d // indirect 191 | github.com/sony/gobreaker v0.4.2-0.20210216022020-dd874f9dd33b // indirect 192 | github.com/spf13/pflag v1.0.5 // indirect 193 | github.com/stretchr/objx v0.5.0 // indirect 194 | github.com/stretchr/testify v1.8.4 // indirect 195 | github.com/tencentcloud/tencentcloud-sdk-go v1.0.162 // indirect 196 | github.com/tklauser/go-sysconf v0.3.10 // indirect 197 | github.com/tklauser/numcpus v0.4.0 // indirect 198 | github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c // indirect 199 | github.com/vmware/govmomi v0.18.0 // indirect 200 | github.com/yusufpapurcu/wmi v1.2.2 // indirect 201 | go.etcd.io/bbolt v1.3.7 // indirect 202 | go.mongodb.org/mongo-driver v1.11.6 // indirect 203 | go.opencensus.io v0.24.0 // indirect 204 | go.uber.org/atomic v1.11.0 // indirect 205 | golang.org/x/net v0.23.0 // indirect 206 | golang.org/x/oauth2 v0.8.0 // indirect 207 | golang.org/x/sync v0.2.0 // indirect 208 | golang.org/x/sys v0.20.0 // indirect 209 | golang.org/x/term v0.20.0 // indirect 210 | golang.org/x/text v0.15.0 // indirect 211 | golang.org/x/time v0.3.0 // indirect 212 | google.golang.org/api v0.124.0 // indirect 213 | google.golang.org/appengine v1.6.7 // indirect 214 | google.golang.org/genproto v0.0.0-20230525154841-bd750badd5c6 // indirect 215 | google.golang.org/grpc v1.56.3 // indirect 216 | google.golang.org/protobuf v1.30.0 // indirect 217 | gopkg.in/inf.v0 v0.9.1 // indirect 218 | gopkg.in/ini.v1 v1.66.2 // indirect 219 | gopkg.in/resty.v1 v1.12.0 // indirect 220 | gopkg.in/square/go-jose.v2 v2.6.0 // indirect 221 | gopkg.in/yaml.v2 v2.4.0 // indirect 222 | gopkg.in/yaml.v3 v3.0.1 // indirect 223 | k8s.io/api v0.27.2 // indirect 224 | k8s.io/apimachinery v0.27.2 // indirect 225 | k8s.io/client-go v0.27.2 // indirect 226 | k8s.io/klog/v2 v2.90.1 // indirect 227 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect 228 | k8s.io/utils v0.0.0-20230220204549-a5ecb0141aa5 // indirect 229 | nhooyr.io/websocket v1.8.7 // indirect 230 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 231 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 232 | sigs.k8s.io/yaml v1.3.0 // indirect 233 | ) 234 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "github.com/crumbhole/argocd-vault-replacer/src/bwvaluesource" 7 | "github.com/crumbhole/argocd-vault-replacer/src/substitution" 8 | "github.com/crumbhole/argocd-vault-replacer/src/vaultvaluesource" 9 | "io/ioutil" 10 | "log" 11 | "os" 12 | "path/filepath" 13 | "regexp" 14 | ) 15 | 16 | type scanner struct { 17 | source substitution.ValueSource 18 | } 19 | 20 | func (s *scanner) process(input []byte) error { 21 | subst := substitution.Substitutor{Source: s.source} 22 | modifiedcontents, err := subst.Substitute(input) 23 | if err != nil { 24 | return err 25 | } 26 | fmt.Printf("---\n%s\n", modifiedcontents) 27 | return nil 28 | } 29 | 30 | func (s *scanner) scanFile(path string, info os.FileInfo, err error) error { 31 | if err != nil { 32 | return err 33 | } 34 | if info.IsDir() { 35 | return nil 36 | } 37 | fileRegexp := regexp.MustCompile(`\.ya?ml$`) 38 | if fileRegexp.MatchString(path) { 39 | filecontents, err := ioutil.ReadFile(path) 40 | if err != nil { 41 | return err 42 | } 43 | err = s.process(filecontents) 44 | if err != nil { 45 | return err 46 | } 47 | } 48 | return nil 49 | } 50 | 51 | func (s *scanner) scanDir(path string) error { 52 | return filepath.Walk(path, s.scanFile) 53 | } 54 | 55 | func selectValueSource() substitution.ValueSource { 56 | // This would be better with a factory pattern 57 | if bwvaluesource.BwSession() { 58 | return bwvaluesource.BitwardenValueSource{} 59 | } 60 | return vaultvaluesource.VaultValueSource{} 61 | } 62 | 63 | func copyEnv() { 64 | for _, envEntry := range []string{`VAULT_ADDR`, `VAULT_TOKEN`} { 65 | val, got := os.LookupEnv(`ARGOCD_ENV_` + envEntry) 66 | if got { 67 | os.Setenv(envEntry, val) 68 | } 69 | } 70 | } 71 | 72 | func main() { 73 | copyEnv() 74 | stat, _ := os.Stdin.Stat() 75 | s := scanner{source: selectValueSource()} 76 | if (stat.Mode() & os.ModeCharDevice) == 0 { 77 | reader := bufio.NewReader(os.Stdin) 78 | filecontents, err := ioutil.ReadAll(reader) 79 | if err != nil { 80 | log.Fatal(err) 81 | } 82 | err = s.process(filecontents) 83 | if err != nil { 84 | log.Fatal(err) 85 | } 86 | } else { 87 | dir, err := os.Getwd() 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | err = s.scanDir(dir) 92 | if err != nil { 93 | log.Fatal(err) 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /main_test.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/crumbhole/argocd-vault-replacer/src/vaultvaluesource" 7 | "github.com/hashicorp/vault/api" 8 | "github.com/hashicorp/vault/http" 9 | "github.com/hashicorp/vault/vault" 10 | "io" 11 | "io/ioutil" 12 | "net" 13 | "os" 14 | "testing" 15 | ) 16 | 17 | const ( 18 | testsPath = "test/" 19 | ) 20 | 21 | func createTestVault(t *testing.T) (net.Listener, *api.Client) { 22 | t.Helper() 23 | 24 | // Create an in-memory, unsealed core (the "backend", if you will). 25 | core, keyShares, rootToken := vault.TestCoreUnsealed(t) 26 | _ = keyShares 27 | 28 | // Start an HTTP server for the core. 29 | ln, addr := http.TestServer(t, core) 30 | 31 | // Create a client that talks to the server, initially authenticating with 32 | // the root token. 33 | conf := api.DefaultConfig() 34 | conf.Address = addr 35 | 36 | client, err := api.NewClient(conf) 37 | if err != nil { 38 | t.Fatal(err) 39 | } 40 | client.SetToken(rootToken) 41 | 42 | // Setup required secrets, policies, etc. 43 | _, err = client.Logical().Write("secret/data/path", map[string]interface{}{ 44 | "data": map[string]interface{}{ 45 | "foo": "hi", 46 | "bar": "example", 47 | }, 48 | }) 49 | if err != nil { 50 | t.Fatal(err) 51 | } 52 | 53 | return ln, client 54 | } 55 | 56 | func checkDir(t *testing.T, s scanner, path string) error { 57 | oldstdout := os.Stdout 58 | r, w, _ := os.Pipe() 59 | os.Stdout = w 60 | outC := make(chan string) 61 | 62 | err := s.scanDir(path) 63 | 64 | go func() { 65 | var buf bytes.Buffer 66 | io.Copy(&buf, r) 67 | outC <- buf.String() 68 | }() 69 | w.Close() 70 | os.Stdout = oldstdout 71 | if err != nil { 72 | return err 73 | } 74 | out := <-outC 75 | 76 | expected, err := ioutil.ReadFile(path + "/expected.txt") 77 | if err != nil { 78 | return err 79 | } 80 | if out != string(expected) { 81 | return fmt.Errorf("Expected %s and got %s", expected, out) 82 | } 83 | return nil 84 | } 85 | 86 | // Finds directories under ./test and substitutes all the .yaml/.ymls 87 | // against the above vault, expecting to see expected.txt as the output 88 | func TestDirectories(t *testing.T) { 89 | ln, client := createTestVault(t) 90 | defer ln.Close() 91 | 92 | dirs, err := ioutil.ReadDir(testsPath) 93 | if err != nil { 94 | t.Error(err) 95 | } 96 | s := scanner{source: vaultvaluesource.VaultValueSource{Client: client}} 97 | 98 | for _, d := range dirs { 99 | t.Run(d.Name(), func(t *testing.T) { 100 | t.Logf("Testing dir %s", testsPath+d.Name()) 101 | err := checkDir(t, s, testsPath+d.Name()) 102 | if err != nil { 103 | t.Error(err) 104 | } 105 | }) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "extends": [ 4 | "config:base" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /src/bwvaluesource/bwValueSource.go: -------------------------------------------------------------------------------- 1 | package bwvaluesource 2 | 3 | import ( 4 | "errors" 5 | "os" 6 | "strings" 7 | 8 | bwwrap "github.com/crumbhole/bitwardenwrapper" 9 | ) 10 | 11 | const ( 12 | envCheck = "BW_SESSION" 13 | argoPrefix = "ARGOCD_ENV_" 14 | ) 15 | 16 | // BwSession returns true of BW_SESSION or ARGOCD_ENV_BW_SESSION are set 17 | // If ARGOCD_ENV_BWSESSION is set the value is copied to BW_SESSION 18 | func BwSession() bool { 19 | val, got := os.LookupEnv(argoPrefix + envCheck) 20 | if !got { 21 | val, got = os.LookupEnv(envCheck) 22 | if !got { 23 | return false 24 | } 25 | return true 26 | } 27 | os.Setenv(envCheck, val) 28 | return true 29 | } 30 | 31 | // BitwardenValueSource is a value source getting values from bitwarden 32 | type BitwardenValueSource struct{} 33 | 34 | func (BitwardenValueSource) getItemSplitPath(path string) (*bwwrap.BwItem, error) { 35 | pathParts := strings.Split(string(path), `/`) 36 | keyUsed := pathParts[len(pathParts)-1] 37 | pathUsed := strings.Join(pathParts[:len(pathParts)-1], `/`) 38 | return bwwrap.GetItemFromFolder(keyUsed, pathUsed) 39 | } 40 | 41 | // GetValue returns a value from a path+key in bitwarden or null if it doesn't exist 42 | func (m BitwardenValueSource) GetValue(path []byte, key []byte) (*[]byte, error) { 43 | if !BwSession() { 44 | return nil, errors.New("Bitwarden session key not present") 45 | } 46 | switch string(key) { 47 | default: 48 | item, err := bwwrap.GetItemFromFolder(string(key), string(path)) 49 | if err != nil { 50 | return nil, err 51 | } 52 | value := []byte(item.Notes) 53 | return &value, nil 54 | case `username`: 55 | item, err := m.getItemSplitPath(string(path)) 56 | if err != nil { 57 | return nil, err 58 | } 59 | value := []byte(item.Login.Username) 60 | return &value, nil 61 | case `password`: 62 | item, err := m.getItemSplitPath(string(path)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | value := []byte(item.Login.Password) 67 | return &value, nil 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/modifier/base64.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/base64" 5 | ) 6 | 7 | type base64Modifier struct{} 8 | 9 | func (base64Modifier) modify(input []byte) ([]byte, error) { 10 | return []byte(base64.StdEncoding.EncodeToString(input)), nil 11 | } 12 | -------------------------------------------------------------------------------- /src/modifier/htaccess.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "golang.org/x/crypto/bcrypt" 8 | ) 9 | 10 | type htaccessModifier struct{} 11 | 12 | func (htaccessModifier) modify(inputJSON []byte) ([]byte, error) { 13 | input := make([]map[string]string, 0) 14 | err := json.Unmarshal(inputJSON, &input) 15 | if err != nil { 16 | inputsingle := make(map[string]string, 0) 17 | err = json.Unmarshal(inputJSON, &inputsingle) 18 | if err != nil { 19 | return inputJSON, err 20 | } 21 | input = append(input, inputsingle) 22 | } 23 | passwords := make(map[string]string, len(input)) 24 | for _, kv := range input { 25 | user, ok := kv[`user`] 26 | if !ok { 27 | return nil, errors.New(`No key called user in input json`) 28 | } 29 | password, ok := kv[`password`] 30 | if !ok { 31 | return nil, errors.New(`No key called password in input json`) 32 | } 33 | hashedpw, err := htaccessBcrypt(password) 34 | if err != nil { 35 | return nil, err 36 | } 37 | passwords[user] = string(hashedpw) 38 | } 39 | return htaccessEncode(passwords), nil 40 | } 41 | 42 | func htaccessEncode(in map[string]string) (out []byte) { 43 | out = []byte{} 44 | for name, pw := range in { 45 | out = append(out, []byte(fmt.Sprintf("%s:%s\n", name, pw))...) 46 | } 47 | return out 48 | } 49 | 50 | func htaccessBcrypt(password string) (hash []byte, err error) { 51 | hash, err = bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) 52 | if err != nil { 53 | return nil, err 54 | } 55 | return hash, nil 56 | } 57 | -------------------------------------------------------------------------------- /src/modifier/jsonKeyedObject.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type jsonKeyedObjectModifier struct { 8 | } 9 | 10 | func (mod jsonKeyedObjectModifier) modify(input []byte) ([]byte, error) { 11 | list, err := textToKvlist(input) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return mod.modifyKvlist(list) 16 | } 17 | 18 | func (jsonKeyedObjectModifier) modifyKvlist(input Kvlist) ([]byte, error) { 19 | keyArray := make(map[string]string, len(input)) 20 | for _, kv := range input { 21 | keyArray[string(kv.Key)] = string(kv.Value) 22 | } 23 | return json.Marshal(keyArray) 24 | } 25 | -------------------------------------------------------------------------------- /src/modifier/jsonKeyedObject_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestJsonKeyedObject(t *testing.T) { 9 | // Fragile test - relies on output order of json.Marshal 10 | tests := map[string]Kvlist{ 11 | `{"key1":"val1","key2":"val2"}`: { 12 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 13 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 14 | }, 15 | `{"key1":"val1","key2":"val2","oink":"foo","sausage":"bar"}`: { 16 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 17 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 18 | {Key: []byte(`oink`), Value: []byte(`foo`)}, 19 | {Key: []byte(`sausage`), Value: []byte(`bar`)}, 20 | }, 21 | } 22 | modifier := jsonKeyedObjectModifier{} 23 | for expect, input := range tests { 24 | res, err := modifier.modifyKvlist(input) 25 | if err != nil { 26 | t.Errorf("%v !-> %v, got an error %s", input, expect, err) 27 | } 28 | if !bytes.Equal(res, []byte(expect)) { 29 | t.Errorf("%v !-> %v, got %s", input, expect, res) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modifier/jsonList.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | ) 6 | 7 | type jsonListModifier struct { 8 | } 9 | 10 | func (mod jsonListModifier) modify(input []byte) ([]byte, error) { 11 | list, err := textToKvlist(input) 12 | if err != nil { 13 | return nil, err 14 | } 15 | return mod.modifyKvlist(list) 16 | } 17 | 18 | func (jsonListModifier) modifyKvlist(input Kvlist) ([]byte, error) { 19 | keyArray := make([]string, 0) 20 | for _, value := range input { 21 | keyArray = append(keyArray, string(value.Value)) 22 | } 23 | return json.Marshal(keyArray) 24 | } 25 | -------------------------------------------------------------------------------- /src/modifier/jsonList_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestJsonList(t *testing.T) { 9 | tests := map[string]Kvlist{ 10 | `["val1","val2"]`: { 11 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 12 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 13 | }, 14 | } 15 | modifier := jsonListModifier{} 16 | for expect, input := range tests { 17 | res, err := modifier.modifyKvlist(input) 18 | if err != nil { 19 | t.Errorf("%v !-> %v, got an error %s", input, expect, err) 20 | } 21 | if !bytes.Equal(res, []byte(expect)) { 22 | t.Errorf("%v !-> %v, got %s", input, expect, res) 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/modifier/jsonObjectToList.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | "regexp" 6 | ) 7 | 8 | // This is special as it takes parameters 9 | 10 | func jsonObjectToListModifierGet(call string) kvmodifier { 11 | // TODO: Some helpful to the user error handling here when parsing fails 12 | reParams := regexp.MustCompile(`jsonobject2list\(\s*([^\,\))]+?)\s*\,\s*([^\,\))]+?)\s*\)`) 13 | paramsFound := reParams.FindStringSubmatch(call) 14 | if paramsFound == nil { 15 | return nil 16 | } 17 | modifier := jsonObjectToListModifier{ 18 | keyname: string(paramsFound[1]), 19 | valuename: string(paramsFound[2]), 20 | } 21 | return modifier 22 | } 23 | 24 | type jsonObjectToListModifier struct { 25 | keyname string 26 | valuename string 27 | } 28 | 29 | func (mod jsonObjectToListModifier) modify(input []byte) ([]byte, error) { 30 | list, err := textToKvlist(input) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return mod.modifyKvlist(list) 35 | } 36 | 37 | func (mod jsonObjectToListModifier) modifyKvlist(input Kvlist) ([]byte, error) { 38 | list := make([]map[string]string, 0) 39 | for _, kv := range input { 40 | newObj := make(map[string]string) 41 | newObj[mod.keyname] = string(kv.Key) 42 | newObj[mod.valuename] = string(kv.Value) 43 | list = append(list, newObj) 44 | } 45 | return json.Marshal(list) 46 | } 47 | -------------------------------------------------------------------------------- /src/modifier/jsonObjectToList_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestJsonObjectToList(t *testing.T) { 9 | // Fragile test - relies on output order of json.Marshal 10 | tests := map[string]Kvlist{ 11 | `[{"keyname":"key1","valuename":"val1"},{"keyname":"key2","valuename":"val2"}]`: { 12 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 13 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 14 | }, 15 | `[{"keyname":"key1","valuename":"val1"},{"keyname":"key2","valuename":"val2"},{"keyname":"oink","valuename":"foo"},{"keyname":"sausage","valuename":"bar"}]`: { 16 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 17 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 18 | {Key: []byte(`oink`), Value: []byte(`foo`)}, 19 | {Key: []byte(`sausage`), Value: []byte(`bar`)}, 20 | }, 21 | } 22 | modifier := jsonObjectToListModifierGet(`jsonobject2list(keyname,valuename)`) 23 | for expect, input := range tests { 24 | res, err := modifier.modifyKvlist(input) 25 | if err != nil { 26 | t.Errorf("%v !-> %v, got an error %s", input, expect, err) 27 | } 28 | if !bytes.Equal(res, []byte(expect)) { 29 | t.Errorf("%v !-> %v, got %s", input, expect, res) 30 | } 31 | } 32 | modifier = jsonObjectToListModifierGet(`jsonobject2list( keyname , valuename )`) 33 | for expect, input := range tests { 34 | res, err := modifier.modifyKvlist(input) 35 | if err != nil { 36 | t.Errorf("%v !-> %v, got an error %s", input, expect, err) 37 | } 38 | if !bytes.Equal(res, []byte(expect)) { 39 | t.Errorf("%v !-> %v, got %s", input, expect, res) 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/modifier/jsonPairedObject.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | ) 7 | 8 | type jsonPairedObjectModifier struct { 9 | } 10 | 11 | func (mod jsonPairedObjectModifier) modify(input []byte) ([]byte, error) { 12 | list, err := textToKvlist(input) 13 | if err != nil { 14 | return nil, err 15 | } 16 | return mod.modifyKvlist(list) 17 | } 18 | 19 | func (jsonPairedObjectModifier) modifyKvlist(input Kvlist) ([]byte, error) { 20 | if len(input)%2 != 0 { 21 | return nil, errors.New(`Paired object needs an even number of inputs`) 22 | } 23 | keyArray := make(map[string]string, len(input)) 24 | for index := 0; index < len(input); index += 2 { 25 | keyArray[string(input[index].Value)] = string(input[index+1].Value) 26 | } 27 | return json.Marshal(keyArray) 28 | } 29 | -------------------------------------------------------------------------------- /src/modifier/jsonPairedObject_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestJsonPairedObject(t *testing.T) { 9 | // Fragile test - relies on output order of json.Marshal 10 | tests := map[string]Kvlist{ 11 | `{"val1":"val2"}`: { 12 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 13 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 14 | }, 15 | `{"foo":"bar","val1":"val2"}`: { 16 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 17 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 18 | {Key: []byte(`oink`), Value: []byte(`foo`)}, 19 | {Key: []byte(`sausage`), Value: []byte(`bar`)}, 20 | }, 21 | } 22 | modifier := jsonPairedObjectModifier{} 23 | for expect, input := range tests { 24 | res, err := modifier.modifyKvlist(input) 25 | if err != nil { 26 | t.Errorf("%v !-> %v, got an error %s", input, expect, err) 27 | } 28 | if !bytes.Equal(res, []byte(expect)) { 29 | t.Errorf("%v !-> %v, got %s", input, expect, res) 30 | } 31 | } 32 | } 33 | 34 | func TestJsonPairedObjectFail(t *testing.T) { 35 | tests := []Kvlist{ 36 | { 37 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 38 | {Key: []byte(`key2`), Value: []byte(`val2`)}, 39 | {Key: []byte(`key3`), Value: []byte(`val3`)}, 40 | }, 41 | { 42 | {Key: []byte(`key1`), Value: []byte(`val1`)}, 43 | }, 44 | } 45 | modifier := jsonPairedObjectModifier{} 46 | for _, input := range tests { 47 | _, err := modifier.modifyKvlist(input) 48 | expectedError := `Paired object needs an even number of inputs` 49 | if err == nil || err.Error() != expectedError { 50 | t.Errorf("Expecting %s, got %s", expectedError, err) 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/modifier/modifier.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | // Kv is a Key Value Pair 4 | type Kv struct { 5 | Key []byte 6 | Value []byte 7 | } 8 | 9 | // Kvlist is a list of key values 10 | type Kvlist []Kv 11 | 12 | type modifier interface { 13 | modify([]byte) ([]byte, error) 14 | } 15 | 16 | type kvmodifier interface { 17 | modifier 18 | modifyKvlist(Kvlist) ([]byte, error) 19 | } 20 | -------------------------------------------------------------------------------- /src/modifier/modify.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sort" 7 | ) 8 | 9 | func textToKvlist(input []byte) (Kvlist, error) { 10 | list := Kvlist{} 11 | err := json.Unmarshal(input, &list) 12 | if err != nil { 13 | flat := make(map[string]string, 0) 14 | err = json.Unmarshal(input, &flat) 15 | if err != nil { 16 | return nil, err 17 | } 18 | for key, val := range flat { 19 | list = append(list, Kv{Key: []byte(key), Value: []byte(val)}) 20 | } 21 | } 22 | // We care that the ordering is stable only 23 | sort.Slice(list, func(i, j int) bool { return string(list[i].Key) < string(list[j].Key) }) 24 | return list, nil 25 | } 26 | 27 | func getModifier(name string) (modifier, error) { 28 | obj2list := jsonObjectToListModifierGet(name) 29 | if obj2list != nil { 30 | return obj2list, nil 31 | } 32 | 33 | modifiers := map[string]modifier{ 34 | "base64": base64Modifier{}, 35 | "json2htaccess": htaccessModifier{}, 36 | "jsonlist": jsonListModifier{}, 37 | "jsonkeyedobject": jsonKeyedObjectModifier{}, 38 | "jsonpairedobject": jsonPairedObjectModifier{}, 39 | } 40 | if found, ok := modifiers[name]; ok { 41 | return found, nil 42 | } 43 | return nil, fmt.Errorf("Invalid modifier %s", name) 44 | } 45 | 46 | // Modify takes some input and the name of a modifier to modify that string 47 | // with and returns the changed input. 48 | func Modify(input []byte, name string) ([]byte, error) { 49 | modifier, err := getModifier(name) 50 | if err != nil { 51 | return nil, err 52 | } 53 | return modifier.modify(input) 54 | } 55 | 56 | func getKVModifier(name string) (kvmodifier, error) { 57 | obj2list := jsonObjectToListModifierGet(name) 58 | if obj2list != nil { 59 | return obj2list, nil 60 | } 61 | 62 | modifiers := map[string]kvmodifier{ 63 | "valuestext": valuesTextModifier{}, 64 | "jsonlist": jsonListModifier{}, 65 | "jsonkeyedobject": jsonKeyedObjectModifier{}, 66 | "jsonpairedobject": jsonPairedObjectModifier{}, 67 | } 68 | if found, ok := modifiers[name]; ok { 69 | return found, nil 70 | } 71 | return nil, fmt.Errorf("Invalid modifier %s", name) 72 | } 73 | 74 | // ModifyKVList takes some key+values and the name of a modifier to modify that list 75 | // with and returns the changed input. 76 | func ModifyKVList(input Kvlist, name string) ([]byte, error) { 77 | modifier, err := getKVModifier(name) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return modifier.modifyKvlist(input) 82 | } 83 | -------------------------------------------------------------------------------- /src/modifier/modify_test.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | func TestNoSuchModifier(t *testing.T) { 9 | _, err := Modify([]byte(`hi`), "nonsense") 10 | expectedError := `Invalid modifier nonsense` 11 | if err.Error() != expectedError { 12 | t.Errorf("Expecting %s, got %s", expectedError, err) 13 | } 14 | } 15 | 16 | func TestBase64(t *testing.T) { 17 | tests := map[string]string{ 18 | `Hello`: `SGVsbG8=`, 19 | `Supersecret 20 | thing`: `U3VwZXJzZWNyZXQKdGhpbmc=`, 21 | } 22 | for input, expect := range tests { 23 | in := []byte(input) 24 | res, err := Modify(in, "base64") 25 | if err != nil { 26 | t.Error(err) 27 | } 28 | if !bytes.Equal(res, []byte(expect)) { 29 | t.Errorf("%s !-> %v, got %s", in, expect, res) 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/modifier/valuesText.go: -------------------------------------------------------------------------------- 1 | package modifier 2 | 3 | type valuesTextModifier struct { 4 | } 5 | 6 | func (mod valuesTextModifier) modify(input []byte) ([]byte, error) { 7 | list, err := textToKvlist(input) 8 | if err != nil { 9 | return nil, err 10 | } 11 | return mod.modifyKvlist(list) 12 | } 13 | 14 | func (valuesTextModifier) modifyKvlist(input Kvlist) ([]byte, error) { 15 | out := []byte{} 16 | for _, value := range input { 17 | out = append(out, value.Value...) 18 | } 19 | return out, nil 20 | } 21 | -------------------------------------------------------------------------------- /src/substitution/mockValueSource.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type pathKeyTuple struct { 9 | path string 10 | key string 11 | } 12 | 13 | type mockValueSource struct { 14 | values map[pathKeyTuple][]byte 15 | } 16 | 17 | func (m mockValueSource) GetValue(path []byte, key []byte) (*[]byte, error) { 18 | var pk = pathKeyTuple{strings.TrimSuffix(string(path), `/`), string(key)} 19 | if val, ok := m.values[pk]; ok { 20 | return &val, nil 21 | } 22 | return nil, fmt.Errorf("Couldn't find %s ! %s", path, key) 23 | } 24 | -------------------------------------------------------------------------------- /src/substitution/mockValueSource_test.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var mockVs = mockValueSource{values: map[pathKeyTuple][]byte{ 9 | {`/path/to/thing`, `key`}: []byte(`value`), 10 | }, 11 | } 12 | 13 | func TestMockSource(t *testing.T) { 14 | val, err := mockVs.GetValue([]byte(`/path/to/thing`), []byte(`key`)) 15 | if err != nil { 16 | t.Errorf("Unexpected error %s", err) 17 | } 18 | if !bytes.Equal(*val, []byte(`value`)) { 19 | t.Errorf("/path/to/thing,key !-> value, got %s", val) 20 | } 21 | } 22 | 23 | func TestMockSourceFail(t *testing.T) { 24 | val, err := mockVs.GetValue([]byte(`pa`), []byte(`key`)) 25 | expectedError := `Couldn't find pa ! key` 26 | if err != nil && err.Error() != expectedError { 27 | t.Errorf("Expecting %s, got %s", expectedError, err) 28 | } 29 | if val != nil { 30 | t.Errorf("pa,key !-> nil, got %s", val) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/substitution/substitute.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "bytes" 5 | "encoding/base64" 6 | "regexp" 7 | ) 8 | 9 | // Substitutor is acting like a class to hold the information to perform substitution on some data 10 | // and collect the errors during that substitution. 11 | type Substitutor struct { 12 | Source ValueSource 13 | errs error 14 | } 15 | 16 | // substitutebase64 takes a whole multi-line []byte and substitutes with base64 decode 17 | func (s *Substitutor) substitutebase64(input []byte) []byte { 18 | decoded, err := base64.StdEncoding.DecodeString(string(input)) 19 | // We don't know this is b64, so failure to decode is fine 20 | if err != nil { 21 | return input 22 | } 23 | // Recurse with the decoded version 24 | out, _ := s.substituteraw(decoded) 25 | 26 | // Fast exit if subsititution has done nothing 27 | // Works around a bug where we have something does not b64 encode->decode 28 | // without without changing, for things that don't need substution 29 | if bytes.Equal(out, decoded) { 30 | return input 31 | } 32 | return []byte(base64.StdEncoding.EncodeToString(out)) 33 | } 34 | 35 | // substituteraw takes a whole multi-line []byte and substitutes without base64 decode 36 | func (s *Substitutor) substituteraw(input []byte) ([]byte, error) { 37 | reValue := regexp.MustCompile(`<[ \t]*(secret|vault):[^\r\n]+?>`) 38 | return reValue.ReplaceAllFunc(input, s.substituteValue), s.errs 39 | } 40 | 41 | // Substitute takes a whole multi-line []byte and finds appropriate subsitutions 42 | func (s *Substitutor) Substitute(input []byte) ([]byte, error) { 43 | // First attempt to base64 decode any secrets encoded by other 44 | // tools, such as helm 45 | reB64Value := regexp.MustCompile(`[A-Za-z0-9\+\/\=]{10,}`) 46 | postbase64input := reB64Value.ReplaceAllFunc(input, s.substitutebase64) 47 | 48 | return s.substituteraw(postbase64input) 49 | } 50 | -------------------------------------------------------------------------------- /src/substitution/substituteValue.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "github.com/crumbhole/argocd-vault-replacer/src/modifier" 7 | "net/url" 8 | "regexp" 9 | ) 10 | 11 | func unescape(input []byte) ([]byte, error) { 12 | result, err := url.QueryUnescape(string(input)) 13 | if err != nil { 14 | return input, err 15 | } 16 | return []byte(result), nil 17 | } 18 | 19 | // Takes the 'dirty' key from the regex and cleans it to the actual key 20 | func getKeys(input []byte) ([]string, error) { 21 | reKey := regexp.MustCompile(`^~\s*(.*?)\s*$`) 22 | reSplit := regexp.MustCompile(`\s*\~\s*`) 23 | keysFound := reKey.FindSubmatch(input) 24 | if keysFound == nil { 25 | return nil, errors.New("Key regex failure") 26 | } 27 | allKeys, err := unescape(keysFound[1]) 28 | if err != nil { 29 | return nil, err 30 | } 31 | return reSplit.Split(string(allKeys), -1), nil 32 | } 33 | 34 | // Takes the 'dirty' modifiers from the regex and turns them into a list 35 | func getModifiers(modifiers []byte) ([]string, error) { 36 | reMod := regexp.MustCompile(`^\|\s*(.*?)\s*$`) 37 | reSplit := regexp.MustCompile(`\s*\|\s*`) 38 | modsFound := reMod.FindSubmatch(modifiers) 39 | if modsFound == nil { 40 | return nil, errors.New("Mods regex failure") 41 | } 42 | return reSplit.Split(string(modsFound[1]), -1), nil 43 | } 44 | 45 | func performModifiers(modifiers []string, input modifier.Kvlist) ([]byte, error) { 46 | var err error 47 | 48 | // First modifier must transform Kvlist->[]byte 49 | value, err := modifier.ModifyKVList(input, modifiers[0]) 50 | if err != nil { 51 | value, err = modifier.ModifyKVList(input, `valuestext`) 52 | if err != nil { 53 | return nil, err 54 | } 55 | } else { 56 | // Take off the first modifier 57 | modifiers = modifiers[1:] 58 | } 59 | 60 | for _, mod := range modifiers { 61 | value, err = modifier.Modify(value, mod) 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | return value, nil 67 | } 68 | 69 | // Swaps a for the value from the valuesource 70 | // input should contain no lf/cf 71 | func (s *Substitutor) substituteValueWithError(input []byte) ([]byte, error) { 72 | reOuter := regexp.MustCompile(`^<\s*(secret|vault):\s*([^\~]*[^\s])\s*(\~\s*[^\|]+)?\s*(\|.*)?\s*>$`) 73 | pathFound := reOuter.FindSubmatch(input) 74 | if pathFound != nil { 75 | if len(pathFound[3]) > 0 { 76 | path, err := unescape(pathFound[2]) 77 | if err != nil { 78 | return nil, err 79 | } 80 | keys, err := getKeys(pathFound[3]) 81 | if err != nil { 82 | return nil, err 83 | } 84 | var kvs modifier.Kvlist 85 | for _, key := range keys { 86 | value, err := s.Source.GetValue(path, []byte(key)) 87 | if err != nil { 88 | return nil, err 89 | } 90 | kvs = append(kvs, modifier.Kv{Key: []byte(key), Value: *value}) 91 | } 92 | modifiers := pathFound[4] 93 | if err != nil { 94 | return nil, err 95 | } 96 | if len(modifiers) == 0 { 97 | modifiers = []byte(`|valuestext`) 98 | } 99 | modList, err := getModifiers(modifiers) 100 | if err != nil { 101 | return nil, err 102 | } 103 | return performModifiers(modList, kvs) 104 | } 105 | return nil, errors.New(`Failed to find path for substitution`) 106 | } 107 | // We pass through things we can't match at all. They shouldn't arrive here. 108 | return input, nil 109 | } 110 | 111 | // Swaps a for the value from the valuesource 112 | // input should contain no lf/cf 113 | func (s *Substitutor) substituteValue(input []byte) []byte { 114 | res, err := s.substituteValueWithError(input) 115 | if err != nil { 116 | longerr := fmt.Errorf("Processing %s failed: %s", string(input), err) 117 | if s.errs == nil { 118 | s.errs = longerr 119 | } else { 120 | s.errs = fmt.Errorf("%s\n%s", s.errs, longerr) 121 | } 122 | } 123 | return res 124 | } 125 | -------------------------------------------------------------------------------- /src/substitution/substituteValue_test.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var substvalVs = mockValueSource{values: map[pathKeyTuple][]byte{ 9 | {`/path/to/thing`, `key`}: []byte(`value`), 10 | {`/path/to/thing`, `foo`}: []byte(`bar`), 11 | {`/path/to/other`, `nose`}: []byte(`out`), 12 | {`/path/to/emoji`, `smile`}: []byte(`😀`), 13 | {`/path/to/😀`, `face`}: []byte(`laugh`), 14 | {`/path/ /other`, `pear`}: []byte(`apple`), 15 | {`/path/ /other`, `ora nge`}: []byte(`satsu ma`), 16 | {`/spacepath/ `, `nice`}: []byte(`time`), 17 | {`/path/to/thing`, ` leadingspace`}: []byte(`yay`), 18 | {`/path/>/<`, `<><>`}: []byte(`pointy`), 19 | }, 20 | } 21 | var subst = Substitutor{Source: substvalVs} 22 | 23 | func TestBasicFail(t *testing.T) { 24 | key := []byte(`blah`) 25 | res, err := subst.substituteValueWithError(key) 26 | if !bytes.Equal(res, key) { 27 | t.Errorf("blah !-> blah, got %s", res) 28 | } 29 | expectedError := `Failed to find path for substitution` 30 | if err != nil && err.Error() != expectedError { 31 | t.Errorf("Expecting %s, got %s", expectedError, err) 32 | } 33 | } 34 | 35 | // func TestBasicSuccess(t *testing.T) { 36 | // key := []byte(``) 37 | // res := subst.substituteValue(key) 38 | // if !bytes.Equal(res, []byte(`/path/to/thing`)) { 39 | // t.Errorf(" !-> /path/to/thing, got %s", res) 40 | // } 41 | // } 42 | 43 | func TestManyGood(t *testing.T) { 44 | tests := map[string]string{ 45 | ``: `value`, 46 | ``: `value`, 47 | ``: `bar`, 48 | `< secret:/path/to/thing~key>`: `value`, 49 | ``: `value`, 50 | ``: `value`, 51 | ``: `value`, 52 | ``: `value`, 53 | `< secret: /path/to/thing ~ key >`: `value`, 54 | `< secret: /path/to/thing ~ key >`: `value`, 55 | ``: `out`, 56 | ``: `laugh`, 57 | ``: `😀`, 58 | ``: `apple`, 59 | ``: `apple`, 60 | ``: `satsu ma`, 61 | ``: `satsu ma`, 62 | ``: `time`, 63 | ``: `yay`, 64 | ``: `pointy`, 65 | ``: `dmFsdWU=`, 66 | ``: `dmFsdWU=`, 67 | ``: `ZG1Gc2RXVT0=`, 68 | 69 | ``: `value`, 70 | ``: `value`, 71 | ``: `bar`, 72 | `< vault:/path/to/thing~key>`: `value`, 73 | ``: `value`, 74 | ``: `value`, 75 | ``: `value`, 76 | ``: `value`, 77 | `< vault: /path/to/thing ~ key >`: `value`, 78 | `< vault: /path/to/thing ~ key >`: `value`, 79 | ``: `out`, 80 | ``: `laugh`, 81 | ``: `😀`, 82 | ``: `apple`, 83 | ``: `apple`, 84 | ``: `satsu ma`, 85 | ``: `satsu ma`, 86 | ``: `time`, 87 | ``: `yay`, 88 | ``: `pointy`, 89 | ``: `dmFsdWU=`, 90 | ``: `dmFsdWU=`, 91 | ``: `ZG1Gc2RXVT0=`, 92 | } 93 | for input, expect := range tests { 94 | in := []byte(input) 95 | res, err := subst.substituteValueWithError(in) 96 | if err != nil { 97 | t.Errorf("%s !-> %v, got an error %s", in, expect, err) 98 | } 99 | if !bytes.Equal(res, []byte(expect)) { 100 | t.Errorf("%s !-> %v, got %s", in, expect, res) 101 | } 102 | } 103 | } 104 | 105 | func TestManyBad(t *testing.T) { 106 | tests := []string{ 107 | ``, 109 | ``, 110 | ``, 111 | 112 | ``, 114 | ``, 115 | ``, 116 | } 117 | for _, input := range tests { 118 | in := []byte(input) 119 | res, err := subst.substituteValueWithError(in) 120 | if err != nil { 121 | t.Errorf("want %s untouched, got an error %s", in, err) 122 | } 123 | if !bytes.Equal(res, in) { 124 | t.Errorf("want %s untouched but got %s", input, res) 125 | } 126 | } 127 | } 128 | 129 | func TestBadSubst(t *testing.T) { 130 | tests := []string{ 131 | ``, 132 | } 133 | for _, input := range tests { 134 | in := []byte(input) 135 | _, err := subst.substituteValueWithError(in) 136 | expectedError := `Invalid modifier nonsense` 137 | if err != nil && err.Error() != expectedError { 138 | t.Errorf("Expecting %s, got %s", expectedError, err) 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/substitution/substitute_test.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | import ( 4 | "bytes" 5 | "testing" 6 | ) 7 | 8 | var substVs = mockValueSource{values: map[pathKeyTuple][]byte{ 9 | {`/path/to/thing`, `foo`}: []byte(`bar`), 10 | {`/path/to/thing`, `frog`}: []byte(`wallop`), 11 | {`/path/to/thing`, `really`}: []byte(`nice`), 12 | {`/spacepath/ `, `nice`}: []byte(`time`), 13 | }, 14 | } 15 | 16 | func TestStringSubst(t *testing.T) { 17 | tests := map[string]string{ 18 | `Hello, we're looking for foo to be here`: `Hello, we're looking for foo bar to be here`, 19 | 20 | `Hello, we're looking for foo to be here`: `Hello, we're looking for foobar to be here`, 21 | 22 | `Hi foo .`: `Hi foobar time.`, 23 | 24 | `Hi foo .`: `Hi foo time.`, 27 | `Hello, my secret is .`: `Hello, my secret is YmFy.`, 28 | `Hi foo .`: `Hi foobarwallop time.`, 29 | 30 | `Hello, we're looking for foo to be here`: `Hello, we're looking for foo bar to be here`, 31 | 32 | `Hello, we're looking for foo to be here`: `Hello, we're looking for foobar to be here`, 33 | 34 | `Hi foo .`: `Hi foobar time.`, 35 | 36 | `Hi foo .`: `Hi foo time.`, 39 | `Hello, my secret is .`: `Hello, my secret is YmFy.`, 40 | `Hi foo .`: `Hi foobarwallop time.`, 41 | } 42 | for input, expect := range tests { 43 | in := []byte(input) 44 | subst := Substitutor{Source: substVs} 45 | res, errs := subst.Substitute(in) 46 | if errs != nil { 47 | t.Errorf("Got unexpected errors in substitute test %s", errs) 48 | } 49 | if !bytes.Equal(res, []byte(expect)) { 50 | t.Errorf("%s !-> %v, got %s", in, expect, res) 51 | } 52 | } 53 | } 54 | 55 | func TestStringSubstB64(t *testing.T) { 56 | tests := map[string]string{ 57 | `foo PHNlY3JldDovcGF0aC90by90aGluZ35mb28+ to be here`: `foo YmFy to be here`, 58 | `foo "PHNlY3JldDovcGF0aC90by90aGluZ35mb28+" to be here`: `foo "YmFy" to be here`, 59 | `foo 'PHNlY3JldDovcGF0aC90by90aGluZ35mb28+' to be here`: `foo 'YmFy' to be here`, 60 | `fooPHNlY3JldDovcGF0aC90by90aGluZ35mb28+ to be here`: `fooPHNlY3JldDovcGF0aC90by90aGluZ35mb28+ to be here`, 61 | `fooPHNlY3JldDovcGF0aC90by90aGluZ35mb28+to be here`: `fooPHNlY3JldDovcGF0aC90by90aGluZ35mb28+to be here`, 62 | `VGhpcyBpcyBhIG1peGVkIHVwIDxzZWNyZXQ6L3BhdGgvdG8vdGhpbmd+Zm9vPiB0aGluZyA8c2VjcmV0Oi9zcGFjZXBhdGgvJTIwfm5pY2U+IGluIGJhc2U2NA==`: `VGhpcyBpcyBhIG1peGVkIHVwIGJhciB0aGluZyB0aW1lIGluIGJhc2U2NA==`, 63 | 64 | `foo PHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4= to be here`: `foo YmFy to be here`, 65 | `foo "PHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4=" to be here`: `foo "YmFy" to be here`, 66 | `foo 'PHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4=' to be here`: `foo 'YmFy' to be here`, 67 | `fooPHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4= to be here`: `fooPHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4= to be here`, 68 | `fooPHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4=to be here`: `fooPHZhdWx0Oi9wYXRoL3RvL3RoaW5nfmZvbz4=to be here`, 69 | `VGhpcyBpcyBhIG1peGVkIHVwIDx2YXVsdDovcGF0aC90by90aGluZ35mb28+IHRoaW5nIDx2YXVsdDovc3BhY2VwYXRoLyUyMH5uaWNlPiBpbiBiYXNlNjQ=`: `VGhpcyBpcyBhIG1peGVkIHVwIGJhciB0aGluZyB0aW1lIGluIGJhc2U2NA==`, 70 | `destination=`: `destination=`, 71 | } 72 | for input, expect := range tests { 73 | in := []byte(input) 74 | subst := Substitutor{Source: substVs} 75 | res, errs := subst.Substitute(in) 76 | if errs != nil { 77 | t.Errorf("Got unexpected errors in substitute test %s", errs) 78 | } 79 | if !bytes.Equal(res, []byte(expect)) { 80 | t.Errorf("%s !-> %v, got %s", in, expect, res) 81 | } 82 | } 83 | } 84 | 85 | func TestModifiers(t *testing.T) { 86 | // Fragile ordering 87 | tests := map[string]string{ 88 | `JSON .`: `JSON ["bar","wallop"].`, 89 | `JSON .`: `JSON ["wallop","bar"].`, 90 | `JSON .`: `JSON {"foo":"bar","frog":"wallop"}.`, 91 | `JSON .`: `JSON {"wallop":"bar"}.`, 92 | `JSON .`: `JSON {"bar":"wallop"}.`, 93 | `JSON .`: `JSON [{"key1":"bar","key2":"wallop"}].`, 94 | `JSON .`: `JSON [{"key1":"bar","key2":"wallop"},{"key1":"wallop","key2":"nice"}].`, 95 | `JSON .`: `JSON [{"key1":"foo","key2":"bar"},{"key1":"frog","key2":"wallop"}].`, 96 | 97 | `JSON .`: `JSON ["bar","wallop"].`, 98 | `JSON .`: `JSON ["wallop","bar"].`, 99 | `JSON .`: `JSON {"foo":"bar","frog":"wallop"}.`, 100 | `JSON .`: `JSON {"wallop":"bar"}.`, 101 | `JSON .`: `JSON {"bar":"wallop"}.`, 102 | `JSON .`: `JSON [{"key1":"bar","key2":"wallop"}].`, 103 | `JSON .`: `JSON [{"key1":"bar","key2":"wallop"},{"key1":"wallop","key2":"nice"}].`, 104 | `JSON .`: `JSON [{"key1":"foo","key2":"bar"},{"key1":"frog","key2":"wallop"}].`, 105 | } 106 | for input, expect := range tests { 107 | in := []byte(input) 108 | subst := Substitutor{Source: substVs} 109 | res, errs := subst.Substitute(in) 110 | if errs != nil { 111 | t.Errorf("Got unexpected errors in substitute test %s", errs) 112 | } 113 | if !bytes.Equal(res, []byte(expect)) { 114 | t.Errorf("%s !-> %v, got %s", in, expect, res) 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/substitution/valueSource.go: -------------------------------------------------------------------------------- 1 | package substitution 2 | 3 | // ValueSource is the interface defining a single call 4 | // GetValue - takes a path and key to a value and returns that value, or null and an error explaining why not 5 | type ValueSource interface { 6 | GetValue(path []byte, key []byte) (*[]byte, error) 7 | } 8 | -------------------------------------------------------------------------------- /src/vaultvaluesource/kubernetes.go: -------------------------------------------------------------------------------- 1 | package vaultvaluesource 2 | 3 | import ( 4 | "fmt" 5 | "io/ioutil" 6 | "log" 7 | "os" 8 | "strings" 9 | ) 10 | 11 | const ( 12 | serviceAccountFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" 13 | roleEnv = "VAULT_ROLE" 14 | defaultRole = "argocd" 15 | authPathEnv = "VAULT_AUTH_PATH" 16 | defaultAuthPath = "/auth/kubernetes/login/" 17 | ) 18 | 19 | const argoPrefix = `ARGOCD_ENV_` 20 | 21 | func getArgoEnv(name string, defaultVal string) string { 22 | result, got := os.LookupEnv(argoPrefix + name) 23 | if !got { 24 | result, got = os.LookupEnv(name) 25 | if !got { 26 | return defaultVal 27 | } 28 | } 29 | return result 30 | } 31 | 32 | // readJWT reads the JWT data for the Agent to submit to Vault. The default is 33 | // to read the JWT from the default service account location, defined by the 34 | // constant serviceAccountFile. In normal use k.jwtData is nil at invocation and 35 | // the method falls back to reading the token path with os.Open, opening a file 36 | // from either the default location or from the token_path path specified in 37 | // configuration. 38 | func readJWT() (string, error) { 39 | // load configured token path if set, default to serviceAccountFile 40 | tokenFilePath := serviceAccountFile 41 | 42 | f, err := os.Open(tokenFilePath) 43 | if err != nil { 44 | log.Printf("Kubernetes authentication - no secret found %v", err) 45 | return "", nil 46 | } 47 | defer f.Close() 48 | 49 | contentBytes, err := ioutil.ReadAll(f) 50 | if err != nil { 51 | return "", err 52 | } 53 | 54 | return strings.TrimSpace(string(contentBytes)), nil 55 | } 56 | 57 | func getVaultRole() string { 58 | return getArgoEnv(roleEnv, defaultRole) 59 | } 60 | 61 | func getVaultAuthPath() string { 62 | path := getArgoEnv(authPathEnv, defaultAuthPath) 63 | if path != defaultAuthPath { 64 | return fmt.Sprintf("/auth/%s/login/", path) 65 | } 66 | return path 67 | } 68 | 69 | func (m *VaultValueSource) tryKubernetesAuth() error { 70 | jwt, err := readJWT() 71 | if err != nil { 72 | return err 73 | } 74 | if jwt == "" { 75 | return nil 76 | } 77 | secret, err := m.Client.Logical().Write(getVaultAuthPath(), map[string]interface{}{ 78 | "role": getVaultRole(), 79 | "jwt": jwt, 80 | }) 81 | if err != nil { 82 | return err 83 | } 84 | m.Client.SetToken(secret.Auth.ClientToken) 85 | return nil 86 | } 87 | -------------------------------------------------------------------------------- /src/vaultvaluesource/vaultValueSource.go: -------------------------------------------------------------------------------- 1 | package vaultvaluesource 2 | 3 | import ( 4 | "fmt" 5 | vault "github.com/hashicorp/vault/api" 6 | ) 7 | 8 | // VaultValueSource is a value source getting values from hashicorp vault 9 | type VaultValueSource struct { 10 | Client *vault.Client 11 | } 12 | 13 | func (m *VaultValueSource) initClient() error { 14 | if m.Client == nil { 15 | client, err := vault.NewClient(nil) 16 | if err != nil { 17 | return err 18 | } 19 | m.Client = client 20 | err = m.tryKubernetesAuth() 21 | if err != nil { 22 | return err 23 | } 24 | } 25 | return nil 26 | } 27 | 28 | // GetValue returns a value from a path+key in hashicorp vault or null if it doesn't exist 29 | func (m VaultValueSource) GetValue(path []byte, key []byte) (*[]byte, error) { 30 | err := m.initClient() 31 | if err != nil { 32 | return nil, err 33 | } 34 | secret, err := m.Client.Logical().Read(string(path)) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if secret == nil { 39 | return nil, fmt.Errorf("Unexpectedly couldn't find %s~%s", path, key) 40 | } 41 | 42 | // Joy of casting in go 43 | if _, ok := secret.Data["data"]; ok { 44 | switch data := secret.Data["data"].(type) { 45 | case map[string]interface{}: 46 | if value, found := data[string(key)]; found { 47 | switch dataval := value.(type) { 48 | case string: 49 | datavalbyte := []byte(dataval) 50 | return &datavalbyte, nil 51 | } 52 | } 53 | } 54 | } 55 | return nil, fmt.Errorf("Couldn't find %s~%s", path, key) 56 | } 57 | -------------------------------------------------------------------------------- /src/vaultvaluesource/vaultValueSource_test.go: -------------------------------------------------------------------------------- 1 | package vaultvaluesource 2 | 3 | import ( 4 | "bytes" 5 | "github.com/hashicorp/vault/api" 6 | "github.com/hashicorp/vault/http" 7 | "github.com/hashicorp/vault/vault" 8 | "net" 9 | "testing" 10 | ) 11 | 12 | func createTestVault(t *testing.T) (net.Listener, *api.Client) { 13 | t.Helper() 14 | 15 | // Create an in-memory, unsealed core (the "backend", if you will). 16 | core, keyShares, rootToken := vault.TestCoreUnsealed(t) 17 | _ = keyShares 18 | 19 | // Start an HTTP server for the core. 20 | ln, addr := http.TestServer(t, core) 21 | 22 | // Create a client that talks to the server, initially authenticating with 23 | // the root token. 24 | conf := api.DefaultConfig() 25 | conf.Address = addr 26 | 27 | client, err := api.NewClient(conf) 28 | if err != nil { 29 | t.Fatal(err) 30 | } 31 | client.SetToken(rootToken) 32 | 33 | // Setup required secrets, policies, etc. 34 | _, err = client.Logical().Write("secret/data/path", map[string]interface{}{ 35 | "data": map[string]interface{}{ 36 | "foo": "hi", 37 | }, 38 | }) 39 | if err != nil { 40 | t.Fatal(err) 41 | } 42 | 43 | return ln, client 44 | } 45 | 46 | func TestGetValue(t *testing.T) { 47 | ln, client := createTestVault(t) 48 | defer ln.Close() 49 | 50 | vs := VaultValueSource{Client: client} 51 | 52 | val, err := vs.GetValue([]byte(`/secret/data/path`), []byte(`foo`)) 53 | if err != nil { 54 | t.Errorf("Unexpected error %s", err) 55 | } 56 | if !bytes.Equal(*val, []byte(`hi`)) { 57 | t.Errorf("/secret/data/path,foo !-> hi, got %s", val) 58 | } 59 | val, err = vs.GetValue([]byte(`pa`), []byte(`key`)) 60 | expectedError := `Unexpectedly couldn't find pa~key` 61 | if err != nil && err.Error() != expectedError { 62 | t.Errorf("Expecting %s, got %s", expectedError, err) 63 | } 64 | if val != nil { 65 | t.Errorf("pa,key !-> nil, got %s", val) 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /test/plain/configmap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: argocd-cm 5 | data: 6 | thingfromvault: 7 | -------------------------------------------------------------------------------- /test/plain/expected.txt: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: v1 3 | kind: ConfigMap 4 | metadata: 5 | name: argocd-cm 6 | data: 7 | thingfromvault: hi 8 | 9 | --- 10 | apiVersion: v1 11 | kind: Secret 12 | metadata: 13 | name: test-secret 14 | type: Opaque 15 | data: 16 | foo: aGk= 17 | bar: ZXhhbXBsZQ== 18 | bar: "ZXhhbXBsZQ==" 19 | bar: 'ZXhhbXBsZQ==' 20 | 21 | -------------------------------------------------------------------------------- /test/plain/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: test-secret 5 | type: Opaque 6 | data: 7 | foo: 8 | bar: 9 | bar: "" 10 | bar: '' 11 | -------------------------------------------------------------------------------- /testvalues/README.md: -------------------------------------------------------------------------------- 1 | * Some test values for reading over git 2 | -------------------------------------------------------------------------------- /testvalues/foo: -------------------------------------------------------------------------------- 1 | bar-oaQuei1aij -------------------------------------------------------------------------------- /testvalues/lemon/fig: -------------------------------------------------------------------------------- 1 | banana-oaQuei1aij -------------------------------------------------------------------------------- /testvalues/test.json: -------------------------------------------------------------------------------- 1 | { 2 | "testvalues": { 3 | "foo": "bar-bohg2luSai", 4 | "lemon": { 5 | "fig": "banana-bohg2luSai" 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /testvalues/test.yaml: -------------------------------------------------------------------------------- 1 | testvalues: 2 | foo: bar-iegeiFe3ae 3 | lemon: 4 | fig: banana-iegeiFe3ae 5 | -------------------------------------------------------------------------------- /testvalues/test.yml: -------------------------------------------------------------------------------- 1 | testvalues: 2 | foo: bar-vieHuch8yi 3 | lemon: 4 | fig: banana-vieHuch8yi 5 | --------------------------------------------------------------------------------