├── .github ├── dependabot.yaml └── workflows │ ├── ci.yaml │ └── scripts │ ├── copywrite.hcl │ └── golangci.yaml ├── .gitignore ├── .goreleaser.yaml ├── Justfile ├── LICENSE ├── Makefile ├── README.md ├── go.mod ├── go.sum ├── hack └── test.sh ├── internal ├── commands │ ├── common.go │ ├── common_test.go │ ├── exec.go │ ├── exec_test.go │ ├── list.go │ ├── list_test.go │ ├── purge.go │ ├── purge_test.go │ ├── set.go │ ├── set_test.go │ ├── show.go │ ├── show_test.go │ └── testing │ │ ├── a.sh │ │ └── b.sh ├── keyring │ ├── keyring.go │ ├── keyring_test.go │ ├── ring_mock.go │ ├── token.go │ └── token_test.go ├── output │ ├── writer.go │ └── writer_test.go ├── safe │ ├── box_mock.go │ ├── namespace.go │ ├── namespace_test.go │ ├── safe.go │ └── safe_test.go └── setup │ ├── tool.go │ └── tool_test.go └── main.go /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: gomod 4 | directory: "/" 5 | schedule: 6 | interval: "monthly" 7 | labels: 8 | - "theme/dependencies" 9 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: Run CI Tests 2 | on: 3 | pull_request: 4 | paths-ignore: 5 | - 'README.md' 6 | - 'LICENSE' 7 | push: 8 | branches: 9 | - 'main' 10 | jobs: 11 | run-copywrite: 12 | timeout-minutes: 5 13 | runs-on: ubuntu-24.04 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: hashicorp/setup-copywrite@v1.1.3 17 | - name: verify copyright 18 | run: | 19 | copywrite --config .github/workflows/scripts/copywrite.hcl \ 20 | headers --spdx "MIT" --plan 21 | run-lint: 22 | timeout-minutes: 5 23 | runs-on: ubuntu-24.04 24 | steps: 25 | - uses: actions/checkout@v4 26 | - uses: golangci/golangci-lint-action@v7 27 | with: 28 | version: v2.0.2 29 | args: --config .github/workflows/scripts/golangci.yaml 30 | run-tests: 31 | timeout-minutes: 5 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | os: 36 | - ubuntu-24.04 37 | - macos-15 38 | runs-on: ${{matrix.os}} 39 | steps: 40 | - uses: actions/checkout@v4 41 | - uses: extractions/setup-just@v2 42 | - uses: actions/setup-go@v5 43 | with: 44 | go-version-file: go.mod 45 | - name: Run Go Test 46 | run: | 47 | just init tidy lint tests 48 | -------------------------------------------------------------------------------- /.github/workflows/scripts/copywrite.hcl: -------------------------------------------------------------------------------- 1 | schema_version = 1 2 | 3 | project { 4 | license = "MIT" 5 | copyright_holder = "Seth Hoenig" 6 | copyright_year = 2020 7 | header_ignore = [ 8 | "**/*.sh", 9 | ".src/**", 10 | ".bin/**", 11 | ".github/**", 12 | ".goreleaser.yaml", 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/scripts/golangci.yaml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | enable: 4 | - asasalint 5 | - asciicheck 6 | - bidichk 7 | - bodyclose 8 | - dogsled 9 | - dupword 10 | - durationcheck 11 | - errname 12 | - errorlint 13 | - exhaustive 14 | - gocritic 15 | - makezero 16 | - misspell 17 | - musttag 18 | - nilnil 19 | - noctx 20 | - perfsprint 21 | - prealloc 22 | - predeclared 23 | - reassign 24 | - revive 25 | - rowserrcheck 26 | - sqlclosecheck 27 | - tagalign 28 | - usetesting 29 | - whitespace 30 | exclusions: 31 | generated: lax 32 | presets: 33 | - comments 34 | - common-false-positives 35 | - legacy 36 | - std-error-handling 37 | paths: 38 | - third_party$ 39 | - builtin$ 40 | - examples$ 41 | formatters: 42 | enable: 43 | - gofmt 44 | exclusions: 45 | generated: lax 46 | paths: 47 | - third_party$ 48 | - builtin$ 49 | - examples$ 50 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | envy 2 | output/ 3 | dist/ 4 | .bin/ 5 | 6 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | before: 2 | hooks: 3 | - go mod tidy 4 | builds: 5 | - env: 6 | - CGO_ENABLED=0 7 | goarch: 8 | - amd64 9 | - arm64 10 | goos: 11 | - linux 12 | - darwin 13 | - windows 14 | - freebsd 15 | - openbsd 16 | checksum: 17 | name_template: 'checksums.txt' 18 | snapshot: 19 | name_template: "{{ incpatch .Version }}-next" 20 | changelog: 21 | sort: asc 22 | filters: 23 | exclude: 24 | - '^docs:' 25 | - '^test:' 26 | -------------------------------------------------------------------------------- /Justfile: -------------------------------------------------------------------------------- 1 | set shell := ["bash", "-u", "-c"] 2 | 3 | export scripts := ".github/workflows/scripts" 4 | export GOBIN := `echo $PWD/.bin` 5 | 6 | # show available commands 7 | [private] 8 | default: 9 | @just --list 10 | 11 | # tidy up Go modules 12 | [group('build')] 13 | tidy: 14 | go mod tidy 15 | 16 | # run tests across source tree 17 | [group('build')] 18 | tests: 19 | go test -v -race -count=1 ./... 20 | 21 | # ensure copywrite headers present on source files 22 | [group('lint')] 23 | copywrite: 24 | copywrite \ 25 | --config {{scripts}}/copywrite.hcl headers \ 26 | --spdx "MIT" 27 | 28 | # apply go vet command on source tree 29 | [group('lint')] 30 | vet: 31 | go vet ./... 32 | 33 | # apply golangci-lint linters on source tree 34 | [group('lint')] 35 | lint: vet 36 | $GOBIN/golangci-lint run --config {{scripts}}/golangci.yaml 37 | 38 | # locally install build dependencies 39 | [group('build')] 40 | init: 41 | go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.0.2 42 | 43 | # show host system information 44 | [group('build')] 45 | @sysinfo: 46 | echo "{{os()/arch()}} {{num_cpus()}}c" 47 | 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Seth Hoenig 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | NAME = envy 2 | 3 | .PHONY: compile 4 | build: clean 5 | @echo "--> Compile ..." 6 | CGO_ENABLED=0 go build -o output/$(NAME) 7 | 8 | .PHONY: clean 9 | clean: 10 | @echo "--> Clean ..." 11 | rm -rf dist output/$(NAME) 12 | 13 | .PHONY: test 14 | test: 15 | @echo "--> Test Go Sources ..." 16 | go test -race ./... 17 | 18 | .PHONY: vet 19 | vet: 20 | @echo "--> Vet Go Sources ..." 21 | go vet ./... 22 | 23 | .PHONY: copywrite 24 | copywrite: 25 | @echo "--> Checking Copywrite ..." 26 | copywrite \ 27 | --config .github/workflows/scripts/copywrite.hcl headers \ 28 | --spdx "MIT" 29 | 30 | .PHONY: lint 31 | lint: vet 32 | @echo "--> Lint ..." 33 | @golangci-lint run --config .github/workflows/scripts/golangci.yaml 34 | 35 | .PHONY: release 36 | release: 37 | @echo "--> RELEASE ..." 38 | envy exec gh-release goreleaser release --clean 39 | $(MAKE) clean 40 | 41 | default: compile 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | envy 2 | ==== 3 | 4 | Use `envy` to manage sensitive environment variables when running commands. 5 | 6 | [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) 7 | [![Run CI Tests](https://github.com/shoenig/envy/actions/workflows/ci.yaml/badge.svg)](https://github.com/shoenig/envy/actions/workflows/ci.yaml) 8 | 9 | # Project Overview 10 | 11 | `github.com/shoenig/envy` provides a CLI utility for running commands with secret 12 | environment variables like `GITHUB_TOKEN`, etc. 13 | 14 | `envy` builds on ideas from [envchain](https://github.com/sorah/envchain) and [schain](https://github.com/evanphx/schain). 15 | It makes use of the [go-keyring](https://github.com/zalando/go-keyring) library for multi-platform keyring management. 16 | Encryption is based on Go's built-in `crypto/aes` library. 17 | Persistent storage is managed through [boltdb](https://github.com/etcd-io/bbolt). 18 | 19 | Supports **Linux**, **macOS**, and **Windows** 20 | 21 | # Getting Started 22 | 23 | #### Install 24 | 25 | The `envy` command is available to download from the [Releases](https://github.com/shoenig/envy/releases) page. 26 | 27 | Multiple operating systems and architectures are supported, including 28 | 29 | - Linux 30 | - macOS 31 | - Windows 32 | 33 | #### Install from Go module 34 | 35 | The `envy` command can be installed from source by running 36 | 37 | ```bash 38 | $ go install github.com/shoenig/envy@latest 39 | ``` 40 | 41 | # Example Usages 42 | 43 | #### usage overview 44 | 45 | ```bash 46 | NAME: 47 | envy - wrangle environment varibles 48 | 49 | USAGE: 50 | envy [global options] [command [command options]] [arguments...] 51 | 52 | VERSION: 53 | v0 54 | 55 | DESCRIPTION: 56 | The envy is a command line tool for managing profiles of 57 | environment variables. Values are stored securely using 58 | encryption with keys protected by your desktop keychain. 59 | 60 | COMMANDS: 61 | list - list environment profiles 62 | set - set environment variable(s) in a profile 63 | purge - purge an environment profile 64 | show - show values in an environment variable profile 65 | exec - run a command using environment variables from profile 66 | 67 | GLOBALS: 68 | --help/-h boolean - print help message 69 | ``` 70 | 71 | #### create/update a profile 72 | 73 | ```bash 74 | $ envy set example FOO=1 BAR=2 BAZ=3 75 | ``` 76 | 77 | #### update existing variable in a profile 78 | 79 | ```bash 80 | $ envy set example FOO=4 81 | ``` 82 | 83 | #### remove variable from profile 84 | 85 | ```bash 86 | $ envy set example -FOO 87 | ``` 88 | 89 | #### execute command 90 | 91 | ```bash 92 | $ envy exec example env 93 | BAR=2 94 | BAZ=3 95 | ... ... 96 | ``` 97 | 98 | #### execute command excluding external environment 99 | 100 | ```bash 101 | $ envy exec --insulate example env 102 | BAR=2 103 | BAZ=3 104 | ``` 105 | 106 | Note that `-i` is short for `--insulate`. 107 | 108 | #### execute command including extra variables 109 | 110 | ```bash 111 | $ envy exec --insulate example EXTRA=value env 112 | EXTRA=value 113 | BAR=2 114 | BAZ-3 115 | ``` 116 | 117 | #### list available profiles 118 | 119 | ```bash 120 | $ envy list 121 | consul/connect-acls:no_tls 122 | example 123 | nomad/e2e 124 | test 125 | ``` 126 | 127 | #### show variables in a profile 128 | 129 | ```bash 130 | $ envy show test 131 | AWS_ACCESS_KEY_ID 132 | AWS_SECRET_ACCESS_KEY 133 | ``` 134 | 135 | #### show profile variables w/ values 136 | 137 | ```bash 138 | $ envy show --unveil test 139 | AWS_ACCESS_KEY_ID=aaabbbccc 140 | AWS_SECRET_ACCESS_KEY=233kjsdf309jfsd 141 | ``` 142 | 143 | Note that `-u` is short for `--unveil`. 144 | 145 | #### delete profile 146 | 147 | ```bash 148 | $ envy purge test 149 | purged profile "test" 150 | ``` 151 | 152 | # Contributing 153 | 154 | The `github.com/shoenig/envy` module is always improving with new features 155 | and error corrections. For contributing bug fixes and new features please file 156 | an issue. 157 | 158 | # LICENSE 159 | 160 | The `github.com/shoenig/envy` module is open source under the [MIT](LICENSE) license. 161 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/shoenig/envy 2 | 3 | go 1.24 4 | 5 | require ( 6 | cattlecloud.net/go/babycli v0.2.0 7 | github.com/gojuno/minimock/v3 v3.4.5 8 | github.com/hashicorp/go-set/v3 v3.0.0 9 | github.com/hashicorp/go-uuid v1.0.3 10 | github.com/pkg/errors v0.9.1 11 | github.com/shoenig/go-conceal v0.5.4 12 | github.com/shoenig/ignore v0.4.0 13 | github.com/shoenig/regexplus v0.3.0 14 | github.com/shoenig/test v1.12.0 15 | github.com/zalando/go-keyring v0.2.6 16 | go.etcd.io/bbolt v1.3.11 17 | ) 18 | 19 | require ( 20 | al.essio.dev/pkg/shellescape v1.5.1 // indirect 21 | cattlecloud.net/go/stacks v1.1.0 // indirect 22 | github.com/danieljoos/wincred v1.2.2 // indirect 23 | github.com/davecgh/go-spew v1.1.1 // indirect 24 | github.com/godbus/dbus/v5 v5.1.0 // indirect 25 | github.com/google/go-cmp v0.6.0 // indirect 26 | github.com/pmezard/go-difflib v1.0.0 // indirect 27 | golang.org/x/sys v0.26.0 // indirect 28 | ) 29 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | al.essio.dev/pkg/shellescape v1.5.1 h1:86HrALUujYS/h+GtqoB26SBEdkWfmMI6FubjXlsXyho= 2 | al.essio.dev/pkg/shellescape v1.5.1/go.mod h1:6sIqp7X2P6mThCQ7twERpZTuigpr6KbZWtls1U8I890= 3 | cattlecloud.net/go/babycli v0.2.0 h1:niw4uPiFQT7bekq8OJ3LoiIsz2B7L6EZcO+esMx9AP0= 4 | cattlecloud.net/go/babycli v0.2.0/go.mod h1:IhC9GNNMMlDr5sx/lMFBDa1d7jvkI1sluTPsKbpvoAU= 5 | cattlecloud.net/go/stacks v1.1.0 h1:uXnOnluwPT+3IfRDzTmkEP8RCfToBeNremjahViWUhY= 6 | cattlecloud.net/go/stacks v1.1.0/go.mod h1:BD7LcLq19hHEyA+oQOrInqmjjiCccCcVTIxt8eEmW4M= 7 | github.com/danieljoos/wincred v1.2.2 h1:774zMFJrqaeYCK2W57BgAem/MLi6mtSE47MB6BOJ0i0= 8 | github.com/danieljoos/wincred v1.2.2/go.mod h1:w7w4Utbrz8lqeMbDAK0lkNJUv5sAOkFi7nd/ogr0Uh8= 9 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 10 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 11 | github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk= 12 | github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 13 | github.com/gojuno/minimock/v3 v3.4.5 h1:Jcb0tEYZvVlQNtAAYpg3jCOoSwss2c1/rNugYTzj304= 14 | github.com/gojuno/minimock/v3 v3.4.5/go.mod h1:o9F8i2IT8v3yirA7mmdpNGzh1WNesm6iQakMtQV6KiE= 15 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 16 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 17 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= 18 | github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= 19 | github.com/hashicorp/go-set/v3 v3.0.0 h1:CaJBQvQCOWoftrBcDt7Nwgo0kdpmrKxar/x2o6pV9JA= 20 | github.com/hashicorp/go-set/v3 v3.0.0/go.mod h1:IEghM2MpE5IaNvL+D7X480dfNtxjRXZ6VMpK3C8s2ok= 21 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 22 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 23 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 24 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 25 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 26 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 27 | github.com/shoenig/go-conceal v0.5.4 h1:xLzarDUw3vUJjz+DirzO58yijkX4I9F1KA+RPZMLGLY= 28 | github.com/shoenig/go-conceal v0.5.4/go.mod h1:LXmjZn/bO1Nrtvfex4VNbKViVE+aMhVvskZx8o7HBfs= 29 | github.com/shoenig/ignore v0.4.0 h1:qPOWs0slbPMtenC0H3cKvu5Kn3hQFTE3yK6YJvyNDlA= 30 | github.com/shoenig/ignore v0.4.0/go.mod h1:VF91FoiYAwXq4KinOq6zP5xfFw/Ib6MfftaGKYTpmwo= 31 | github.com/shoenig/regexplus v0.3.0 h1:+eJZ5P4y1IY4+NgkIIvIqqdG/uYRBuSg4vWUjEppLcY= 32 | github.com/shoenig/regexplus v0.3.0/go.mod h1:AT46KcYMK7iD/drPHdFtmB42TetxBe5S/O3vMGPa5cw= 33 | github.com/shoenig/test v1.12.0 h1:5gu0WaxkayLUad6B/VCnBWMi5VR7oVYCw/d34SU1ed0= 34 | github.com/shoenig/test v1.12.0/go.mod h1:UxJ6u/x2v/TNs/LoLxBNJRV9DiwBBKYxXSyczsBHFoI= 35 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 36 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 37 | github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= 38 | github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 39 | github.com/zalando/go-keyring v0.2.6 h1:r7Yc3+H+Ux0+M72zacZoItR3UDxeWfKTcabvkI8ua9s= 40 | github.com/zalando/go-keyring v0.2.6/go.mod h1:2TCrxYrbUNYfNS/Kgy/LSrkSQzZ5UPVH85RwfczwvcI= 41 | go.etcd.io/bbolt v1.3.11 h1:yGEzV1wPz2yVCLsD8ZAiGHhHVlczyC9d1rP43/VCRJ0= 42 | go.etcd.io/bbolt v1.3.11/go.mod h1:dksAq7YMXoljX0xu6VF5DMZGbhYYoLUalEiSySYAS4I= 43 | golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= 44 | golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 45 | golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= 46 | golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 47 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 48 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 49 | -------------------------------------------------------------------------------- /hack/test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "a: is ${a}, b is: ${b}" 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /internal/commands/common.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "regexp" 8 | "strings" 9 | 10 | "cattlecloud.net/go/babycli" 11 | "github.com/hashicorp/go-set/v3" 12 | "github.com/pkg/errors" 13 | "github.com/shoenig/envy/internal/keyring" 14 | "github.com/shoenig/envy/internal/safe" 15 | "github.com/shoenig/envy/internal/setup" 16 | "github.com/shoenig/go-conceal" 17 | "github.com/shoenig/regexplus" 18 | ) 19 | 20 | var ( 21 | argRe = regexp.MustCompile(`^(?P\w+)=(?P.+)$`) 22 | profileRe = regexp.MustCompile(`^[-:/\w]+$`) 23 | ) 24 | 25 | const ( 26 | description = ` 27 | The envy is a command line tool for managing profiles of 28 | environment variables. Values are stored securely using 29 | encryption with keys protected by your desktop keychain.` 30 | ) 31 | 32 | func Invoke(args []string, tool *setup.Tool) babycli.Code { 33 | return invoke(args, tool) 34 | } 35 | 36 | func invoke(args []string, tool *setup.Tool) babycli.Code { 37 | r := babycli.New(&babycli.Configuration{ 38 | Arguments: args, 39 | Version: "v0", 40 | Top: &babycli.Component{ 41 | Name: "envy", 42 | Help: "wrangle environment varibles", 43 | Description: description, 44 | Components: babycli.Components{ 45 | newListCmd(tool), 46 | newSetCmd(tool), 47 | newPurgeCmd(tool), 48 | newShowCmd(tool), 49 | newExecCmd(tool), 50 | }, 51 | }, 52 | }) 53 | return r.Run() 54 | } 55 | 56 | func checkName(profile string) error { 57 | if !profileRe.MatchString(profile) { 58 | return errors.New("name uses non-word characters") 59 | } 60 | return nil 61 | } 62 | 63 | type Extractor interface { 64 | Process(args []string) (string, *set.Set[string], *set.HashSet[*conceal.Text, int], error) 65 | Profile(vars *set.HashSet[*conceal.Text, int]) (*safe.Profile, error) 66 | } 67 | 68 | type extractor struct { 69 | ring keyring.Ring 70 | } 71 | 72 | func newExtractor(ring keyring.Ring) Extractor { 73 | return &extractor{ 74 | ring: ring, 75 | } 76 | } 77 | 78 | // Process returns 79 | // - the profile 80 | // - the set of keys to be removed 81 | // - the set of key/values to be added 82 | // - any error 83 | func (e *extractor) Process(args []string) (string, *set.Set[string], *set.HashSet[*conceal.Text, int], error) { 84 | if len(args) < 2 { 85 | return "", nil, nil, errors.New("requires at least 2 arguments (profile, )") 86 | } 87 | ns := args[0] 88 | rm := set.New[string](4) 89 | add := set.NewHashSet[*conceal.Text](8) 90 | for i := 1; i < len(args); i++ { 91 | s := args[i] 92 | switch { 93 | case strings.HasPrefix(s, "-"): 94 | rm.Insert(strings.TrimPrefix(s, "-")) 95 | case strings.Contains(s, "="): 96 | add.Insert(conceal.New(s)) 97 | default: 98 | return "", nil, nil, errors.New("argument must start with '-' or contain '='") 99 | } 100 | } 101 | return ns, rm, add, nil 102 | } 103 | 104 | func (e *extractor) Profile(vars *set.HashSet[*conceal.Text, int]) (*safe.Profile, error) { 105 | content, err := e.process(vars.Slice()) 106 | if err != nil { 107 | return nil, err 108 | } 109 | return &safe.Profile{ 110 | Name: "", 111 | Content: content, 112 | }, nil 113 | } 114 | 115 | func (e *extractor) process(args []*conceal.Text) (map[string]safe.Encrypted, error) { 116 | content := make(map[string]safe.Encrypted, len(args)) 117 | for _, kv := range args { 118 | key, secret, err := e.encryptEnvVar(kv) 119 | if err != nil { 120 | return nil, err 121 | } 122 | content[key] = secret 123 | } 124 | return content, nil 125 | } 126 | 127 | func (e *extractor) encryptEnvVar(kv *conceal.Text) (string, safe.Encrypted, error) { 128 | m := regexplus.FindNamedSubmatches(argRe, kv.Unveil()) 129 | if len(m) == 2 { 130 | s := e.encrypt(conceal.New(m["secret"])) 131 | return m["key"], s, nil 132 | } 133 | return "", nil, errors.New("malformed environment variable pair") 134 | } 135 | 136 | func (e *extractor) encrypt(s *conceal.Text) safe.Encrypted { 137 | return e.ring.Encrypt(s) 138 | } 139 | -------------------------------------------------------------------------------- /internal/commands/common_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "bytes" 8 | 9 | "github.com/shoenig/envy/internal/output" 10 | "github.com/zalando/go-keyring" 11 | ) 12 | 13 | func init() { 14 | // For tests only, use the mock implementation of the keyring provider. 15 | keyring.MockInit() 16 | } 17 | 18 | func newWriter() (*bytes.Buffer, *bytes.Buffer, output.Writer) { 19 | var a, b bytes.Buffer 20 | return &a, &b, output.New(&a, &b) 21 | } 22 | -------------------------------------------------------------------------------- /internal/commands/exec.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "context" 8 | "fmt" 9 | "os" 10 | "os/exec" 11 | "strings" 12 | 13 | "cattlecloud.net/go/babycli" 14 | "github.com/shoenig/envy/internal/safe" 15 | "github.com/shoenig/envy/internal/setup" 16 | ) 17 | 18 | func newExecCmd(tool *setup.Tool) *babycli.Component { 19 | return &babycli.Component{ 20 | Name: "exec", 21 | Help: "run a command using environment variables from profile", 22 | Flags: babycli.Flags{ 23 | { 24 | Type: babycli.BooleanFlag, 25 | Long: "insulate", 26 | Short: "i", 27 | Help: "disable child process from inheriting parent environment variables", 28 | Default: &babycli.Default{ 29 | Value: false, 30 | Show: false, 31 | }, 32 | }, 33 | }, 34 | Function: func(c *babycli.Component) babycli.Code { 35 | if c.Nargs() < 2 { 36 | tool.Writer.Errorf("must specify profile and command argument(s)") 37 | return babycli.Failure 38 | } 39 | 40 | args := c.Arguments() 41 | p, err := tool.Box.Get(args[0]) 42 | if err != nil { 43 | tool.Writer.Errorf("unable to read profile: %v", err) 44 | return babycli.Failure 45 | } 46 | 47 | insulate := c.GetBool("insulate") 48 | argVars, command, args := splitArgs(args[1:]) 49 | cmd := newCmd(tool, p, insulate, argVars, command, args) 50 | 51 | if err := cmd.Run(); err != nil { 52 | tool.Writer.Errorf("failed to exec: %v", err) 53 | return babycli.Failure 54 | } 55 | 56 | return babycli.Success 57 | }, 58 | } 59 | } 60 | func env(tool *setup.Tool, pr *safe.Profile, environment []string) []string { 61 | for key, value := range pr.Content { 62 | secret := tool.Ring.Decrypt(value).Unveil() 63 | environment = append(environment, fmt.Sprintf( 64 | "%s=%s", key, secret, 65 | )) 66 | } 67 | return environment 68 | } 69 | 70 | func envContext(insulate bool) []string { 71 | if insulate { 72 | return nil 73 | } 74 | return os.Environ() 75 | } 76 | 77 | func newCmd(tool *setup.Tool, ns *safe.Profile, insulate bool, argVars []string, command string, args []string) *exec.Cmd { 78 | ctx := context.Background() 79 | cmd := exec.CommandContext(ctx, command, args...) 80 | 81 | // Environment variables are injected in the following order: 82 | // 1. OS variables if insulate is false 83 | // 2. envy profile vars 84 | // 3. Variables in input args 85 | cmd.Env = append(env(tool, ns, envContext(insulate)), argVars...) 86 | cmd.Stdout = os.Stdout 87 | cmd.Stderr = os.Stderr 88 | cmd.Stdin = os.Stdin 89 | return cmd 90 | } 91 | 92 | // splitArgs will split the list of flag.Args() into: 93 | // in-lined env vars, command, command args 94 | func splitArgs(flagArgs []string) (argVars []string, command string, commandArgs []string) { 95 | var ( 96 | startIdx = 0 97 | commandIdx = 0 98 | ) 99 | 100 | // Special case for when variables are injected in the form: "env FOO=BAR command" 101 | if flagArgs[0] == "env" { 102 | startIdx++ 103 | commandIdx++ 104 | } 105 | 106 | for commandIdx < len(flagArgs) { 107 | _, err := exec.LookPath(flagArgs[commandIdx]) 108 | if err != nil && strings.Contains(flagArgs[commandIdx], "=") { 109 | // Assume that arguments not in the path and with an equal sign are env vars. 110 | commandIdx++ 111 | } else { 112 | break 113 | } 114 | } 115 | 116 | // Only detected environment-setting, fall back to assuming the first is the command. 117 | if commandIdx >= len(flagArgs) { 118 | return nil, flagArgs[0], flagArgs[1:] 119 | } 120 | 121 | command = flagArgs[commandIdx] 122 | argVars = flagArgs[startIdx:commandIdx] 123 | commandArgs = flagArgs[commandIdx+1:] 124 | 125 | return argVars, command, commandArgs 126 | } 127 | -------------------------------------------------------------------------------- /internal/commands/exec_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "bytes" 8 | "os" 9 | "path/filepath" 10 | "runtime" 11 | "testing" 12 | 13 | "github.com/shoenig/envy/internal/keyring" 14 | "github.com/shoenig/envy/internal/safe" 15 | "github.com/shoenig/envy/internal/setup" 16 | "github.com/shoenig/go-conceal" 17 | "github.com/shoenig/test/must" 18 | ) 19 | 20 | func skipOS(t *testing.T) { 21 | switch runtime.GOOS { 22 | case "windows": 23 | t.Skip("skipping on windows") 24 | default: 25 | // do not skip 26 | } 27 | } 28 | 29 | func TestExecCmd_ok(t *testing.T) { 30 | skipOS(t) 31 | 32 | box := safe.NewBoxMock(t) 33 | defer box.MinimockFinish() 34 | 35 | ring := keyring.NewRingMock(t) 36 | defer ring.MinimockFinish() 37 | 38 | _, _, w := newWriter() 39 | 40 | tool := &setup.Tool{ 41 | Writer: w, 42 | Ring: ring, 43 | Box: box, 44 | } 45 | 46 | box.GetMock.Expect("myNS").Return(&safe.Profile{ 47 | Name: "myNS", 48 | Content: map[string]safe.Encrypted{ 49 | "a": {0x1}, 50 | "b": {0x2}, 51 | }, 52 | }, nil) 53 | 54 | ring.DecryptMock.When(safe.Encrypted{0x1}).Then(conceal.New("passw0rd")) 55 | ring.DecryptMock.When(safe.Encrypted{0x2}).Then(conceal.New("hunter2")) 56 | 57 | rc := invoke([]string{"exec", "myNS", "./testing/a.sh"}, tool) 58 | 59 | must.Zero(t, rc) 60 | } 61 | 62 | func TestExecCmd_no_command(t *testing.T) { 63 | box := safe.NewBoxMock(t) 64 | defer box.MinimockFinish() 65 | 66 | ring := keyring.NewRingMock(t) 67 | defer ring.MinimockFinish() 68 | 69 | a, b, w := newWriter() 70 | var c, d bytes.Buffer 71 | 72 | tool := &setup.Tool{ 73 | Writer: w, 74 | Ring: ring, 75 | Box: box, 76 | } 77 | 78 | rc := invoke([]string{"exec", "myNS"}, tool) 79 | 80 | must.One(t, rc) 81 | must.Eq(t, "", a.String()) 82 | must.Eq(t, "envy: must specify profile and command argument(s)\n", b.String()) 83 | must.Eq(t, "", c.String()) 84 | must.Eq(t, "", d.String()) 85 | } 86 | 87 | func TestExecCmd_bad_command(t *testing.T) { 88 | box := safe.NewBoxMock(t) 89 | defer box.MinimockFinish() 90 | 91 | ring := keyring.NewRingMock(t) 92 | defer ring.MinimockFinish() 93 | 94 | a, b, w := newWriter() 95 | var c, d bytes.Buffer 96 | 97 | tool := &setup.Tool{ 98 | Writer: w, 99 | Ring: ring, 100 | Box: box, 101 | } 102 | 103 | box.GetMock.Expect("myNS").Return(&safe.Profile{ 104 | Name: "myNS", 105 | Content: map[string]safe.Encrypted{ 106 | "a": {0x1}, 107 | "b": {0x2}, 108 | }, 109 | }, nil) 110 | 111 | ring.DecryptMock.When(safe.Encrypted{0x1}).Then(conceal.New("passw0rd")) 112 | ring.DecryptMock.When(safe.Encrypted{0x2}).Then(conceal.New("hunter2")) 113 | 114 | rc := invoke([]string{"exec", "myNS", "/does/not/exist"}, tool) 115 | 116 | must.One(t, rc) 117 | must.Eq(t, "", a.String()) 118 | must.Eq(t, "", c.String()) 119 | must.Eq(t, "", d.String()) 120 | 121 | switch runtime.GOOS { 122 | case "windows": 123 | must.StrContains(t, b.String(), "envy: failed to exec: exec:") // nolint: dupword 124 | default: 125 | must.Eq(t, "envy: failed to exec: fork/exec /does/not/exist: no such file or directory\n", b.String()) 126 | } 127 | } 128 | 129 | func Test_splitArgs(t *testing.T) { 130 | skipOS(t) 131 | 132 | type args struct { 133 | flagArgs []string 134 | } 135 | tests := map[string]struct { 136 | setup func() 137 | args args 138 | wantArgVars []string 139 | wantCommand string 140 | wantCommandArgs []string 141 | }{ 142 | "no env vars": { 143 | args: args{ 144 | flagArgs: []string{"cat", "log.out"}, 145 | }, 146 | wantArgVars: []string{}, 147 | wantCommand: "cat", 148 | wantCommandArgs: []string{"log.out"}, 149 | }, 150 | "env vars with command but no args": { 151 | args: args{ 152 | flagArgs: []string{"FOO=BAR", "ZIP=ZAP", "ls"}, 153 | }, 154 | wantArgVars: []string{"FOO=BAR", "ZIP=ZAP"}, 155 | wantCommand: "ls", 156 | wantCommandArgs: []string{}, 157 | }, 158 | "env vars with command and args": { 159 | args: args{ 160 | flagArgs: []string{"FOO=BAR", "ZIP=ZAP", "curl", "-k", "localhost:8501"}, 161 | }, 162 | wantArgVars: []string{"FOO=BAR", "ZIP=ZAP"}, 163 | wantCommand: "curl", 164 | wantCommandArgs: []string{"-k", "localhost:8501"}, 165 | }, 166 | "explicit env": { 167 | args: args{ 168 | flagArgs: []string{"env", "FOO=BAR", "ZIP=ZAP", "curl", "-k", "localhost:8501"}, 169 | }, 170 | wantArgVars: []string{"FOO=BAR", "ZIP=ZAP"}, 171 | wantCommand: "curl", 172 | wantCommandArgs: []string{"-k", "localhost:8501"}, 173 | }, 174 | "only env": { 175 | args: args{ 176 | flagArgs: []string{"env"}, 177 | }, 178 | wantArgVars: nil, 179 | wantCommand: "env", 180 | wantCommandArgs: []string{}, 181 | }, 182 | "all vars and none in path": { 183 | args: args{ 184 | flagArgs: []string{"FOO=BAR", "ZIP=ZAP", "BIP=BOP"}, 185 | }, 186 | wantArgVars: nil, 187 | wantCommand: "FOO=BAR", 188 | wantCommandArgs: []string{"ZIP=ZAP", "BIP=BOP"}, 189 | }, 190 | "cmd with equal in path is allowed": { 191 | setup: func() { 192 | tempDir := t.TempDir() 193 | t.Cleanup(func() { os.RemoveAll(tempDir) }) 194 | 195 | f, err := os.OpenFile(filepath.Join(tempDir, "my=cmd"), os.O_CREATE|os.O_EXCL, 0700) 196 | must.NoError(t, err) 197 | must.NoError(t, f.Close()) 198 | 199 | t.Setenv("PATH", tempDir) 200 | }, 201 | args: args{ 202 | flagArgs: []string{"FOO=BAR", "my=cmd", "ZIP=ZAP"}, 203 | }, 204 | wantArgVars: []string{"FOO=BAR"}, 205 | wantCommand: "my=cmd", 206 | wantCommandArgs: []string{"ZIP=ZAP"}, 207 | }, 208 | } 209 | for name, tt := range tests { 210 | t.Run(name, func(t *testing.T) { 211 | if tt.setup != nil { 212 | tt.setup() 213 | } 214 | gotArgVars, gotCommand, gotCommandArgs := splitArgs(tt.args.flagArgs) 215 | 216 | must.Eq(t, tt.wantArgVars, gotArgVars) 217 | must.Eq(t, tt.wantCommand, gotCommand) 218 | must.Eq(t, tt.wantCommandArgs, gotCommandArgs) 219 | }) 220 | } 221 | } 222 | -------------------------------------------------------------------------------- /internal/commands/list.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "cattlecloud.net/go/babycli" 8 | "github.com/shoenig/envy/internal/setup" 9 | ) 10 | 11 | func newListCmd(tool *setup.Tool) *babycli.Component { 12 | return &babycli.Component{ 13 | Name: "list", 14 | Help: "list environment profiles", 15 | Function: func(c *babycli.Component) babycli.Code { 16 | if c.Nargs() > 0 { 17 | tool.Writer.Errorf("list command expects no args") 18 | return babycli.Failure 19 | } 20 | profiles, err := tool.Box.List() 21 | if err != nil { 22 | tool.Writer.Errorf("unable to list profiles: %v", err) 23 | return babycli.Failure 24 | } 25 | 26 | for _, profile := range profiles { 27 | tool.Writer.Printf("%s", profile) 28 | } 29 | return babycli.Success 30 | }, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /internal/commands/list_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/shoenig/envy/internal/safe" 11 | "github.com/shoenig/envy/internal/setup" 12 | "github.com/shoenig/test/must" 13 | ) 14 | 15 | func TestListCmd_ok(t *testing.T) { 16 | box := safe.NewBoxMock(t) 17 | defer box.MinimockFinish() 18 | 19 | a, b, w := newWriter() 20 | 21 | tool := &setup.Tool{ 22 | Writer: w, 23 | Box: box, 24 | } 25 | 26 | box.ListMock.Expect().Return([]string{ 27 | "namespace1", "ns2", "my-ns", 28 | }, nil) 29 | 30 | // no arguments for list 31 | rc := invoke([]string{"list"}, tool) 32 | 33 | must.Zero(t, rc) 34 | must.Eq(t, "namespace1\nns2\nmy-ns\n", a.String()) 35 | must.Eq(t, "", b.String()) 36 | } 37 | 38 | func TestListCmd_list_fails(t *testing.T) { 39 | box := safe.NewBoxMock(t) 40 | defer box.MinimockFinish() 41 | 42 | a, b, w := newWriter() 43 | 44 | tool := &setup.Tool{ 45 | Writer: w, 46 | Box: box, 47 | } 48 | 49 | box.ListMock.Expect().Return(nil, errors.New("io error")) 50 | 51 | // no arguments for list 52 | rc := invoke([]string{"list"}, tool) 53 | 54 | must.One(t, rc) 55 | must.Eq(t, "", a.String()) 56 | must.Eq(t, "envy: unable to list profiles: io error\n", b.String()) 57 | } 58 | 59 | func TestListCmd_extra_args(t *testing.T) { 60 | box := safe.NewBoxMock(t) 61 | defer box.MinimockFinish() 62 | 63 | a, b, w := newWriter() 64 | 65 | tool := &setup.Tool{ 66 | Writer: w, 67 | Box: box, 68 | } 69 | 70 | // nonsense args for list 71 | rc := invoke([]string{"list", "a=b", "c=d"}, tool) 72 | 73 | must.One(t, rc) 74 | must.Eq(t, "", a.String()) 75 | must.Eq(t, "envy: list command expects no args\n", b.String()) 76 | } 77 | -------------------------------------------------------------------------------- /internal/commands/purge.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "cattlecloud.net/go/babycli" 8 | "github.com/shoenig/envy/internal/setup" 9 | ) 10 | 11 | func newPurgeCmd(tool *setup.Tool) *babycli.Component { 12 | return &babycli.Component{ 13 | Name: "purge", 14 | Help: "purge an environment profile", 15 | Function: func(c *babycli.Component) babycli.Code { 16 | if c.Nargs() != 1 { 17 | tool.Writer.Errorf("must specify one profile") 18 | return babycli.Failure 19 | } 20 | 21 | args := c.Arguments() 22 | profile := args[0] 23 | 24 | if err := checkName(profile); err != nil { 25 | tool.Writer.Errorf("unable to purge profile: %v", err) 26 | return babycli.Failure 27 | } 28 | 29 | if err := tool.Box.Purge(profile); err != nil { 30 | tool.Writer.Errorf("unable to purge profile: %v", err) 31 | return babycli.Failure 32 | } 33 | 34 | tool.Writer.Printf("purged profile %q", profile) 35 | return babycli.Success 36 | }, 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /internal/commands/purge_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/shoenig/envy/internal/safe" 11 | "github.com/shoenig/envy/internal/setup" 12 | "github.com/shoenig/test/must" 13 | ) 14 | 15 | func TestPurgeCmd_ok(t *testing.T) { 16 | box := safe.NewBoxMock(t) 17 | defer box.MinimockFinish() 18 | 19 | a, b, w := newWriter() 20 | 21 | tool := &setup.Tool{ 22 | Writer: w, 23 | Box: box, 24 | } 25 | 26 | box.PurgeMock.Expect("myNS").Return(nil) 27 | 28 | rc := invoke([]string{"purge", "myNS"}, tool) 29 | 30 | must.Zero(t, rc) 31 | must.Eq(t, "purged profile \"myNS\"\n", a.String()) 32 | must.Eq(t, "", b.String()) 33 | } 34 | 35 | func TestPurgeCmd_fails(t *testing.T) { 36 | box := safe.NewBoxMock(t) 37 | a, b, w := newWriter() 38 | 39 | tool := &setup.Tool{ 40 | Writer: w, 41 | Box: box, 42 | } 43 | 44 | box.PurgeMock.Expect("myNS").Return(errors.New("does not exist")) 45 | 46 | rc := invoke([]string{"purge", "myNS"}, tool) 47 | 48 | must.One(t, rc) 49 | must.Eq(t, "", a.String()) 50 | must.Eq(t, "envy: unable to purge profile: does not exist\n", b.String()) 51 | } 52 | 53 | func TestPurgeCmd_no_arg(t *testing.T) { 54 | box := safe.NewBoxMock(t) 55 | defer box.MinimockFinish() 56 | 57 | a, b, w := newWriter() 58 | 59 | tool := &setup.Tool{ 60 | Writer: w, 61 | Box: box, 62 | } 63 | 64 | rc := invoke([]string{"purge"}, tool) 65 | 66 | must.One(t, rc) 67 | must.Eq(t, "", a.String()) 68 | must.Eq(t, "envy: must specify one profile\n", b.String()) 69 | } 70 | 71 | func TestPurgeCmd_bad_profile(t *testing.T) { 72 | box := safe.NewBoxMock(t) 73 | defer box.MinimockFinish() 74 | 75 | a, b, w := newWriter() 76 | 77 | tool := &setup.Tool{ 78 | Writer: w, 79 | Box: box, 80 | } 81 | 82 | // profile must be valid 83 | rc := invoke([]string{"purge", "foo=bar"}, tool) 84 | 85 | must.One(t, rc) 86 | must.Eq(t, "", a.String()) 87 | must.Eq(t, "envy: unable to purge profile: name uses non-word characters\n", b.String()) 88 | } 89 | 90 | func TestPurgeCmd_two_arg(t *testing.T) { 91 | box := safe.NewBoxMock(t) 92 | defer box.MinimockFinish() 93 | 94 | a, b, w := newWriter() 95 | 96 | tool := &setup.Tool{ 97 | Writer: w, 98 | Box: box, 99 | } 100 | 101 | rc := invoke([]string{"purge", "ns1", "ns2"}, tool) 102 | 103 | must.One(t, rc) 104 | must.Eq(t, "", a.String()) 105 | must.Eq(t, "envy: must specify one profile\n", b.String()) 106 | } 107 | -------------------------------------------------------------------------------- /internal/commands/set.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "cattlecloud.net/go/babycli" 8 | "github.com/shoenig/envy/internal/setup" 9 | ) 10 | 11 | func newSetCmd(tool *setup.Tool) *babycli.Component { 12 | return &babycli.Component{ 13 | Name: "set", 14 | Help: "set environment variable(s) in a profile", 15 | Function: func(c *babycli.Component) babycli.Code { 16 | args := c.Arguments() 17 | extractor := newExtractor(tool.Ring) 18 | profile, remove, add, err := extractor.Process(args) 19 | if err != nil { 20 | tool.Writer.Errorf("unable to parse args: %v", err) 21 | return babycli.Failure 22 | } 23 | 24 | if err = checkName(profile); err != nil { 25 | tool.Writer.Errorf("could not set profile: %v", err) 26 | return babycli.Failure 27 | } 28 | 29 | if !remove.Empty() { 30 | if err := tool.Box.Delete(profile, remove); err != nil { 31 | tool.Writer.Errorf("coult not remove variables: %v", err) 32 | return babycli.Failure 33 | } 34 | } 35 | 36 | n, err := extractor.Profile(add) 37 | if err != nil { 38 | tool.Writer.Errorf("unable to parse args: %v", err) 39 | return babycli.Failure 40 | } 41 | n.Name = profile 42 | 43 | if err = tool.Box.Set(n); err != nil { 44 | tool.Writer.Errorf("unable to set variables: %v", err) 45 | return babycli.Failure 46 | } 47 | 48 | return babycli.Success 49 | }, 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /internal/commands/set_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/pkg/errors" 10 | "github.com/shoenig/envy/internal/keyring" 11 | "github.com/shoenig/envy/internal/safe" 12 | "github.com/shoenig/envy/internal/setup" 13 | "github.com/shoenig/go-conceal" 14 | "github.com/shoenig/test/must" 15 | ) 16 | 17 | func TestSetCmd_ok(t *testing.T) { 18 | box := safe.NewBoxMock(t) 19 | defer box.MinimockFinish() 20 | 21 | ring := keyring.NewRingMock(t) 22 | defer ring.MinimockFinish() 23 | 24 | a, b, w := newWriter() 25 | 26 | ring.EncryptMock.When(conceal.New("abc123")).Then(safe.Encrypted{8, 8, 8, 8, 8, 8}) 27 | ring.EncryptMock.When(conceal.New("1234")).Then(safe.Encrypted{9, 9, 9, 9}) 28 | 29 | box.SetMock.Expect(&safe.Profile{ 30 | Name: "myNS", 31 | Content: map[string]safe.Encrypted{ 32 | "foo": {8, 8, 8, 8, 8, 8}, 33 | "bar": {9, 9, 9, 9}, 34 | }, 35 | }).Return(nil) 36 | 37 | tool := &setup.Tool{ 38 | Writer: w, 39 | Ring: ring, 40 | Box: box, 41 | } 42 | 43 | rc := invoke([]string{"set", "myNS", "foo=abc123", "bar=1234"}, tool) 44 | 45 | must.Zero(t, rc) 46 | must.Eq(t, "", a.String()) 47 | must.Eq(t, "", b.String()) 48 | } 49 | 50 | func TestSetCmd_io_error(t *testing.T) { 51 | box := safe.NewBoxMock(t) 52 | defer box.MinimockFinish() 53 | 54 | ring := keyring.NewRingMock(t) 55 | defer ring.MinimockFinish() 56 | 57 | a, b, w := newWriter() 58 | 59 | box.SetMock.Expect(&safe.Profile{ 60 | Name: "myNS", 61 | Content: map[string]safe.Encrypted{ 62 | "foo": {8, 8, 8, 8, 8, 8}, 63 | "bar": {9, 9, 9, 9}, 64 | }, 65 | }).Return(errors.New("io error")) 66 | 67 | ring.EncryptMock.When(conceal.New("abc123")).Then(safe.Encrypted{8, 8, 8, 8, 8, 8}) 68 | ring.EncryptMock.When(conceal.New("1234")).Then(safe.Encrypted{9, 9, 9, 9}) 69 | 70 | tool := &setup.Tool{ 71 | Writer: w, 72 | Ring: ring, 73 | Box: box, 74 | } 75 | 76 | rc := invoke([]string{"set", "myNS", "foo=abc123", "bar=1234"}, tool) 77 | 78 | must.One(t, rc) 79 | must.Eq(t, "", a.String()) 80 | must.Eq(t, "envy: unable to set variables: io error\n", b.String()) 81 | } 82 | 83 | func TestSetCmd_bad_ns(t *testing.T) { 84 | box := safe.NewBoxMock(t) 85 | defer box.MinimockFinish() 86 | 87 | ring := keyring.NewRingMock(t) 88 | defer ring.MinimockFinish() 89 | 90 | a, b, w := newWriter() 91 | 92 | tool := &setup.Tool{ 93 | Writer: w, 94 | Ring: ring, 95 | Box: box, 96 | } 97 | 98 | // e.g. forgot to specify profile 99 | rc := invoke([]string{"set", "foo=abc123", "bar=1234"}, tool) 100 | 101 | must.One(t, rc) 102 | must.Eq(t, "", a.String()) 103 | must.Eq(t, "envy: could not set profile: name uses non-word characters\n", b.String()) 104 | } 105 | 106 | func TestSetCmd_no_vars(t *testing.T) { 107 | box := safe.NewBoxMock(t) 108 | defer box.MinimockFinish() 109 | 110 | ring := keyring.NewRingMock(t) 111 | defer ring.MinimockFinish() 112 | 113 | a, b, w := newWriter() 114 | 115 | tool := &setup.Tool{ 116 | Writer: w, 117 | Ring: ring, 118 | Box: box, 119 | } 120 | 121 | // e.g. reminder to use purge to remove profile 122 | rc := invoke([]string{"set", "ns1"}, tool) 123 | 124 | must.One(t, rc) 125 | must.Eq(t, "", a.String()) 126 | must.Eq(t, "envy: unable to parse args: requires at least 2 arguments (profile, )\n", b.String()) 127 | } 128 | -------------------------------------------------------------------------------- /internal/commands/show.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "sort" 8 | 9 | "cattlecloud.net/go/babycli" 10 | "github.com/shoenig/envy/internal/setup" 11 | ) 12 | 13 | func newShowCmd(tool *setup.Tool) *babycli.Component { 14 | return &babycli.Component{ 15 | Name: "show", 16 | Help: "show values in an environment variable profile", 17 | Flags: babycli.Flags{ 18 | { 19 | Type: babycli.BooleanFlag, 20 | Long: "unveil", 21 | Short: "u", 22 | Help: "show decrypted values", 23 | Default: &babycli.Default{ 24 | Value: false, 25 | Show: false, 26 | }, 27 | }, 28 | }, 29 | Function: func(c *babycli.Component) babycli.Code { 30 | args := c.Arguments() 31 | 32 | if len(args) != 1 { 33 | tool.Writer.Errorf("must specify profile and command argument(s)") 34 | return babycli.Failure 35 | } 36 | 37 | name := args[0] 38 | p, err := tool.Box.Get(name) 39 | if err != nil { 40 | tool.Writer.Errorf("could not read profile: %v", err) 41 | return babycli.Failure 42 | } 43 | 44 | keys := make([]string, 0, len(p.Content)) 45 | for k := range p.Content { 46 | keys = append(keys, k) 47 | } 48 | sort.Strings(keys) 49 | 50 | for _, key := range keys { 51 | if c.GetBool("unveil") { 52 | value := p.Content[key] 53 | secret := tool.Ring.Decrypt(value) 54 | tool.Writer.Printf("%s=%s", key, secret.Unveil()) 55 | } else { 56 | tool.Writer.Printf("%s", key) 57 | } 58 | } 59 | 60 | return babycli.Success 61 | }, 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/commands/show_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package commands 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/shoenig/envy/internal/keyring" 10 | "github.com/shoenig/envy/internal/safe" 11 | "github.com/shoenig/envy/internal/setup" 12 | "github.com/shoenig/go-conceal" 13 | "github.com/shoenig/test/must" 14 | ) 15 | 16 | func TestShowCmd_Execute(t *testing.T) { 17 | box := safe.NewBoxMock(t) 18 | defer box.MinimockFinish() 19 | 20 | ring := keyring.NewRingMock(t) 21 | defer ring.MinimockFinish() 22 | 23 | a, b, w := newWriter() 24 | 25 | tool := &setup.Tool{ 26 | Writer: w, 27 | Ring: ring, 28 | Box: box, 29 | } 30 | 31 | box.GetMock.Expect("myNS").Return(&safe.Profile{ 32 | Name: "myNS", 33 | Content: map[string]safe.Encrypted{ 34 | "foo": {1, 1, 1}, 35 | "bar": {2, 2, 2}, 36 | }, 37 | }, nil) 38 | 39 | rc := invoke([]string{"show", "myNS"}, tool) 40 | 41 | must.Zero(t, rc) 42 | must.Eq(t, "bar\nfoo\n", a.String()) 43 | must.Eq(t, "", b.String()) 44 | } 45 | 46 | func TestShowCmd_Execute_unveil(t *testing.T) { 47 | box := safe.NewBoxMock(t) 48 | defer box.MinimockFinish() 49 | 50 | ring := keyring.NewRingMock(t) 51 | defer ring.MinimockFinish() 52 | 53 | a, b, w := newWriter() 54 | 55 | tool := &setup.Tool{ 56 | Writer: w, 57 | Ring: ring, 58 | Box: box, 59 | } 60 | 61 | box.GetMock.Expect("myNS").Return(&safe.Profile{ 62 | Name: "myNS", 63 | Content: map[string]safe.Encrypted{ 64 | "foo": {1, 1, 1}, 65 | "bar": {2, 2, 2}, 66 | }, 67 | }, nil) 68 | 69 | ring.DecryptMock.When([]byte{2, 2, 2}).Then(conceal.New("passw0rd")) 70 | ring.DecryptMock.When([]byte{1, 1, 1}).Then(conceal.New("hunter2")) 71 | 72 | rc := invoke([]string{"show", "--unveil", "myNS"}, tool) 73 | 74 | must.Zero(t, rc) 75 | must.Eq(t, "bar=passw0rd\nfoo=hunter2\n", a.String()) 76 | must.Eq(t, "", b.String()) 77 | } 78 | 79 | func TestShowCmd_Execute_noNS(t *testing.T) { 80 | box := safe.NewBoxMock(t) 81 | defer box.MinimockFinish() 82 | 83 | ring := keyring.NewRingMock(t) 84 | defer ring.MinimockFinish() 85 | 86 | a, b, w := newWriter() 87 | 88 | tool := &setup.Tool{ 89 | Writer: w, 90 | Ring: ring, 91 | Box: box, 92 | } 93 | 94 | rc := invoke([]string{"show"}, tool) 95 | 96 | must.One(t, rc) 97 | must.Eq(t, "", a.String()) 98 | must.Eq(t, "envy: must specify profile and command argument(s)\n", b.String()) 99 | } 100 | -------------------------------------------------------------------------------- /internal/commands/testing/a.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -euo pipefail 4 | 5 | echo "a is ${a}" 6 | echo "b is ${b}" 7 | 8 | 9 | -------------------------------------------------------------------------------- /internal/commands/testing/b.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | echo "hi" 4 | -------------------------------------------------------------------------------- /internal/keyring/keyring.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package keyring 5 | 6 | import ( 7 | "crypto/aes" 8 | "crypto/cipher" 9 | "crypto/rand" 10 | 11 | "github.com/shoenig/envy/internal/safe" 12 | "github.com/shoenig/go-conceal" 13 | ) 14 | 15 | // A Ring is used to encrypt and decrypt secrets. 16 | // 17 | //go:generate go run github.com/gojuno/minimock/v3/cmd/minimock@v3.0.10 -g -i Ring -s _mock.go 18 | type Ring interface { 19 | Encrypt(*conceal.Text) safe.Encrypted 20 | Decrypt(safe.Encrypted) *conceal.Text 21 | } 22 | 23 | type ring struct { 24 | key *conceal.Bytes 25 | } 26 | 27 | func New(key *conceal.Text) Ring { 28 | return &ring{ 29 | key: uuidToLen32(key), 30 | } 31 | } 32 | 33 | func uuidToLen32(id *conceal.Text) *conceal.Bytes { 34 | return conceal.NewBytes([]byte(trim(id.Unveil()))) 35 | } 36 | 37 | func (r *ring) Encrypt(s *conceal.Text) safe.Encrypted { 38 | bCipher, err := aes.NewCipher(r.key.Unveil()) 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | gcm, err := cipher.NewGCM(bCipher) 44 | if err != nil { 45 | panic(err) 46 | } 47 | 48 | nonce := make([]byte, gcm.NonceSize()) 49 | if _, err = rand.Read(nonce); err != nil { 50 | panic(err) 51 | } 52 | 53 | return safe.Encrypted(gcm.Seal(nonce, nonce, []byte(s.Unveil()), nil)) 54 | } 55 | 56 | func (r *ring) Decrypt(s safe.Encrypted) *conceal.Text { 57 | bCipher, err := aes.NewCipher(r.key.Unveil()) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | gcm, err := cipher.NewGCM(bCipher) 63 | if err != nil { 64 | panic(err) 65 | } 66 | 67 | nonce, cText := s[:gcm.NonceSize()], s[gcm.NonceSize():] 68 | plainText, err := gcm.Open(nil, nonce, cText, nil) 69 | if err != nil { 70 | panic(err) 71 | } 72 | 73 | return conceal.New(string(plainText)) 74 | } 75 | -------------------------------------------------------------------------------- /internal/keyring/keyring_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package keyring 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/hashicorp/go-uuid" 10 | "github.com/shoenig/go-conceal" 11 | "github.com/shoenig/test/must" 12 | ) 13 | 14 | func TestRing_EncryptDecrypt(t *testing.T) { 15 | id, err := uuid.GenerateUUID() 16 | must.NoError(t, err) 17 | 18 | password := "passw0rd" 19 | r := New(conceal.New(id)) 20 | 21 | enc := r.Encrypt(conceal.New(password)) 22 | must.NotNil(t, enc) 23 | must.NotEq(t, []byte(password), enc) 24 | 25 | plain := r.Decrypt(enc) 26 | must.Eq(t, password, plain.Unveil()) 27 | } 28 | -------------------------------------------------------------------------------- /internal/keyring/ring_mock.go: -------------------------------------------------------------------------------- 1 | package keyring 2 | 3 | // Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT. 4 | 5 | import ( 6 | "sync" 7 | mm_atomic "sync/atomic" 8 | mm_time "time" 9 | 10 | "github.com/gojuno/minimock/v3" 11 | "github.com/shoenig/envy/internal/safe" 12 | "github.com/shoenig/go-conceal" 13 | ) 14 | 15 | // RingMock implements Ring 16 | type RingMock struct { 17 | t minimock.Tester 18 | 19 | funcDecrypt func(e1 safe.Encrypted) (tp1 *conceal.Text) 20 | inspectFuncDecrypt func(e1 safe.Encrypted) 21 | afterDecryptCounter uint64 22 | beforeDecryptCounter uint64 23 | DecryptMock mRingMockDecrypt 24 | 25 | funcEncrypt func(tp1 *conceal.Text) (e1 safe.Encrypted) 26 | inspectFuncEncrypt func(tp1 *conceal.Text) 27 | afterEncryptCounter uint64 28 | beforeEncryptCounter uint64 29 | EncryptMock mRingMockEncrypt 30 | } 31 | 32 | // NewRingMock returns a mock for Ring 33 | func NewRingMock(t minimock.Tester) *RingMock { 34 | m := &RingMock{t: t} 35 | if controller, ok := t.(minimock.MockController); ok { 36 | controller.RegisterMocker(m) 37 | } 38 | 39 | m.DecryptMock = mRingMockDecrypt{mock: m} 40 | m.DecryptMock.callArgs = []*RingMockDecryptParams{} 41 | 42 | m.EncryptMock = mRingMockEncrypt{mock: m} 43 | m.EncryptMock.callArgs = []*RingMockEncryptParams{} 44 | 45 | return m 46 | } 47 | 48 | type mRingMockDecrypt struct { 49 | mock *RingMock 50 | defaultExpectation *RingMockDecryptExpectation 51 | expectations []*RingMockDecryptExpectation 52 | 53 | callArgs []*RingMockDecryptParams 54 | mutex sync.RWMutex 55 | } 56 | 57 | // RingMockDecryptExpectation specifies expectation struct of the Ring.Decrypt 58 | type RingMockDecryptExpectation struct { 59 | mock *RingMock 60 | params *RingMockDecryptParams 61 | results *RingMockDecryptResults 62 | Counter uint64 63 | } 64 | 65 | // RingMockDecryptParams contains parameters of the Ring.Decrypt 66 | type RingMockDecryptParams struct { 67 | e1 safe.Encrypted 68 | } 69 | 70 | // RingMockDecryptResults contains results of the Ring.Decrypt 71 | type RingMockDecryptResults struct { 72 | tp1 *conceal.Text 73 | } 74 | 75 | // Expect sets up expected params for Ring.Decrypt 76 | func (mmDecrypt *mRingMockDecrypt) Expect(e1 safe.Encrypted) *mRingMockDecrypt { 77 | if mmDecrypt.mock.funcDecrypt != nil { 78 | mmDecrypt.mock.t.Fatalf("RingMock.Decrypt mock is already set by Set") 79 | } 80 | 81 | if mmDecrypt.defaultExpectation == nil { 82 | mmDecrypt.defaultExpectation = &RingMockDecryptExpectation{} 83 | } 84 | 85 | mmDecrypt.defaultExpectation.params = &RingMockDecryptParams{e1} 86 | for _, e := range mmDecrypt.expectations { 87 | if minimock.Equal(e.params, mmDecrypt.defaultExpectation.params) { 88 | mmDecrypt.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmDecrypt.defaultExpectation.params) 89 | } 90 | } 91 | 92 | return mmDecrypt 93 | } 94 | 95 | // Inspect accepts an inspector function that has same arguments as the Ring.Decrypt 96 | func (mmDecrypt *mRingMockDecrypt) Inspect(f func(e1 safe.Encrypted)) *mRingMockDecrypt { 97 | if mmDecrypt.mock.inspectFuncDecrypt != nil { 98 | mmDecrypt.mock.t.Fatalf("Inspect function is already set for RingMock.Decrypt") 99 | } 100 | 101 | mmDecrypt.mock.inspectFuncDecrypt = f 102 | 103 | return mmDecrypt 104 | } 105 | 106 | // Return sets up results that will be returned by Ring.Decrypt 107 | func (mmDecrypt *mRingMockDecrypt) Return(tp1 *conceal.Text) *RingMock { 108 | if mmDecrypt.mock.funcDecrypt != nil { 109 | mmDecrypt.mock.t.Fatalf("RingMock.Decrypt mock is already set by Set") 110 | } 111 | 112 | if mmDecrypt.defaultExpectation == nil { 113 | mmDecrypt.defaultExpectation = &RingMockDecryptExpectation{mock: mmDecrypt.mock} 114 | } 115 | mmDecrypt.defaultExpectation.results = &RingMockDecryptResults{tp1} 116 | return mmDecrypt.mock 117 | } 118 | 119 | // Set uses given function f to mock the Ring.Decrypt method 120 | func (mmDecrypt *mRingMockDecrypt) Set(f func(e1 safe.Encrypted) (tp1 *conceal.Text)) *RingMock { 121 | if mmDecrypt.defaultExpectation != nil { 122 | mmDecrypt.mock.t.Fatalf("Default expectation is already set for the Ring.Decrypt method") 123 | } 124 | 125 | if len(mmDecrypt.expectations) > 0 { 126 | mmDecrypt.mock.t.Fatalf("Some expectations are already set for the Ring.Decrypt method") 127 | } 128 | 129 | mmDecrypt.mock.funcDecrypt = f 130 | return mmDecrypt.mock 131 | } 132 | 133 | // When sets expectation for the Ring.Decrypt which will trigger the result defined by the following 134 | // Then helper 135 | func (mmDecrypt *mRingMockDecrypt) When(e1 safe.Encrypted) *RingMockDecryptExpectation { 136 | if mmDecrypt.mock.funcDecrypt != nil { 137 | mmDecrypt.mock.t.Fatalf("RingMock.Decrypt mock is already set by Set") 138 | } 139 | 140 | expectation := &RingMockDecryptExpectation{ 141 | mock: mmDecrypt.mock, 142 | params: &RingMockDecryptParams{e1}, 143 | } 144 | mmDecrypt.expectations = append(mmDecrypt.expectations, expectation) 145 | return expectation 146 | } 147 | 148 | // Then sets up Ring.Decrypt return parameters for the expectation previously defined by the When method 149 | func (e *RingMockDecryptExpectation) Then(tp1 *conceal.Text) *RingMock { 150 | e.results = &RingMockDecryptResults{tp1} 151 | return e.mock 152 | } 153 | 154 | // Decrypt implements Ring 155 | func (mmDecrypt *RingMock) Decrypt(e1 safe.Encrypted) (tp1 *conceal.Text) { 156 | mm_atomic.AddUint64(&mmDecrypt.beforeDecryptCounter, 1) 157 | defer mm_atomic.AddUint64(&mmDecrypt.afterDecryptCounter, 1) 158 | 159 | if mmDecrypt.inspectFuncDecrypt != nil { 160 | mmDecrypt.inspectFuncDecrypt(e1) 161 | } 162 | 163 | mm_params := &RingMockDecryptParams{e1} 164 | 165 | // Record call args 166 | mmDecrypt.DecryptMock.mutex.Lock() 167 | mmDecrypt.DecryptMock.callArgs = append(mmDecrypt.DecryptMock.callArgs, mm_params) 168 | mmDecrypt.DecryptMock.mutex.Unlock() 169 | 170 | for _, e := range mmDecrypt.DecryptMock.expectations { 171 | if minimock.Equal(e.params, mm_params) { 172 | mm_atomic.AddUint64(&e.Counter, 1) 173 | return e.results.tp1 174 | } 175 | } 176 | 177 | if mmDecrypt.DecryptMock.defaultExpectation != nil { 178 | mm_atomic.AddUint64(&mmDecrypt.DecryptMock.defaultExpectation.Counter, 1) 179 | mm_want := mmDecrypt.DecryptMock.defaultExpectation.params 180 | mm_got := RingMockDecryptParams{e1} 181 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 182 | mmDecrypt.t.Errorf("RingMock.Decrypt got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 183 | } 184 | 185 | mm_results := mmDecrypt.DecryptMock.defaultExpectation.results 186 | if mm_results == nil { 187 | mmDecrypt.t.Fatal("No results are set for the RingMock.Decrypt") 188 | } 189 | return (*mm_results).tp1 190 | } 191 | if mmDecrypt.funcDecrypt != nil { 192 | return mmDecrypt.funcDecrypt(e1) 193 | } 194 | mmDecrypt.t.Fatalf("Unexpected call to RingMock.Decrypt. %v", e1) 195 | return 196 | } 197 | 198 | // DecryptAfterCounter returns a count of finished RingMock.Decrypt invocations 199 | func (mmDecrypt *RingMock) DecryptAfterCounter() uint64 { 200 | return mm_atomic.LoadUint64(&mmDecrypt.afterDecryptCounter) 201 | } 202 | 203 | // DecryptBeforeCounter returns a count of RingMock.Decrypt invocations 204 | func (mmDecrypt *RingMock) DecryptBeforeCounter() uint64 { 205 | return mm_atomic.LoadUint64(&mmDecrypt.beforeDecryptCounter) 206 | } 207 | 208 | // Calls returns a list of arguments used in each call to RingMock.Decrypt. 209 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 210 | func (mmDecrypt *mRingMockDecrypt) Calls() []*RingMockDecryptParams { 211 | mmDecrypt.mutex.RLock() 212 | 213 | argCopy := make([]*RingMockDecryptParams, len(mmDecrypt.callArgs)) 214 | copy(argCopy, mmDecrypt.callArgs) 215 | 216 | mmDecrypt.mutex.RUnlock() 217 | 218 | return argCopy 219 | } 220 | 221 | // MinimockDecryptDone returns true if the count of the Decrypt invocations corresponds 222 | // the number of defined expectations 223 | func (m *RingMock) MinimockDecryptDone() bool { 224 | for _, e := range m.DecryptMock.expectations { 225 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 226 | return false 227 | } 228 | } 229 | 230 | // if default expectation was set then invocations count should be greater than zero 231 | if m.DecryptMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterDecryptCounter) < 1 { 232 | return false 233 | } 234 | // if func was set then invocations count should be greater than zero 235 | if m.funcDecrypt != nil && mm_atomic.LoadUint64(&m.afterDecryptCounter) < 1 { 236 | return false 237 | } 238 | return true 239 | } 240 | 241 | // MinimockDecryptInspect logs each unmet expectation 242 | func (m *RingMock) MinimockDecryptInspect() { 243 | for _, e := range m.DecryptMock.expectations { 244 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 245 | m.t.Errorf("Expected call to RingMock.Decrypt with params: %#v", *e.params) 246 | } 247 | } 248 | 249 | // if default expectation was set then invocations count should be greater than zero 250 | if m.DecryptMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterDecryptCounter) < 1 { 251 | if m.DecryptMock.defaultExpectation.params == nil { 252 | m.t.Error("Expected call to RingMock.Decrypt") 253 | } else { 254 | m.t.Errorf("Expected call to RingMock.Decrypt with params: %#v", *m.DecryptMock.defaultExpectation.params) 255 | } 256 | } 257 | // if func was set then invocations count should be greater than zero 258 | if m.funcDecrypt != nil && mm_atomic.LoadUint64(&m.afterDecryptCounter) < 1 { 259 | m.t.Error("Expected call to RingMock.Decrypt") 260 | } 261 | } 262 | 263 | type mRingMockEncrypt struct { 264 | mock *RingMock 265 | defaultExpectation *RingMockEncryptExpectation 266 | expectations []*RingMockEncryptExpectation 267 | 268 | callArgs []*RingMockEncryptParams 269 | mutex sync.RWMutex 270 | } 271 | 272 | // RingMockEncryptExpectation specifies expectation struct of the Ring.Encrypt 273 | type RingMockEncryptExpectation struct { 274 | mock *RingMock 275 | params *RingMockEncryptParams 276 | results *RingMockEncryptResults 277 | Counter uint64 278 | } 279 | 280 | // RingMockEncryptParams contains parameters of the Ring.Encrypt 281 | type RingMockEncryptParams struct { 282 | tp1 *conceal.Text 283 | } 284 | 285 | // RingMockEncryptResults contains results of the Ring.Encrypt 286 | type RingMockEncryptResults struct { 287 | e1 safe.Encrypted 288 | } 289 | 290 | // Expect sets up expected params for Ring.Encrypt 291 | func (mmEncrypt *mRingMockEncrypt) Expect(tp1 *conceal.Text) *mRingMockEncrypt { 292 | if mmEncrypt.mock.funcEncrypt != nil { 293 | mmEncrypt.mock.t.Fatalf("RingMock.Encrypt mock is already set by Set") 294 | } 295 | 296 | if mmEncrypt.defaultExpectation == nil { 297 | mmEncrypt.defaultExpectation = &RingMockEncryptExpectation{} 298 | } 299 | 300 | mmEncrypt.defaultExpectation.params = &RingMockEncryptParams{tp1} 301 | for _, e := range mmEncrypt.expectations { 302 | if minimock.Equal(e.params, mmEncrypt.defaultExpectation.params) { 303 | mmEncrypt.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmEncrypt.defaultExpectation.params) 304 | } 305 | } 306 | 307 | return mmEncrypt 308 | } 309 | 310 | // Inspect accepts an inspector function that has same arguments as the Ring.Encrypt 311 | func (mmEncrypt *mRingMockEncrypt) Inspect(f func(tp1 *conceal.Text)) *mRingMockEncrypt { 312 | if mmEncrypt.mock.inspectFuncEncrypt != nil { 313 | mmEncrypt.mock.t.Fatalf("Inspect function is already set for RingMock.Encrypt") 314 | } 315 | 316 | mmEncrypt.mock.inspectFuncEncrypt = f 317 | 318 | return mmEncrypt 319 | } 320 | 321 | // Return sets up results that will be returned by Ring.Encrypt 322 | func (mmEncrypt *mRingMockEncrypt) Return(e1 safe.Encrypted) *RingMock { 323 | if mmEncrypt.mock.funcEncrypt != nil { 324 | mmEncrypt.mock.t.Fatalf("RingMock.Encrypt mock is already set by Set") 325 | } 326 | 327 | if mmEncrypt.defaultExpectation == nil { 328 | mmEncrypt.defaultExpectation = &RingMockEncryptExpectation{mock: mmEncrypt.mock} 329 | } 330 | mmEncrypt.defaultExpectation.results = &RingMockEncryptResults{e1} 331 | return mmEncrypt.mock 332 | } 333 | 334 | // Set uses given function f to mock the Ring.Encrypt method 335 | func (mmEncrypt *mRingMockEncrypt) Set(f func(tp1 *conceal.Text) (e1 safe.Encrypted)) *RingMock { 336 | if mmEncrypt.defaultExpectation != nil { 337 | mmEncrypt.mock.t.Fatalf("Default expectation is already set for the Ring.Encrypt method") 338 | } 339 | 340 | if len(mmEncrypt.expectations) > 0 { 341 | mmEncrypt.mock.t.Fatalf("Some expectations are already set for the Ring.Encrypt method") 342 | } 343 | 344 | mmEncrypt.mock.funcEncrypt = f 345 | return mmEncrypt.mock 346 | } 347 | 348 | // When sets expectation for the Ring.Encrypt which will trigger the result defined by the following 349 | // Then helper 350 | func (mmEncrypt *mRingMockEncrypt) When(tp1 *conceal.Text) *RingMockEncryptExpectation { 351 | if mmEncrypt.mock.funcEncrypt != nil { 352 | mmEncrypt.mock.t.Fatalf("RingMock.Encrypt mock is already set by Set") 353 | } 354 | 355 | expectation := &RingMockEncryptExpectation{ 356 | mock: mmEncrypt.mock, 357 | params: &RingMockEncryptParams{tp1}, 358 | } 359 | mmEncrypt.expectations = append(mmEncrypt.expectations, expectation) 360 | return expectation 361 | } 362 | 363 | // Then sets up Ring.Encrypt return parameters for the expectation previously defined by the When method 364 | func (e *RingMockEncryptExpectation) Then(e1 safe.Encrypted) *RingMock { 365 | e.results = &RingMockEncryptResults{e1} 366 | return e.mock 367 | } 368 | 369 | // Encrypt implements Ring 370 | func (mmEncrypt *RingMock) Encrypt(tp1 *conceal.Text) (e1 safe.Encrypted) { 371 | mm_atomic.AddUint64(&mmEncrypt.beforeEncryptCounter, 1) 372 | defer mm_atomic.AddUint64(&mmEncrypt.afterEncryptCounter, 1) 373 | 374 | if mmEncrypt.inspectFuncEncrypt != nil { 375 | mmEncrypt.inspectFuncEncrypt(tp1) 376 | } 377 | 378 | mm_params := &RingMockEncryptParams{tp1} 379 | 380 | // Record call args 381 | mmEncrypt.EncryptMock.mutex.Lock() 382 | mmEncrypt.EncryptMock.callArgs = append(mmEncrypt.EncryptMock.callArgs, mm_params) 383 | mmEncrypt.EncryptMock.mutex.Unlock() 384 | 385 | for _, e := range mmEncrypt.EncryptMock.expectations { 386 | if minimock.Equal(e.params, mm_params) { 387 | mm_atomic.AddUint64(&e.Counter, 1) 388 | return e.results.e1 389 | } 390 | } 391 | 392 | if mmEncrypt.EncryptMock.defaultExpectation != nil { 393 | mm_atomic.AddUint64(&mmEncrypt.EncryptMock.defaultExpectation.Counter, 1) 394 | mm_want := mmEncrypt.EncryptMock.defaultExpectation.params 395 | mm_got := RingMockEncryptParams{tp1} 396 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 397 | mmEncrypt.t.Errorf("RingMock.Encrypt got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 398 | } 399 | 400 | mm_results := mmEncrypt.EncryptMock.defaultExpectation.results 401 | if mm_results == nil { 402 | mmEncrypt.t.Fatal("No results are set for the RingMock.Encrypt") 403 | } 404 | return (*mm_results).e1 405 | } 406 | if mmEncrypt.funcEncrypt != nil { 407 | return mmEncrypt.funcEncrypt(tp1) 408 | } 409 | mmEncrypt.t.Fatalf("Unexpected call to RingMock.Encrypt. %v", tp1) 410 | return 411 | } 412 | 413 | // EncryptAfterCounter returns a count of finished RingMock.Encrypt invocations 414 | func (mmEncrypt *RingMock) EncryptAfterCounter() uint64 { 415 | return mm_atomic.LoadUint64(&mmEncrypt.afterEncryptCounter) 416 | } 417 | 418 | // EncryptBeforeCounter returns a count of RingMock.Encrypt invocations 419 | func (mmEncrypt *RingMock) EncryptBeforeCounter() uint64 { 420 | return mm_atomic.LoadUint64(&mmEncrypt.beforeEncryptCounter) 421 | } 422 | 423 | // Calls returns a list of arguments used in each call to RingMock.Encrypt. 424 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 425 | func (mmEncrypt *mRingMockEncrypt) Calls() []*RingMockEncryptParams { 426 | mmEncrypt.mutex.RLock() 427 | 428 | argCopy := make([]*RingMockEncryptParams, len(mmEncrypt.callArgs)) 429 | copy(argCopy, mmEncrypt.callArgs) 430 | 431 | mmEncrypt.mutex.RUnlock() 432 | 433 | return argCopy 434 | } 435 | 436 | // MinimockEncryptDone returns true if the count of the Encrypt invocations corresponds 437 | // the number of defined expectations 438 | func (m *RingMock) MinimockEncryptDone() bool { 439 | for _, e := range m.EncryptMock.expectations { 440 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 441 | return false 442 | } 443 | } 444 | 445 | // if default expectation was set then invocations count should be greater than zero 446 | if m.EncryptMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterEncryptCounter) < 1 { 447 | return false 448 | } 449 | // if func was set then invocations count should be greater than zero 450 | if m.funcEncrypt != nil && mm_atomic.LoadUint64(&m.afterEncryptCounter) < 1 { 451 | return false 452 | } 453 | return true 454 | } 455 | 456 | // MinimockEncryptInspect logs each unmet expectation 457 | func (m *RingMock) MinimockEncryptInspect() { 458 | for _, e := range m.EncryptMock.expectations { 459 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 460 | m.t.Errorf("Expected call to RingMock.Encrypt with params: %#v", *e.params) 461 | } 462 | } 463 | 464 | // if default expectation was set then invocations count should be greater than zero 465 | if m.EncryptMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterEncryptCounter) < 1 { 466 | if m.EncryptMock.defaultExpectation.params == nil { 467 | m.t.Error("Expected call to RingMock.Encrypt") 468 | } else { 469 | m.t.Errorf("Expected call to RingMock.Encrypt with params: %#v", *m.EncryptMock.defaultExpectation.params) 470 | } 471 | } 472 | // if func was set then invocations count should be greater than zero 473 | if m.funcEncrypt != nil && mm_atomic.LoadUint64(&m.afterEncryptCounter) < 1 { 474 | m.t.Error("Expected call to RingMock.Encrypt") 475 | } 476 | } 477 | 478 | // MinimockFinish checks that all mocked methods have been called the expected number of times 479 | func (m *RingMock) MinimockFinish() { 480 | if !m.minimockDone() { 481 | m.MinimockDecryptInspect() 482 | 483 | m.MinimockEncryptInspect() 484 | m.t.FailNow() 485 | } 486 | } 487 | 488 | // MinimockWait waits for all mocked methods to be called the expected number of times 489 | func (m *RingMock) MinimockWait(timeout mm_time.Duration) { 490 | timeoutCh := mm_time.After(timeout) 491 | for { 492 | if m.minimockDone() { 493 | return 494 | } 495 | select { 496 | case <-timeoutCh: 497 | m.MinimockFinish() 498 | return 499 | case <-mm_time.After(10 * mm_time.Millisecond): 500 | } 501 | } 502 | } 503 | 504 | func (m *RingMock) minimockDone() bool { 505 | done := true 506 | return done && 507 | m.MinimockDecryptDone() && 508 | m.MinimockEncryptDone() 509 | } 510 | -------------------------------------------------------------------------------- /internal/keyring/token.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package keyring 5 | 6 | import ( 7 | "errors" 8 | "os" 9 | "strings" 10 | 11 | "github.com/hashicorp/go-uuid" 12 | "github.com/shoenig/go-conceal" 13 | "github.com/zalando/go-keyring" 14 | ) 15 | 16 | const ( 17 | userEnvOverride = "ENVY_USER" 18 | userEnvDefault = "USER" 19 | userDefault = "default" 20 | ) 21 | 22 | func user() string { 23 | usr := userDefault 24 | if envUsr := os.Getenv(userEnvDefault); envUsr != "" { 25 | usr = envUsr 26 | } 27 | if envUsr := os.Getenv(userEnvOverride); envUsr != "" { 28 | usr = envUsr 29 | } 30 | return usr 31 | } 32 | 33 | func trim(id string) string { 34 | return strings.TrimSpace(strings.ReplaceAll(id, "-", "")) 35 | } 36 | 37 | func bootstrap(name, user string) *conceal.Text { 38 | token, err := uuid.GenerateUUID() 39 | if err != nil { 40 | panic(err) 41 | } 42 | 43 | if err = keyring.Set(name, user, token); err != nil { 44 | panic(err) 45 | } 46 | return conceal.New(token) 47 | } 48 | 49 | // Init will acquire the secret that envy uses to encrypt values from the OS 50 | // keyring provider. 51 | func Init(name string) *conceal.Text { 52 | usr := user() 53 | token, err := keyring.Get(name, usr) 54 | 55 | switch { 56 | case errors.Is(err, keyring.ErrNotFound): 57 | return bootstrap(name, usr) 58 | case err != nil: 59 | panic(err) 60 | default: 61 | return conceal.New(token) 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /internal/keyring/token_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package keyring 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/hashicorp/go-uuid" 11 | "github.com/shoenig/test/must" 12 | "github.com/zalando/go-keyring" 13 | ) 14 | 15 | func init() { 16 | // For tests only, use the mock implementation of the keyring provider. 17 | keyring.MockInit() 18 | } 19 | 20 | func setEnv(t *testing.T, key, value string) string { 21 | previous := os.Getenv(key) 22 | t.Setenv(key, value) 23 | return previous 24 | } 25 | 26 | func TestInit_user(t *testing.T) { 27 | // no parallel, sets environment 28 | 29 | t.Run("default", func(t *testing.T) { 30 | prevUser := setEnv(t, "USER", "") 31 | defer setEnv(t, "USER", prevUser) 32 | 33 | prevEnvyUser := setEnv(t, "ENVY_USER", "") 34 | defer setEnv(t, "ENVY_USER", prevEnvyUser) 35 | 36 | u := user() 37 | must.Eq(t, "default", u) 38 | }) 39 | 40 | t.Run("user", func(t *testing.T) { 41 | prevUser := setEnv(t, "USER", "alice") 42 | defer setEnv(t, "USER", prevUser) 43 | 44 | prevEnvyUser := setEnv(t, "ENVY_USER", "") 45 | defer setEnv(t, "ENVY_USER", prevEnvyUser) 46 | 47 | u := user() 48 | must.Eq(t, "alice", u) 49 | }) 50 | 51 | t.Run("envy_user", func(t *testing.T) { 52 | prevUser := setEnv(t, "USER", "alice") 53 | defer setEnv(t, "USER", prevUser) 54 | 55 | prevEnvyUser := setEnv(t, "ENVY_USER", "bob") 56 | defer setEnv(t, "ENVY_USER", prevEnvyUser) 57 | 58 | u := user() 59 | must.Eq(t, "bob", u) 60 | }) 61 | } 62 | 63 | func isUUID(t *testing.T, id string) { 64 | _, err := uuid.ParseUUID(id) 65 | must.NoError(t, err) 66 | } 67 | 68 | func TestInit_bootstrap(t *testing.T) { 69 | // no parallel, sets environment 70 | 71 | id := bootstrap("envy.name", "alice") 72 | isUUID(t, id.Unveil()) 73 | } 74 | 75 | func TestInit_init(t *testing.T) { 76 | // no parallel, sets environment 77 | 78 | prevEnvyUser := setEnv(t, "ENVY_USER", "alice") 79 | defer setEnv(t, "ENVY_USER", prevEnvyUser) 80 | 81 | // first time goes through bootstrap 82 | id := Init("envy.name") 83 | isUUID(t, id.Unveil()) 84 | 85 | // subsequent time should already exist 86 | id2 := Init("envy.name") 87 | isUUID(t, id2.Unveil()) 88 | 89 | // and the result should be the same 90 | must.Eq(t, id.Unveil(), id2.Unveil()) 91 | } 92 | -------------------------------------------------------------------------------- /internal/output/writer.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package output 5 | 6 | import ( 7 | "fmt" 8 | "io" 9 | ) 10 | 11 | type Writer interface { 12 | Printf(format string, args ...interface{}) 13 | Errorf(format string, args ...interface{}) 14 | } 15 | 16 | // New creates a new Writer with the given output sinks. Typically one 17 | // would plug normal into os.Stdout and failure into os.Stderr, but other 18 | // outputs may be provided, for example in use of test cases. 19 | // 20 | // todo: make tracing configurable 21 | func New(normal, failure io.Writer) Writer { 22 | return &writer{ 23 | normal: normal, 24 | failure: failure, 25 | traces: false, 26 | } 27 | } 28 | 29 | type writer struct { 30 | normal io.Writer 31 | failure io.Writer 32 | traces bool 33 | } 34 | 35 | func (w *writer) Printf(format string, args ...interface{}) { 36 | tweaked := format + "\n" 37 | s := fmt.Sprintf(tweaked, args...) 38 | w.write(s) 39 | } 40 | 41 | func (w *writer) Errorf(format string, args ...interface{}) { 42 | tweaked := "envy: " + format + "\n" 43 | s := fmt.Sprintf(tweaked, args...) 44 | w.error(s) 45 | } 46 | 47 | func (w *writer) write(s string) { 48 | _, _ = io.WriteString(w.normal, s) 49 | } 50 | 51 | func (w *writer) error(s string) { 52 | _, _ = io.WriteString(w.failure, s) 53 | } 54 | -------------------------------------------------------------------------------- /internal/output/writer_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package output 5 | 6 | import ( 7 | "bytes" 8 | "testing" 9 | 10 | "github.com/shoenig/test/must" 11 | ) 12 | 13 | func TestWriter_Directf(t *testing.T) { 14 | var a, b bytes.Buffer 15 | 16 | w := New(&a, &b) 17 | w.Printf("foo: %d", 42) 18 | must.Eq(t, "foo: 42\n", a.String()) 19 | must.Eq(t, "", b.String()) 20 | } 21 | 22 | func TestWriter_Errorf(t *testing.T) { 23 | var a, b bytes.Buffer 24 | 25 | w := New(&a, &b) 26 | w.Errorf("foo: %d", 99) 27 | must.Eq(t, "", a.String()) 28 | must.Eq(t, "envy: foo: 99\n", b.String()) 29 | } 30 | -------------------------------------------------------------------------------- /internal/safe/box_mock.go: -------------------------------------------------------------------------------- 1 | package safe 2 | 3 | // Code generated by http://github.com/gojuno/minimock (dev). DO NOT EDIT. 4 | 5 | import ( 6 | "sync" 7 | mm_atomic "sync/atomic" 8 | mm_time "time" 9 | 10 | "github.com/gojuno/minimock/v3" 11 | "github.com/hashicorp/go-set/v3" 12 | ) 13 | 14 | // BoxMock implements Box 15 | type BoxMock struct { 16 | t minimock.Tester 17 | 18 | funcDelete func(s1 string, pp1 *set.Set[string]) (err error) 19 | inspectFuncDelete func(s1 string, pp1 *set.Set[string]) 20 | afterDeleteCounter uint64 21 | beforeDeleteCounter uint64 22 | DeleteMock mBoxMockDelete 23 | 24 | funcGet func(s1 string) (np1 *Profile, err error) 25 | inspectFuncGet func(s1 string) 26 | afterGetCounter uint64 27 | beforeGetCounter uint64 28 | GetMock mBoxMockGet 29 | 30 | funcList func() (sa1 []string, err error) 31 | inspectFuncList func() 32 | afterListCounter uint64 33 | beforeListCounter uint64 34 | ListMock mBoxMockList 35 | 36 | funcPurge func(s1 string) (err error) 37 | inspectFuncPurge func(s1 string) 38 | afterPurgeCounter uint64 39 | beforePurgeCounter uint64 40 | PurgeMock mBoxMockPurge 41 | 42 | funcSet func(np1 *Profile) (err error) 43 | inspectFuncSet func(np1 *Profile) 44 | afterSetCounter uint64 45 | beforeSetCounter uint64 46 | SetMock mBoxMockSet 47 | } 48 | 49 | // NewBoxMock returns a mock for Box 50 | func NewBoxMock(t minimock.Tester) *BoxMock { 51 | m := &BoxMock{t: t} 52 | if controller, ok := t.(minimock.MockController); ok { 53 | controller.RegisterMocker(m) 54 | } 55 | 56 | m.DeleteMock = mBoxMockDelete{mock: m} 57 | m.DeleteMock.callArgs = []*BoxMockDeleteParams{} 58 | 59 | m.GetMock = mBoxMockGet{mock: m} 60 | m.GetMock.callArgs = []*BoxMockGetParams{} 61 | 62 | m.ListMock = mBoxMockList{mock: m} 63 | 64 | m.PurgeMock = mBoxMockPurge{mock: m} 65 | m.PurgeMock.callArgs = []*BoxMockPurgeParams{} 66 | 67 | m.SetMock = mBoxMockSet{mock: m} 68 | m.SetMock.callArgs = []*BoxMockSetParams{} 69 | 70 | return m 71 | } 72 | 73 | type mBoxMockDelete struct { 74 | mock *BoxMock 75 | defaultExpectation *BoxMockDeleteExpectation 76 | expectations []*BoxMockDeleteExpectation 77 | 78 | callArgs []*BoxMockDeleteParams 79 | mutex sync.RWMutex 80 | } 81 | 82 | // BoxMockDeleteExpectation specifies expectation struct of the Box.Delete 83 | type BoxMockDeleteExpectation struct { 84 | mock *BoxMock 85 | params *BoxMockDeleteParams 86 | results *BoxMockDeleteResults 87 | Counter uint64 88 | } 89 | 90 | // BoxMockDeleteParams contains parameters of the Box.Delete 91 | type BoxMockDeleteParams struct { 92 | s1 string 93 | pp1 *set.Set[string] 94 | } 95 | 96 | // BoxMockDeleteResults contains results of the Box.Delete 97 | type BoxMockDeleteResults struct { 98 | err error 99 | } 100 | 101 | // Expect sets up expected params for Box.Delete 102 | func (mmDelete *mBoxMockDelete) Expect(s1 string, pp1 *set.Set[string]) *mBoxMockDelete { 103 | if mmDelete.mock.funcDelete != nil { 104 | mmDelete.mock.t.Fatalf("BoxMock.Delete mock is already set by Set") 105 | } 106 | 107 | if mmDelete.defaultExpectation == nil { 108 | mmDelete.defaultExpectation = &BoxMockDeleteExpectation{} 109 | } 110 | 111 | mmDelete.defaultExpectation.params = &BoxMockDeleteParams{s1, pp1} 112 | for _, e := range mmDelete.expectations { 113 | if minimock.Equal(e.params, mmDelete.defaultExpectation.params) { 114 | mmDelete.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmDelete.defaultExpectation.params) 115 | } 116 | } 117 | 118 | return mmDelete 119 | } 120 | 121 | // Inspect accepts an inspector function that has same arguments as the Box.Delete 122 | func (mmDelete *mBoxMockDelete) Inspect(f func(s1 string, pp1 *set.Set[string])) *mBoxMockDelete { 123 | if mmDelete.mock.inspectFuncDelete != nil { 124 | mmDelete.mock.t.Fatalf("Inspect function is already set for BoxMock.Delete") 125 | } 126 | 127 | mmDelete.mock.inspectFuncDelete = f 128 | 129 | return mmDelete 130 | } 131 | 132 | // Return sets up results that will be returned by Box.Delete 133 | func (mmDelete *mBoxMockDelete) Return(err error) *BoxMock { 134 | if mmDelete.mock.funcDelete != nil { 135 | mmDelete.mock.t.Fatalf("BoxMock.Delete mock is already set by Set") 136 | } 137 | 138 | if mmDelete.defaultExpectation == nil { 139 | mmDelete.defaultExpectation = &BoxMockDeleteExpectation{mock: mmDelete.mock} 140 | } 141 | mmDelete.defaultExpectation.results = &BoxMockDeleteResults{err} 142 | return mmDelete.mock 143 | } 144 | 145 | // Set uses given function f to mock the Box.Delete method 146 | func (mmDelete *mBoxMockDelete) Set(f func(s1 string, pp1 *set.Set[string]) (err error)) *BoxMock { 147 | if mmDelete.defaultExpectation != nil { 148 | mmDelete.mock.t.Fatalf("Default expectation is already set for the Box.Delete method") 149 | } 150 | 151 | if len(mmDelete.expectations) > 0 { 152 | mmDelete.mock.t.Fatalf("Some expectations are already set for the Box.Delete method") 153 | } 154 | 155 | mmDelete.mock.funcDelete = f 156 | return mmDelete.mock 157 | } 158 | 159 | // When sets expectation for the Box.Delete which will trigger the result defined by the following 160 | // Then helper 161 | func (mmDelete *mBoxMockDelete) When(s1 string, pp1 *set.Set[string]) *BoxMockDeleteExpectation { 162 | if mmDelete.mock.funcDelete != nil { 163 | mmDelete.mock.t.Fatalf("BoxMock.Delete mock is already set by Set") 164 | } 165 | 166 | expectation := &BoxMockDeleteExpectation{ 167 | mock: mmDelete.mock, 168 | params: &BoxMockDeleteParams{s1, pp1}, 169 | } 170 | mmDelete.expectations = append(mmDelete.expectations, expectation) 171 | return expectation 172 | } 173 | 174 | // Then sets up Box.Delete return parameters for the expectation previously defined by the When method 175 | func (e *BoxMockDeleteExpectation) Then(err error) *BoxMock { 176 | e.results = &BoxMockDeleteResults{err} 177 | return e.mock 178 | } 179 | 180 | // Delete implements Box 181 | func (mmDelete *BoxMock) Delete(s1 string, pp1 *set.Set[string]) (err error) { 182 | mm_atomic.AddUint64(&mmDelete.beforeDeleteCounter, 1) 183 | defer mm_atomic.AddUint64(&mmDelete.afterDeleteCounter, 1) 184 | 185 | if mmDelete.inspectFuncDelete != nil { 186 | mmDelete.inspectFuncDelete(s1, pp1) 187 | } 188 | 189 | mm_params := &BoxMockDeleteParams{s1, pp1} 190 | 191 | // Record call args 192 | mmDelete.DeleteMock.mutex.Lock() 193 | mmDelete.DeleteMock.callArgs = append(mmDelete.DeleteMock.callArgs, mm_params) 194 | mmDelete.DeleteMock.mutex.Unlock() 195 | 196 | for _, e := range mmDelete.DeleteMock.expectations { 197 | if minimock.Equal(e.params, mm_params) { 198 | mm_atomic.AddUint64(&e.Counter, 1) 199 | return e.results.err 200 | } 201 | } 202 | 203 | if mmDelete.DeleteMock.defaultExpectation != nil { 204 | mm_atomic.AddUint64(&mmDelete.DeleteMock.defaultExpectation.Counter, 1) 205 | mm_want := mmDelete.DeleteMock.defaultExpectation.params 206 | mm_got := BoxMockDeleteParams{s1, pp1} 207 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 208 | mmDelete.t.Errorf("BoxMock.Delete got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 209 | } 210 | 211 | mm_results := mmDelete.DeleteMock.defaultExpectation.results 212 | if mm_results == nil { 213 | mmDelete.t.Fatal("No results are set for the BoxMock.Delete") 214 | } 215 | return (*mm_results).err 216 | } 217 | if mmDelete.funcDelete != nil { 218 | return mmDelete.funcDelete(s1, pp1) 219 | } 220 | mmDelete.t.Fatalf("Unexpected call to BoxMock.Delete. %v %v", s1, pp1) 221 | return 222 | } 223 | 224 | // DeleteAfterCounter returns a count of finished BoxMock.Delete invocations 225 | func (mmDelete *BoxMock) DeleteAfterCounter() uint64 { 226 | return mm_atomic.LoadUint64(&mmDelete.afterDeleteCounter) 227 | } 228 | 229 | // DeleteBeforeCounter returns a count of BoxMock.Delete invocations 230 | func (mmDelete *BoxMock) DeleteBeforeCounter() uint64 { 231 | return mm_atomic.LoadUint64(&mmDelete.beforeDeleteCounter) 232 | } 233 | 234 | // Calls returns a list of arguments used in each call to BoxMock.Delete. 235 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 236 | func (mmDelete *mBoxMockDelete) Calls() []*BoxMockDeleteParams { 237 | mmDelete.mutex.RLock() 238 | 239 | argCopy := make([]*BoxMockDeleteParams, len(mmDelete.callArgs)) 240 | copy(argCopy, mmDelete.callArgs) 241 | 242 | mmDelete.mutex.RUnlock() 243 | 244 | return argCopy 245 | } 246 | 247 | // MinimockDeleteDone returns true if the count of the Delete invocations corresponds 248 | // the number of defined expectations 249 | func (m *BoxMock) MinimockDeleteDone() bool { 250 | for _, e := range m.DeleteMock.expectations { 251 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 252 | return false 253 | } 254 | } 255 | 256 | // if default expectation was set then invocations count should be greater than zero 257 | if m.DeleteMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterDeleteCounter) < 1 { 258 | return false 259 | } 260 | // if func was set then invocations count should be greater than zero 261 | if m.funcDelete != nil && mm_atomic.LoadUint64(&m.afterDeleteCounter) < 1 { 262 | return false 263 | } 264 | return true 265 | } 266 | 267 | // MinimockDeleteInspect logs each unmet expectation 268 | func (m *BoxMock) MinimockDeleteInspect() { 269 | for _, e := range m.DeleteMock.expectations { 270 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 271 | m.t.Errorf("Expected call to BoxMock.Delete with params: %#v", *e.params) 272 | } 273 | } 274 | 275 | // if default expectation was set then invocations count should be greater than zero 276 | if m.DeleteMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterDeleteCounter) < 1 { 277 | if m.DeleteMock.defaultExpectation.params == nil { 278 | m.t.Error("Expected call to BoxMock.Delete") 279 | } else { 280 | m.t.Errorf("Expected call to BoxMock.Delete with params: %#v", *m.DeleteMock.defaultExpectation.params) 281 | } 282 | } 283 | // if func was set then invocations count should be greater than zero 284 | if m.funcDelete != nil && mm_atomic.LoadUint64(&m.afterDeleteCounter) < 1 { 285 | m.t.Error("Expected call to BoxMock.Delete") 286 | } 287 | } 288 | 289 | type mBoxMockGet struct { 290 | mock *BoxMock 291 | defaultExpectation *BoxMockGetExpectation 292 | expectations []*BoxMockGetExpectation 293 | 294 | callArgs []*BoxMockGetParams 295 | mutex sync.RWMutex 296 | } 297 | 298 | // BoxMockGetExpectation specifies expectation struct of the Box.Get 299 | type BoxMockGetExpectation struct { 300 | mock *BoxMock 301 | params *BoxMockGetParams 302 | results *BoxMockGetResults 303 | Counter uint64 304 | } 305 | 306 | // BoxMockGetParams contains parameters of the Box.Get 307 | type BoxMockGetParams struct { 308 | s1 string 309 | } 310 | 311 | // BoxMockGetResults contains results of the Box.Get 312 | type BoxMockGetResults struct { 313 | np1 *Profile 314 | err error 315 | } 316 | 317 | // Expect sets up expected params for Box.Get 318 | func (mmGet *mBoxMockGet) Expect(s1 string) *mBoxMockGet { 319 | if mmGet.mock.funcGet != nil { 320 | mmGet.mock.t.Fatalf("BoxMock.Get mock is already set by Set") 321 | } 322 | 323 | if mmGet.defaultExpectation == nil { 324 | mmGet.defaultExpectation = &BoxMockGetExpectation{} 325 | } 326 | 327 | mmGet.defaultExpectation.params = &BoxMockGetParams{s1} 328 | for _, e := range mmGet.expectations { 329 | if minimock.Equal(e.params, mmGet.defaultExpectation.params) { 330 | mmGet.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmGet.defaultExpectation.params) 331 | } 332 | } 333 | 334 | return mmGet 335 | } 336 | 337 | // Inspect accepts an inspector function that has same arguments as the Box.Get 338 | func (mmGet *mBoxMockGet) Inspect(f func(s1 string)) *mBoxMockGet { 339 | if mmGet.mock.inspectFuncGet != nil { 340 | mmGet.mock.t.Fatalf("Inspect function is already set for BoxMock.Get") 341 | } 342 | 343 | mmGet.mock.inspectFuncGet = f 344 | 345 | return mmGet 346 | } 347 | 348 | // Return sets up results that will be returned by Box.Get 349 | func (mmGet *mBoxMockGet) Return(np1 *Profile, err error) *BoxMock { 350 | if mmGet.mock.funcGet != nil { 351 | mmGet.mock.t.Fatalf("BoxMock.Get mock is already set by Set") 352 | } 353 | 354 | if mmGet.defaultExpectation == nil { 355 | mmGet.defaultExpectation = &BoxMockGetExpectation{mock: mmGet.mock} 356 | } 357 | mmGet.defaultExpectation.results = &BoxMockGetResults{np1, err} 358 | return mmGet.mock 359 | } 360 | 361 | // Set uses given function f to mock the Box.Get method 362 | func (mmGet *mBoxMockGet) Set(f func(s1 string) (np1 *Profile, err error)) *BoxMock { 363 | if mmGet.defaultExpectation != nil { 364 | mmGet.mock.t.Fatalf("Default expectation is already set for the Box.Get method") 365 | } 366 | 367 | if len(mmGet.expectations) > 0 { 368 | mmGet.mock.t.Fatalf("Some expectations are already set for the Box.Get method") 369 | } 370 | 371 | mmGet.mock.funcGet = f 372 | return mmGet.mock 373 | } 374 | 375 | // When sets expectation for the Box.Get which will trigger the result defined by the following 376 | // Then helper 377 | func (mmGet *mBoxMockGet) When(s1 string) *BoxMockGetExpectation { 378 | if mmGet.mock.funcGet != nil { 379 | mmGet.mock.t.Fatalf("BoxMock.Get mock is already set by Set") 380 | } 381 | 382 | expectation := &BoxMockGetExpectation{ 383 | mock: mmGet.mock, 384 | params: &BoxMockGetParams{s1}, 385 | } 386 | mmGet.expectations = append(mmGet.expectations, expectation) 387 | return expectation 388 | } 389 | 390 | // Then sets up Box.Get return parameters for the expectation previously defined by the When method 391 | func (e *BoxMockGetExpectation) Then(np1 *Profile, err error) *BoxMock { 392 | e.results = &BoxMockGetResults{np1, err} 393 | return e.mock 394 | } 395 | 396 | // Get implements Box 397 | func (mmGet *BoxMock) Get(s1 string) (np1 *Profile, err error) { 398 | mm_atomic.AddUint64(&mmGet.beforeGetCounter, 1) 399 | defer mm_atomic.AddUint64(&mmGet.afterGetCounter, 1) 400 | 401 | if mmGet.inspectFuncGet != nil { 402 | mmGet.inspectFuncGet(s1) 403 | } 404 | 405 | mm_params := &BoxMockGetParams{s1} 406 | 407 | // Record call args 408 | mmGet.GetMock.mutex.Lock() 409 | mmGet.GetMock.callArgs = append(mmGet.GetMock.callArgs, mm_params) 410 | mmGet.GetMock.mutex.Unlock() 411 | 412 | for _, e := range mmGet.GetMock.expectations { 413 | if minimock.Equal(e.params, mm_params) { 414 | mm_atomic.AddUint64(&e.Counter, 1) 415 | return e.results.np1, e.results.err 416 | } 417 | } 418 | 419 | if mmGet.GetMock.defaultExpectation != nil { 420 | mm_atomic.AddUint64(&mmGet.GetMock.defaultExpectation.Counter, 1) 421 | mm_want := mmGet.GetMock.defaultExpectation.params 422 | mm_got := BoxMockGetParams{s1} 423 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 424 | mmGet.t.Errorf("BoxMock.Get got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 425 | } 426 | 427 | mm_results := mmGet.GetMock.defaultExpectation.results 428 | if mm_results == nil { 429 | mmGet.t.Fatal("No results are set for the BoxMock.Get") 430 | } 431 | return (*mm_results).np1, (*mm_results).err 432 | } 433 | if mmGet.funcGet != nil { 434 | return mmGet.funcGet(s1) 435 | } 436 | mmGet.t.Fatalf("Unexpected call to BoxMock.Get. %v", s1) 437 | return 438 | } 439 | 440 | // GetAfterCounter returns a count of finished BoxMock.Get invocations 441 | func (mmGet *BoxMock) GetAfterCounter() uint64 { 442 | return mm_atomic.LoadUint64(&mmGet.afterGetCounter) 443 | } 444 | 445 | // GetBeforeCounter returns a count of BoxMock.Get invocations 446 | func (mmGet *BoxMock) GetBeforeCounter() uint64 { 447 | return mm_atomic.LoadUint64(&mmGet.beforeGetCounter) 448 | } 449 | 450 | // Calls returns a list of arguments used in each call to BoxMock.Get. 451 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 452 | func (mmGet *mBoxMockGet) Calls() []*BoxMockGetParams { 453 | mmGet.mutex.RLock() 454 | 455 | argCopy := make([]*BoxMockGetParams, len(mmGet.callArgs)) 456 | copy(argCopy, mmGet.callArgs) 457 | 458 | mmGet.mutex.RUnlock() 459 | 460 | return argCopy 461 | } 462 | 463 | // MinimockGetDone returns true if the count of the Get invocations corresponds 464 | // the number of defined expectations 465 | func (m *BoxMock) MinimockGetDone() bool { 466 | for _, e := range m.GetMock.expectations { 467 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 468 | return false 469 | } 470 | } 471 | 472 | // if default expectation was set then invocations count should be greater than zero 473 | if m.GetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterGetCounter) < 1 { 474 | return false 475 | } 476 | // if func was set then invocations count should be greater than zero 477 | if m.funcGet != nil && mm_atomic.LoadUint64(&m.afterGetCounter) < 1 { 478 | return false 479 | } 480 | return true 481 | } 482 | 483 | // MinimockGetInspect logs each unmet expectation 484 | func (m *BoxMock) MinimockGetInspect() { 485 | for _, e := range m.GetMock.expectations { 486 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 487 | m.t.Errorf("Expected call to BoxMock.Get with params: %#v", *e.params) 488 | } 489 | } 490 | 491 | // if default expectation was set then invocations count should be greater than zero 492 | if m.GetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterGetCounter) < 1 { 493 | if m.GetMock.defaultExpectation.params == nil { 494 | m.t.Error("Expected call to BoxMock.Get") 495 | } else { 496 | m.t.Errorf("Expected call to BoxMock.Get with params: %#v", *m.GetMock.defaultExpectation.params) 497 | } 498 | } 499 | // if func was set then invocations count should be greater than zero 500 | if m.funcGet != nil && mm_atomic.LoadUint64(&m.afterGetCounter) < 1 { 501 | m.t.Error("Expected call to BoxMock.Get") 502 | } 503 | } 504 | 505 | type mBoxMockList struct { 506 | mock *BoxMock 507 | defaultExpectation *BoxMockListExpectation 508 | expectations []*BoxMockListExpectation 509 | } 510 | 511 | // BoxMockListExpectation specifies expectation struct of the Box.List 512 | type BoxMockListExpectation struct { 513 | mock *BoxMock 514 | 515 | results *BoxMockListResults 516 | Counter uint64 517 | } 518 | 519 | // BoxMockListResults contains results of the Box.List 520 | type BoxMockListResults struct { 521 | sa1 []string 522 | err error 523 | } 524 | 525 | // Expect sets up expected params for Box.List 526 | func (mmList *mBoxMockList) Expect() *mBoxMockList { 527 | if mmList.mock.funcList != nil { 528 | mmList.mock.t.Fatalf("BoxMock.List mock is already set by Set") 529 | } 530 | 531 | if mmList.defaultExpectation == nil { 532 | mmList.defaultExpectation = &BoxMockListExpectation{} 533 | } 534 | 535 | return mmList 536 | } 537 | 538 | // Inspect accepts an inspector function that has same arguments as the Box.List 539 | func (mmList *mBoxMockList) Inspect(f func()) *mBoxMockList { 540 | if mmList.mock.inspectFuncList != nil { 541 | mmList.mock.t.Fatalf("Inspect function is already set for BoxMock.List") 542 | } 543 | 544 | mmList.mock.inspectFuncList = f 545 | 546 | return mmList 547 | } 548 | 549 | // Return sets up results that will be returned by Box.List 550 | func (mmList *mBoxMockList) Return(sa1 []string, err error) *BoxMock { 551 | if mmList.mock.funcList != nil { 552 | mmList.mock.t.Fatalf("BoxMock.List mock is already set by Set") 553 | } 554 | 555 | if mmList.defaultExpectation == nil { 556 | mmList.defaultExpectation = &BoxMockListExpectation{mock: mmList.mock} 557 | } 558 | mmList.defaultExpectation.results = &BoxMockListResults{sa1, err} 559 | return mmList.mock 560 | } 561 | 562 | // Set uses given function f to mock the Box.List method 563 | func (mmList *mBoxMockList) Set(f func() (sa1 []string, err error)) *BoxMock { 564 | if mmList.defaultExpectation != nil { 565 | mmList.mock.t.Fatalf("Default expectation is already set for the Box.List method") 566 | } 567 | 568 | if len(mmList.expectations) > 0 { 569 | mmList.mock.t.Fatalf("Some expectations are already set for the Box.List method") 570 | } 571 | 572 | mmList.mock.funcList = f 573 | return mmList.mock 574 | } 575 | 576 | // List implements Box 577 | func (mmList *BoxMock) List() (sa1 []string, err error) { 578 | mm_atomic.AddUint64(&mmList.beforeListCounter, 1) 579 | defer mm_atomic.AddUint64(&mmList.afterListCounter, 1) 580 | 581 | if mmList.inspectFuncList != nil { 582 | mmList.inspectFuncList() 583 | } 584 | 585 | if mmList.ListMock.defaultExpectation != nil { 586 | mm_atomic.AddUint64(&mmList.ListMock.defaultExpectation.Counter, 1) 587 | 588 | mm_results := mmList.ListMock.defaultExpectation.results 589 | if mm_results == nil { 590 | mmList.t.Fatal("No results are set for the BoxMock.List") 591 | } 592 | return (*mm_results).sa1, (*mm_results).err 593 | } 594 | if mmList.funcList != nil { 595 | return mmList.funcList() 596 | } 597 | mmList.t.Fatalf("Unexpected call to BoxMock.List.") 598 | return 599 | } 600 | 601 | // ListAfterCounter returns a count of finished BoxMock.List invocations 602 | func (mmList *BoxMock) ListAfterCounter() uint64 { 603 | return mm_atomic.LoadUint64(&mmList.afterListCounter) 604 | } 605 | 606 | // ListBeforeCounter returns a count of BoxMock.List invocations 607 | func (mmList *BoxMock) ListBeforeCounter() uint64 { 608 | return mm_atomic.LoadUint64(&mmList.beforeListCounter) 609 | } 610 | 611 | // MinimockListDone returns true if the count of the List invocations corresponds 612 | // the number of defined expectations 613 | func (m *BoxMock) MinimockListDone() bool { 614 | for _, e := range m.ListMock.expectations { 615 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 616 | return false 617 | } 618 | } 619 | 620 | // if default expectation was set then invocations count should be greater than zero 621 | if m.ListMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterListCounter) < 1 { 622 | return false 623 | } 624 | // if func was set then invocations count should be greater than zero 625 | if m.funcList != nil && mm_atomic.LoadUint64(&m.afterListCounter) < 1 { 626 | return false 627 | } 628 | return true 629 | } 630 | 631 | // MinimockListInspect logs each unmet expectation 632 | func (m *BoxMock) MinimockListInspect() { 633 | for _, e := range m.ListMock.expectations { 634 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 635 | m.t.Error("Expected call to BoxMock.List") 636 | } 637 | } 638 | 639 | // if default expectation was set then invocations count should be greater than zero 640 | if m.ListMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterListCounter) < 1 { 641 | m.t.Error("Expected call to BoxMock.List") 642 | } 643 | // if func was set then invocations count should be greater than zero 644 | if m.funcList != nil && mm_atomic.LoadUint64(&m.afterListCounter) < 1 { 645 | m.t.Error("Expected call to BoxMock.List") 646 | } 647 | } 648 | 649 | type mBoxMockPurge struct { 650 | mock *BoxMock 651 | defaultExpectation *BoxMockPurgeExpectation 652 | expectations []*BoxMockPurgeExpectation 653 | 654 | callArgs []*BoxMockPurgeParams 655 | mutex sync.RWMutex 656 | } 657 | 658 | // BoxMockPurgeExpectation specifies expectation struct of the Box.Purge 659 | type BoxMockPurgeExpectation struct { 660 | mock *BoxMock 661 | params *BoxMockPurgeParams 662 | results *BoxMockPurgeResults 663 | Counter uint64 664 | } 665 | 666 | // BoxMockPurgeParams contains parameters of the Box.Purge 667 | type BoxMockPurgeParams struct { 668 | s1 string 669 | } 670 | 671 | // BoxMockPurgeResults contains results of the Box.Purge 672 | type BoxMockPurgeResults struct { 673 | err error 674 | } 675 | 676 | // Expect sets up expected params for Box.Purge 677 | func (mmPurge *mBoxMockPurge) Expect(s1 string) *mBoxMockPurge { 678 | if mmPurge.mock.funcPurge != nil { 679 | mmPurge.mock.t.Fatalf("BoxMock.Purge mock is already set by Set") 680 | } 681 | 682 | if mmPurge.defaultExpectation == nil { 683 | mmPurge.defaultExpectation = &BoxMockPurgeExpectation{} 684 | } 685 | 686 | mmPurge.defaultExpectation.params = &BoxMockPurgeParams{s1} 687 | for _, e := range mmPurge.expectations { 688 | if minimock.Equal(e.params, mmPurge.defaultExpectation.params) { 689 | mmPurge.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmPurge.defaultExpectation.params) 690 | } 691 | } 692 | 693 | return mmPurge 694 | } 695 | 696 | // Inspect accepts an inspector function that has same arguments as the Box.Purge 697 | func (mmPurge *mBoxMockPurge) Inspect(f func(s1 string)) *mBoxMockPurge { 698 | if mmPurge.mock.inspectFuncPurge != nil { 699 | mmPurge.mock.t.Fatalf("Inspect function is already set for BoxMock.Purge") 700 | } 701 | 702 | mmPurge.mock.inspectFuncPurge = f 703 | 704 | return mmPurge 705 | } 706 | 707 | // Return sets up results that will be returned by Box.Purge 708 | func (mmPurge *mBoxMockPurge) Return(err error) *BoxMock { 709 | if mmPurge.mock.funcPurge != nil { 710 | mmPurge.mock.t.Fatalf("BoxMock.Purge mock is already set by Set") 711 | } 712 | 713 | if mmPurge.defaultExpectation == nil { 714 | mmPurge.defaultExpectation = &BoxMockPurgeExpectation{mock: mmPurge.mock} 715 | } 716 | mmPurge.defaultExpectation.results = &BoxMockPurgeResults{err} 717 | return mmPurge.mock 718 | } 719 | 720 | // Set uses given function f to mock the Box.Purge method 721 | func (mmPurge *mBoxMockPurge) Set(f func(s1 string) (err error)) *BoxMock { 722 | if mmPurge.defaultExpectation != nil { 723 | mmPurge.mock.t.Fatalf("Default expectation is already set for the Box.Purge method") 724 | } 725 | 726 | if len(mmPurge.expectations) > 0 { 727 | mmPurge.mock.t.Fatalf("Some expectations are already set for the Box.Purge method") 728 | } 729 | 730 | mmPurge.mock.funcPurge = f 731 | return mmPurge.mock 732 | } 733 | 734 | // When sets expectation for the Box.Purge which will trigger the result defined by the following 735 | // Then helper 736 | func (mmPurge *mBoxMockPurge) When(s1 string) *BoxMockPurgeExpectation { 737 | if mmPurge.mock.funcPurge != nil { 738 | mmPurge.mock.t.Fatalf("BoxMock.Purge mock is already set by Set") 739 | } 740 | 741 | expectation := &BoxMockPurgeExpectation{ 742 | mock: mmPurge.mock, 743 | params: &BoxMockPurgeParams{s1}, 744 | } 745 | mmPurge.expectations = append(mmPurge.expectations, expectation) 746 | return expectation 747 | } 748 | 749 | // Then sets up Box.Purge return parameters for the expectation previously defined by the When method 750 | func (e *BoxMockPurgeExpectation) Then(err error) *BoxMock { 751 | e.results = &BoxMockPurgeResults{err} 752 | return e.mock 753 | } 754 | 755 | // Purge implements Box 756 | func (mmPurge *BoxMock) Purge(s1 string) (err error) { 757 | mm_atomic.AddUint64(&mmPurge.beforePurgeCounter, 1) 758 | defer mm_atomic.AddUint64(&mmPurge.afterPurgeCounter, 1) 759 | 760 | if mmPurge.inspectFuncPurge != nil { 761 | mmPurge.inspectFuncPurge(s1) 762 | } 763 | 764 | mm_params := &BoxMockPurgeParams{s1} 765 | 766 | // Record call args 767 | mmPurge.PurgeMock.mutex.Lock() 768 | mmPurge.PurgeMock.callArgs = append(mmPurge.PurgeMock.callArgs, mm_params) 769 | mmPurge.PurgeMock.mutex.Unlock() 770 | 771 | for _, e := range mmPurge.PurgeMock.expectations { 772 | if minimock.Equal(e.params, mm_params) { 773 | mm_atomic.AddUint64(&e.Counter, 1) 774 | return e.results.err 775 | } 776 | } 777 | 778 | if mmPurge.PurgeMock.defaultExpectation != nil { 779 | mm_atomic.AddUint64(&mmPurge.PurgeMock.defaultExpectation.Counter, 1) 780 | mm_want := mmPurge.PurgeMock.defaultExpectation.params 781 | mm_got := BoxMockPurgeParams{s1} 782 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 783 | mmPurge.t.Errorf("BoxMock.Purge got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 784 | } 785 | 786 | mm_results := mmPurge.PurgeMock.defaultExpectation.results 787 | if mm_results == nil { 788 | mmPurge.t.Fatal("No results are set for the BoxMock.Purge") 789 | } 790 | return (*mm_results).err 791 | } 792 | if mmPurge.funcPurge != nil { 793 | return mmPurge.funcPurge(s1) 794 | } 795 | mmPurge.t.Fatalf("Unexpected call to BoxMock.Purge. %v", s1) 796 | return 797 | } 798 | 799 | // PurgeAfterCounter returns a count of finished BoxMock.Purge invocations 800 | func (mmPurge *BoxMock) PurgeAfterCounter() uint64 { 801 | return mm_atomic.LoadUint64(&mmPurge.afterPurgeCounter) 802 | } 803 | 804 | // PurgeBeforeCounter returns a count of BoxMock.Purge invocations 805 | func (mmPurge *BoxMock) PurgeBeforeCounter() uint64 { 806 | return mm_atomic.LoadUint64(&mmPurge.beforePurgeCounter) 807 | } 808 | 809 | // Calls returns a list of arguments used in each call to BoxMock.Purge. 810 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 811 | func (mmPurge *mBoxMockPurge) Calls() []*BoxMockPurgeParams { 812 | mmPurge.mutex.RLock() 813 | 814 | argCopy := make([]*BoxMockPurgeParams, len(mmPurge.callArgs)) 815 | copy(argCopy, mmPurge.callArgs) 816 | 817 | mmPurge.mutex.RUnlock() 818 | 819 | return argCopy 820 | } 821 | 822 | // MinimockPurgeDone returns true if the count of the Purge invocations corresponds 823 | // the number of defined expectations 824 | func (m *BoxMock) MinimockPurgeDone() bool { 825 | for _, e := range m.PurgeMock.expectations { 826 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 827 | return false 828 | } 829 | } 830 | 831 | // if default expectation was set then invocations count should be greater than zero 832 | if m.PurgeMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterPurgeCounter) < 1 { 833 | return false 834 | } 835 | // if func was set then invocations count should be greater than zero 836 | if m.funcPurge != nil && mm_atomic.LoadUint64(&m.afterPurgeCounter) < 1 { 837 | return false 838 | } 839 | return true 840 | } 841 | 842 | // MinimockPurgeInspect logs each unmet expectation 843 | func (m *BoxMock) MinimockPurgeInspect() { 844 | for _, e := range m.PurgeMock.expectations { 845 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 846 | m.t.Errorf("Expected call to BoxMock.Purge with params: %#v", *e.params) 847 | } 848 | } 849 | 850 | // if default expectation was set then invocations count should be greater than zero 851 | if m.PurgeMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterPurgeCounter) < 1 { 852 | if m.PurgeMock.defaultExpectation.params == nil { 853 | m.t.Error("Expected call to BoxMock.Purge") 854 | } else { 855 | m.t.Errorf("Expected call to BoxMock.Purge with params: %#v", *m.PurgeMock.defaultExpectation.params) 856 | } 857 | } 858 | // if func was set then invocations count should be greater than zero 859 | if m.funcPurge != nil && mm_atomic.LoadUint64(&m.afterPurgeCounter) < 1 { 860 | m.t.Error("Expected call to BoxMock.Purge") 861 | } 862 | } 863 | 864 | type mBoxMockSet struct { 865 | mock *BoxMock 866 | defaultExpectation *BoxMockSetExpectation 867 | expectations []*BoxMockSetExpectation 868 | 869 | callArgs []*BoxMockSetParams 870 | mutex sync.RWMutex 871 | } 872 | 873 | // BoxMockSetExpectation specifies expectation struct of the Box.Set 874 | type BoxMockSetExpectation struct { 875 | mock *BoxMock 876 | params *BoxMockSetParams 877 | results *BoxMockSetResults 878 | Counter uint64 879 | } 880 | 881 | // BoxMockSetParams contains parameters of the Box.Set 882 | type BoxMockSetParams struct { 883 | np1 *Profile 884 | } 885 | 886 | // BoxMockSetResults contains results of the Box.Set 887 | type BoxMockSetResults struct { 888 | err error 889 | } 890 | 891 | // Expect sets up expected params for Box.Set 892 | func (mmSet *mBoxMockSet) Expect(np1 *Profile) *mBoxMockSet { 893 | if mmSet.mock.funcSet != nil { 894 | mmSet.mock.t.Fatalf("BoxMock.Set mock is already set by Set") 895 | } 896 | 897 | if mmSet.defaultExpectation == nil { 898 | mmSet.defaultExpectation = &BoxMockSetExpectation{} 899 | } 900 | 901 | mmSet.defaultExpectation.params = &BoxMockSetParams{np1} 902 | for _, e := range mmSet.expectations { 903 | if minimock.Equal(e.params, mmSet.defaultExpectation.params) { 904 | mmSet.mock.t.Fatalf("Expectation set by When has same params: %#v", *mmSet.defaultExpectation.params) 905 | } 906 | } 907 | 908 | return mmSet 909 | } 910 | 911 | // Inspect accepts an inspector function that has same arguments as the Box.Set 912 | func (mmSet *mBoxMockSet) Inspect(f func(np1 *Profile)) *mBoxMockSet { 913 | if mmSet.mock.inspectFuncSet != nil { 914 | mmSet.mock.t.Fatalf("Inspect function is already set for BoxMock.Set") 915 | } 916 | 917 | mmSet.mock.inspectFuncSet = f 918 | 919 | return mmSet 920 | } 921 | 922 | // Return sets up results that will be returned by Box.Set 923 | func (mmSet *mBoxMockSet) Return(err error) *BoxMock { 924 | if mmSet.mock.funcSet != nil { 925 | mmSet.mock.t.Fatalf("BoxMock.Set mock is already set by Set") 926 | } 927 | 928 | if mmSet.defaultExpectation == nil { 929 | mmSet.defaultExpectation = &BoxMockSetExpectation{mock: mmSet.mock} 930 | } 931 | mmSet.defaultExpectation.results = &BoxMockSetResults{err} 932 | return mmSet.mock 933 | } 934 | 935 | // Set uses given function f to mock the Box.Set method 936 | func (mmSet *mBoxMockSet) Set(f func(np1 *Profile) (err error)) *BoxMock { 937 | if mmSet.defaultExpectation != nil { 938 | mmSet.mock.t.Fatalf("Default expectation is already set for the Box.Set method") 939 | } 940 | 941 | if len(mmSet.expectations) > 0 { 942 | mmSet.mock.t.Fatalf("Some expectations are already set for the Box.Set method") 943 | } 944 | 945 | mmSet.mock.funcSet = f 946 | return mmSet.mock 947 | } 948 | 949 | // When sets expectation for the Box.Set which will trigger the result defined by the following 950 | // Then helper 951 | func (mmSet *mBoxMockSet) When(np1 *Profile) *BoxMockSetExpectation { 952 | if mmSet.mock.funcSet != nil { 953 | mmSet.mock.t.Fatalf("BoxMock.Set mock is already set by Set") 954 | } 955 | 956 | expectation := &BoxMockSetExpectation{ 957 | mock: mmSet.mock, 958 | params: &BoxMockSetParams{np1}, 959 | } 960 | mmSet.expectations = append(mmSet.expectations, expectation) 961 | return expectation 962 | } 963 | 964 | // Then sets up Box.Set return parameters for the expectation previously defined by the When method 965 | func (e *BoxMockSetExpectation) Then(err error) *BoxMock { 966 | e.results = &BoxMockSetResults{err} 967 | return e.mock 968 | } 969 | 970 | // Set implements Box 971 | func (mmSet *BoxMock) Set(np1 *Profile) (err error) { 972 | mm_atomic.AddUint64(&mmSet.beforeSetCounter, 1) 973 | defer mm_atomic.AddUint64(&mmSet.afterSetCounter, 1) 974 | 975 | if mmSet.inspectFuncSet != nil { 976 | mmSet.inspectFuncSet(np1) 977 | } 978 | 979 | mm_params := &BoxMockSetParams{np1} 980 | 981 | // Record call args 982 | mmSet.SetMock.mutex.Lock() 983 | mmSet.SetMock.callArgs = append(mmSet.SetMock.callArgs, mm_params) 984 | mmSet.SetMock.mutex.Unlock() 985 | 986 | for _, e := range mmSet.SetMock.expectations { 987 | if minimock.Equal(e.params, mm_params) { 988 | mm_atomic.AddUint64(&e.Counter, 1) 989 | return e.results.err 990 | } 991 | } 992 | 993 | if mmSet.SetMock.defaultExpectation != nil { 994 | mm_atomic.AddUint64(&mmSet.SetMock.defaultExpectation.Counter, 1) 995 | mm_want := mmSet.SetMock.defaultExpectation.params 996 | mm_got := BoxMockSetParams{np1} 997 | if mm_want != nil && !minimock.Equal(*mm_want, mm_got) { 998 | mmSet.t.Errorf("BoxMock.Set got unexpected parameters, want: %#v, got: %#v%s\n", *mm_want, mm_got, minimock.Diff(*mm_want, mm_got)) 999 | } 1000 | 1001 | mm_results := mmSet.SetMock.defaultExpectation.results 1002 | if mm_results == nil { 1003 | mmSet.t.Fatal("No results are set for the BoxMock.Set") 1004 | } 1005 | return (*mm_results).err 1006 | } 1007 | if mmSet.funcSet != nil { 1008 | return mmSet.funcSet(np1) 1009 | } 1010 | mmSet.t.Fatalf("Unexpected call to BoxMock.Set. %v", np1) 1011 | return 1012 | } 1013 | 1014 | // SetAfterCounter returns a count of finished BoxMock.Set invocations 1015 | func (mmSet *BoxMock) SetAfterCounter() uint64 { 1016 | return mm_atomic.LoadUint64(&mmSet.afterSetCounter) 1017 | } 1018 | 1019 | // SetBeforeCounter returns a count of BoxMock.Set invocations 1020 | func (mmSet *BoxMock) SetBeforeCounter() uint64 { 1021 | return mm_atomic.LoadUint64(&mmSet.beforeSetCounter) 1022 | } 1023 | 1024 | // Calls returns a list of arguments used in each call to BoxMock.Set. 1025 | // The list is in the same order as the calls were made (i.e. recent calls have a higher index) 1026 | func (mmSet *mBoxMockSet) Calls() []*BoxMockSetParams { 1027 | mmSet.mutex.RLock() 1028 | 1029 | argCopy := make([]*BoxMockSetParams, len(mmSet.callArgs)) 1030 | copy(argCopy, mmSet.callArgs) 1031 | 1032 | mmSet.mutex.RUnlock() 1033 | 1034 | return argCopy 1035 | } 1036 | 1037 | // MinimockSetDone returns true if the count of the Set invocations corresponds 1038 | // the number of defined expectations 1039 | func (m *BoxMock) MinimockSetDone() bool { 1040 | for _, e := range m.SetMock.expectations { 1041 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 1042 | return false 1043 | } 1044 | } 1045 | 1046 | // if default expectation was set then invocations count should be greater than zero 1047 | if m.SetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterSetCounter) < 1 { 1048 | return false 1049 | } 1050 | // if func was set then invocations count should be greater than zero 1051 | if m.funcSet != nil && mm_atomic.LoadUint64(&m.afterSetCounter) < 1 { 1052 | return false 1053 | } 1054 | return true 1055 | } 1056 | 1057 | // MinimockSetInspect logs each unmet expectation 1058 | func (m *BoxMock) MinimockSetInspect() { 1059 | for _, e := range m.SetMock.expectations { 1060 | if mm_atomic.LoadUint64(&e.Counter) < 1 { 1061 | m.t.Errorf("Expected call to BoxMock.Set with params: %#v", *e.params) 1062 | } 1063 | } 1064 | 1065 | // if default expectation was set then invocations count should be greater than zero 1066 | if m.SetMock.defaultExpectation != nil && mm_atomic.LoadUint64(&m.afterSetCounter) < 1 { 1067 | if m.SetMock.defaultExpectation.params == nil { 1068 | m.t.Error("Expected call to BoxMock.Set") 1069 | } else { 1070 | m.t.Errorf("Expected call to BoxMock.Set with params: %#v", *m.SetMock.defaultExpectation.params) 1071 | } 1072 | } 1073 | // if func was set then invocations count should be greater than zero 1074 | if m.funcSet != nil && mm_atomic.LoadUint64(&m.afterSetCounter) < 1 { 1075 | m.t.Error("Expected call to BoxMock.Set") 1076 | } 1077 | } 1078 | 1079 | // MinimockFinish checks that all mocked methods have been called the expected number of times 1080 | func (m *BoxMock) MinimockFinish() { 1081 | if !m.minimockDone() { 1082 | m.MinimockDeleteInspect() 1083 | 1084 | m.MinimockGetInspect() 1085 | 1086 | m.MinimockListInspect() 1087 | 1088 | m.MinimockPurgeInspect() 1089 | 1090 | m.MinimockSetInspect() 1091 | m.t.FailNow() 1092 | } 1093 | } 1094 | 1095 | // MinimockWait waits for all mocked methods to be called the expected number of times 1096 | func (m *BoxMock) MinimockWait(timeout mm_time.Duration) { 1097 | timeoutCh := mm_time.After(timeout) 1098 | for { 1099 | if m.minimockDone() { 1100 | return 1101 | } 1102 | select { 1103 | case <-timeoutCh: 1104 | m.MinimockFinish() 1105 | return 1106 | case <-mm_time.After(10 * mm_time.Millisecond): 1107 | } 1108 | } 1109 | } 1110 | 1111 | func (m *BoxMock) minimockDone() bool { 1112 | done := true 1113 | return done && 1114 | m.MinimockDeleteDone() && 1115 | m.MinimockGetDone() && 1116 | m.MinimockListDone() && 1117 | m.MinimockPurgeDone() && 1118 | m.MinimockSetDone() 1119 | } 1120 | -------------------------------------------------------------------------------- /internal/safe/namespace.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package safe 5 | 6 | import ( 7 | "fmt" 8 | "sort" 9 | "strings" 10 | ) 11 | 12 | type Encrypted []byte 13 | 14 | type Profile struct { 15 | Name string 16 | Content map[string]Encrypted 17 | } 18 | 19 | func (ns *Profile) String() string { 20 | keys := ns.Keys() 21 | return fmt.Sprintf("(%s [%s])", ns.Name, strings.Join(keys, " ")) 22 | } 23 | 24 | func (ns *Profile) Keys() []string { 25 | keys := make([]string, 0, len(ns.Content)) 26 | for key := range ns.Content { 27 | keys = append(keys, key) 28 | } 29 | sort.Strings(keys) 30 | return keys 31 | } 32 | -------------------------------------------------------------------------------- /internal/safe/namespace_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package safe 5 | 6 | import ( 7 | "testing" 8 | 9 | "github.com/shoenig/test/must" 10 | ) 11 | 12 | func TestProfile_String(t *testing.T) { 13 | pr := &Profile{ 14 | Name: "ns1", 15 | Content: map[string]Encrypted{ 16 | "foo": []byte{1, 1, 1, 1, 1}, 17 | "bar": []byte{2, 2, 2, 2, 2}, 18 | }, 19 | } 20 | s := pr.String() 21 | exp := "(ns1 [bar foo])" 22 | must.Eq(t, exp, s) 23 | } 24 | -------------------------------------------------------------------------------- /internal/safe/safe.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package safe 5 | 6 | import ( 7 | "os" 8 | "path/filepath" 9 | "slices" 10 | "sync" 11 | "time" 12 | 13 | "github.com/hashicorp/go-set/v3" 14 | "github.com/pkg/errors" 15 | "go.etcd.io/bbolt" 16 | ) 17 | 18 | const ( 19 | filename = "envy.safe" 20 | ) 21 | 22 | // Path returns the filepath to the state store (boltdb) used by envoy for 23 | // persisting encrypted secrets. 24 | func Path(dbFile string) (string, error) { 25 | if dbFile != "" { 26 | return dbFile, nil 27 | } 28 | configs, err := os.UserConfigDir() 29 | if err != nil { 30 | return "", errors.Wrap(err, "no user config directory") 31 | } 32 | 33 | dir := filepath.Join(configs, "envy") 34 | if err = os.MkdirAll(dir, 0700); err != nil { 35 | return "", errors.Wrap(err, "unable to not create config directory") 36 | } 37 | 38 | return filepath.Join(dir, filename), nil 39 | } 40 | 41 | // A Box represents the persistent storage of encrypted secrets. 42 | // 43 | //go:generate go run github.com/gojuno/minimock/v3/cmd/minimock@v3.0.10 -g -i Box -s _mock.go 44 | type Box interface { 45 | Set(*Profile) error 46 | Delete(string, *set.Set[string]) error 47 | Purge(string) error 48 | Get(string) (*Profile, error) 49 | List() ([]string, error) 50 | } 51 | 52 | type box struct { 53 | file string 54 | 55 | lock sync.Mutex 56 | database *bbolt.DB 57 | } 58 | 59 | func New(file string) Box { 60 | return &box{ 61 | file: file, 62 | } 63 | } 64 | 65 | func (b *box) open() error { 66 | b.lock.Lock() 67 | defer b.lock.Unlock() 68 | 69 | if b.database != nil { 70 | return errors.New("database already open") 71 | } 72 | 73 | options := &bbolt.Options{ 74 | Timeout: 3 * time.Second, 75 | } 76 | db, err := bbolt.Open(b.file, 0600, options) 77 | if err != nil { 78 | return errors.Wrap(err, "unable to open persistent storage") 79 | } 80 | b.database = db 81 | return nil 82 | } 83 | 84 | func (b *box) close(openErr error) { 85 | if openErr != nil { 86 | panic(openErr) 87 | } 88 | 89 | b.lock.Lock() 90 | defer b.lock.Unlock() 91 | 92 | if b.database == nil { 93 | panic("database already closed") 94 | } 95 | 96 | if err := b.database.Close(); err != nil { 97 | panic(err) 98 | } 99 | 100 | b.database = nil 101 | } 102 | 103 | func bucket(create bool, tx *bbolt.Tx, profile string) (*bbolt.Bucket, error) { 104 | if create { 105 | return tx.CreateBucketIfNotExists([]byte(profile)) 106 | } 107 | if bkt := tx.Bucket([]byte(profile)); bkt != nil { 108 | return bkt, nil 109 | } 110 | return nil, errors.Errorf("profile %q does not exist", profile) 111 | } 112 | 113 | func put(bkt *bbolt.Bucket, pr *Profile) error { 114 | for k, v := range pr.Content { 115 | if err := bkt.Put([]byte(k), []byte(v)); err != nil { 116 | return err 117 | } 118 | } 119 | return nil 120 | } 121 | 122 | // Purge will delete the profile, including any existing content. 123 | func (b *box) Purge(profile string) error { 124 | defer b.close(b.open()) 125 | 126 | return b.database.Update(func(tx *bbolt.Tx) error { 127 | return tx.DeleteBucket([]byte(profile)) 128 | }) 129 | } 130 | 131 | // Set will amend the content of ns. Any overlapping pre-existing values will be 132 | // overwritten. 133 | func (b *box) Set(pr *Profile) error { 134 | defer b.close(b.open()) 135 | 136 | return b.database.Update(func(tx *bbolt.Tx) error { 137 | bkt, err := bucket(true, tx, pr.Name) 138 | if err != nil { 139 | return err 140 | } 141 | return put(bkt, pr) 142 | }) 143 | } 144 | 145 | // Delete will remove keys from profile. 146 | func (b *box) Delete(profile string, keys *set.Set[string]) error { 147 | defer b.close(b.open()) 148 | 149 | return b.database.Update(func(tx *bbolt.Tx) error { 150 | bkt, err := bucket(true, tx, profile) 151 | if err != nil { 152 | return err 153 | } 154 | 155 | for _, key := range keys.Slice() { 156 | if err = bkt.Delete([]byte(key)); err != nil { 157 | return err 158 | } 159 | } 160 | return nil 161 | }) 162 | } 163 | 164 | // Get will return the contents of profile. 165 | func (b *box) Get(profile string) (*Profile, error) { 166 | defer b.close(b.open()) 167 | 168 | content := make(map[string]Encrypted) 169 | if err := b.database.View(func(tx *bbolt.Tx) error { 170 | bkt, err := bucket(false, tx, profile) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | if err = bkt.ForEach(func(k []byte, v []byte) error { 176 | content[string(k)] = Encrypted(slices.Clone(v)) 177 | return nil 178 | }); err != nil { 179 | return err 180 | } 181 | 182 | return nil 183 | }); err != nil { 184 | return nil, err 185 | } 186 | 187 | return &Profile{ 188 | Name: profile, 189 | Content: content, 190 | }, nil 191 | } 192 | 193 | // List will return a list of profile that have been created. 194 | func (b *box) List() ([]string, error) { 195 | defer b.close(b.open()) 196 | 197 | var profiles []string 198 | if err := b.database.View(func(tx *bbolt.Tx) error { 199 | return tx.ForEach(func(ns []byte, _ *bbolt.Bucket) error { 200 | profiles = append(profiles, string(ns)) 201 | return nil 202 | }) 203 | }); err != nil { 204 | return nil, err 205 | } 206 | return profiles, nil 207 | } 208 | -------------------------------------------------------------------------------- /internal/safe/safe_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package safe 5 | 6 | import ( 7 | "runtime" 8 | "testing" 9 | 10 | "github.com/shoenig/test/must" 11 | "github.com/shoenig/test/util" 12 | ) 13 | 14 | var _ Box = (*box)(nil) 15 | 16 | func TestSafe_Path(t *testing.T) { 17 | t.Run("default", func(t *testing.T) { 18 | p, err := Path("") 19 | must.NoError(t, err) 20 | 21 | switch runtime.GOOS { 22 | case "windows": 23 | must.StrHasSuffix(t, `\envy\envy.safe`, p) 24 | default: 25 | must.StrHasSuffix(t, "/envy/envy.safe", p) 26 | } 27 | }) 28 | 29 | t.Run("non-default", func(t *testing.T) { 30 | p, err := Path("/my/custom/path") 31 | must.NoError(t, err) 32 | must.Eq(t, "/my/custom/path", p) 33 | }) 34 | } 35 | 36 | func TestSafe_Set(t *testing.T) { 37 | b := New(util.TempFile(t)) 38 | 39 | _, err := b.Get("does-not-exist") 40 | must.EqError(t, err, "profile \"does-not-exist\" does not exist") 41 | 42 | // set ns1 first time 43 | err = b.Set(&Profile{ 44 | Name: "ns1", 45 | Content: map[string]Encrypted{ 46 | "key1": []byte("value1"), 47 | "key2": []byte("value2"), 48 | }, 49 | }) 50 | must.NoError(t, err) 51 | 52 | // set ns2 first time 53 | err = b.Set(&Profile{ 54 | Name: "ns2", 55 | Content: map[string]Encrypted{ 56 | "keyA": []byte("foo"), 57 | "keyB": []byte("bar"), 58 | }, 59 | }) 60 | must.NoError(t, err) 61 | 62 | ns1, err := b.Get("ns1") 63 | must.NoError(t, err) 64 | must.Eq(t, &Profile{ 65 | Name: "ns1", 66 | Content: map[string]Encrypted{ 67 | "key1": []byte("value1"), 68 | "key2": []byte("value2"), 69 | }, 70 | }, ns1) 71 | 72 | ns2, err := b.Get("ns2") 73 | must.NoError(t, err) 74 | must.Eq(t, &Profile{ 75 | Name: "ns2", 76 | Content: map[string]Encrypted{ 77 | "keyA": []byte("foo"), 78 | "keyB": []byte("bar"), 79 | }, 80 | }, ns2) 81 | 82 | // set ns2 second time 83 | err = b.Set(&Profile{ 84 | Name: "ns1", 85 | Content: map[string]Encrypted{ 86 | "key1": []byte("value1"), 87 | "key2": []byte("value3"), 88 | "key3": []byte("value4"), 89 | }, 90 | }) 91 | must.NoError(t, err) 92 | 93 | ns1, err = b.Get("ns1") 94 | must.NoError(t, err) 95 | must.Eq(t, &Profile{ 96 | Name: "ns1", 97 | Content: map[string]Encrypted{ 98 | "key1": []byte("value1"), 99 | "key2": []byte("value3"), 100 | "key3": []byte("value4"), 101 | }, 102 | }, ns1) 103 | } 104 | 105 | func TestSafe_Purge(t *testing.T) { 106 | b := New(util.TempFile(t)) 107 | 108 | // set ns1 109 | err := b.Set(&Profile{ 110 | Name: "ns1", 111 | Content: map[string]Encrypted{ 112 | "key1": []byte("value1"), 113 | "key2": []byte("value2"), 114 | }, 115 | }) 116 | must.NoError(t, err) 117 | 118 | // ensure ns1 is set 119 | ns1, err := b.Get("ns1") 120 | must.NoError(t, err) 121 | must.Eq(t, &Profile{ 122 | Name: "ns1", 123 | Content: map[string]Encrypted{ 124 | "key1": []byte("value1"), 125 | "key2": []byte("value2"), 126 | }, 127 | }, ns1) 128 | 129 | // purge ns1 130 | err = b.Purge("ns1") 131 | must.NoError(t, err) 132 | 133 | // ensure ns1 is not set anymore 134 | _, err = b.Get("ns1") 135 | must.EqError(t, err, `profile "ns1" does not exist`) 136 | } 137 | 138 | func TestSafe_Update(t *testing.T) { 139 | b := New(util.TempFile(t)) 140 | 141 | // set ns1 142 | err := b.Set(&Profile{ 143 | Name: "ns1", 144 | Content: map[string]Encrypted{ 145 | "key1": []byte("value1"), 146 | "key2": []byte("value2"), 147 | }, 148 | }) 149 | must.NoError(t, err) 150 | 151 | // ensure ns1 is set 152 | ns1, err := b.Get("ns1") 153 | must.NoError(t, err) 154 | must.Eq(t, &Profile{ 155 | Name: "ns1", 156 | Content: map[string]Encrypted{ 157 | "key1": []byte("value1"), 158 | "key2": []byte("value2"), 159 | }, 160 | }, ns1) 161 | 162 | // update ns1 163 | err = b.Set(&Profile{ 164 | Name: "ns1", 165 | Content: map[string]Encrypted{ 166 | "key2": []byte("value2"), 167 | "key3": []byte("value3"), 168 | }, 169 | }) 170 | must.NoError(t, err) 171 | 172 | // ensure ns1 is joined union 173 | ns1, err = b.Get("ns1") 174 | must.NoError(t, err) 175 | must.Eq(t, &Profile{ 176 | Name: "ns1", 177 | Content: map[string]Encrypted{ 178 | "key1": []byte("value1"), 179 | "key2": []byte("value2"), 180 | "key3": []byte("value3"), 181 | }, 182 | }, ns1) 183 | } 184 | -------------------------------------------------------------------------------- /internal/setup/tool.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package setup 5 | 6 | import ( 7 | "github.com/shoenig/envy/internal/keyring" 8 | "github.com/shoenig/envy/internal/output" 9 | "github.com/shoenig/envy/internal/safe" 10 | ) 11 | 12 | const ( 13 | envyKeyringName = "envy.secure.env.vars" 14 | ) 15 | 16 | type Tool struct { 17 | Writer output.Writer 18 | Ring keyring.Ring 19 | Box safe.Box 20 | } 21 | 22 | func New(file string, w output.Writer) *Tool { 23 | dbFile, err := safe.Path(file) 24 | if err != nil { 25 | panic(err) 26 | } 27 | 28 | return &Tool{ 29 | Writer: w, 30 | Ring: keyring.New(keyring.Init(envyKeyringName)), 31 | Box: safe.New(dbFile), 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /internal/setup/tool_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package setup 5 | 6 | import ( 7 | "os" 8 | "testing" 9 | 10 | "github.com/shoenig/envy/internal/output" 11 | "github.com/shoenig/test/must" 12 | "github.com/shoenig/test/util" 13 | "github.com/zalando/go-keyring" 14 | ) 15 | 16 | func init() { 17 | // For tests only, use the mock implementation of the keyring provider. 18 | keyring.MockInit() 19 | } 20 | 21 | func TestTool_New(t *testing.T) { 22 | db := util.TempFile(t) 23 | 24 | tool := New(db, output.New(os.Stdout, os.Stdout)) 25 | must.NotNil(t, tool.Box) 26 | must.NotNil(t, tool.Ring) 27 | must.NotNil(t, tool.Writer) 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // Copyright (c) Seth Hoenig 2 | // SPDX-License-Identifier: MIT 3 | 4 | package main 5 | 6 | import ( 7 | "os" 8 | 9 | "cattlecloud.net/go/babycli" 10 | "github.com/shoenig/envy/internal/commands" 11 | "github.com/shoenig/envy/internal/output" 12 | "github.com/shoenig/envy/internal/setup" 13 | ) 14 | 15 | func main() { 16 | tool := setup.New( 17 | os.Getenv("ENVY_DB_FILE"), 18 | output.New(os.Stdout, os.Stderr), 19 | ) 20 | 21 | args := babycli.Arguments() 22 | rc := commands.Invoke(args, tool) 23 | os.Exit(rc) 24 | } 25 | --------------------------------------------------------------------------------